---
title: "WordPress Airtable Integration: API, Upsert and Limits"
description: "A WordPress Airtable integration on the Web API: personal access tokens, the fields envelope, performUpsert, and the monthly call cap on Free workspaces."
url: "https://wpwebhooks.org/blog/wordpress-airtable-integration/"
date: "2026-09-14"
---

# WordPress Airtable Integration: API, Upsert and Limits

**TL;DR:** Writing WordPress data into Airtable is a small request with a monthly budget most tutorials never mention.

-   Authenticate with a **personal access token** that has the `data.records:write` scope _and_ the base added as a resource. Legacy API keys stopped working on 1 February 2024.
-   Cell values go inside a **`fields`** object. Use `performUpsert` with a merge field so a retried delivery updates the row instead of duplicating it.
-   The rate limit is **5 requests per second per base**, and a 429 costs you a 30-second wait.
-   The limit that actually ends integrations is monthly: **1,000 API calls on Free, 100,000 on Team**, per workspace.

/ Direction

## What does a WordPress Airtable integration actually mean?

Two different jobs share the phrase, and they need different tools. **Airtable into WordPress** treats a base as a content source: rows become posts, products or directory listings. That is what sync plugins such as [Air WP Sync](https://wordpress.org/plugins/air-wp-sync/) do, on a schedule, by reading the base.¹

**WordPress into Airtable** is the reverse and the subject here: something happens on the site, an order, a registration, a published post, a form entry, and a row should appear in a base where the rest of the team works. No polling is involved. WordPress fires a hook, and the integration turns that hook into one HTTP request. If the source is a specific form plugin, the [Contact Form 7 to Airtable walkthrough](https://wpwebhooks.org/blog/contact-form-7-to-airtable/) covers the form-side details; this article is about the Airtable side, which is the same whatever fired the event.

/ Auth

## How do you authenticate to the Airtable API from WordPress?

With a personal access token in a bearer header. Airtable's [authentication reference](https://airtable.com/developers/web/api/authentication) is blunt about the old way: the deprecation period for API keys ended on 1 February 2024, and passing a token through the legacy `api_key` URL parameter is not supported. Any tutorial that shows `?api_key=` predates that and no longer works.

A token needs two things configured separately, and forgetting the second produces a confusing failure. The **scope** says what the token may do; creating and updating rows needs `data.records:write`. The **resource** says where; the base has to be added to the token explicitly. A token with the right scope and no base attached authenticates fine and then cannot see your base.

The token also acts as you. Airtable describes personal access tokens as acting as the user account that created them, limited by scope and resource, and that user needs editor access to the base. OAuth exists for products where other people connect their own Airtable accounts; for your site writing to your base, a personal access token is the intended path, and on Enterprise plans a service account keeps it from depending on one employee.

/ Request

## What does the create-records request look like?

A POST to `https://api.airtable.com/v0/{baseId}/{tableIdOrName}`, with the cell values nested one level down. The [create records reference](https://airtable.com/developers/web/api/create-records) accepts a single record as a top-level `fields` object, or up to 10 at once in a `records` array. Sending the values at the top level of the body, which is what a generic webhook produces by default, returns a 422.

Use the table id, `tbl…`, rather than its name. Both work, and Airtable recommends ids for the reason you would expect: someone renames the table in the UI and a name-based URL starts returning 404 with no change on your side. The same applies to field names, which you can swap for field ids once the integration is stable.

JSON — one record, values inside the fields object

```
{
  "fields": {
    "Email": "anna@example.com",
    "Name": "Anna Kowalska",
    "Order total": 129.5,
    "Source": "woocommerce"
  },
  "typecast": true
}
```

`typecast` is off by default, and the default is the right one for data you control. With it on, Airtable makes a best-effort conversion from strings, which includes creating a new single-select option when the value does not match an existing one. That is convenient for a status field fed by a form and a slow way to collect "Pending", "pending" and "PENDING" as three options. Turn it on knowingly. Numbers are the other trap: a number field rejects `"129.50"` as a string without typecast, so cast on the WordPress side.

FIG 01 — A WordPress event becomes one upsert: the merge field decides update or create, the budget decides whether it runs at all

/ Upsert

## How do you stop retries from creating duplicate rows?

With `performUpsert`. Create is not idempotent: deliver the same event twice, because a timeout hid a success, and you get two rows. The [update multiple records](https://airtable.com/developers/web/api/update-multiple-records) endpoint, a PATCH on the same table URL, has an upsert mode that closes that gap. You name one to three fields in `fieldsToMergeOn`, and Airtable uses them as an external id:

-   no match: a record is created;
-   one match: that record is updated;
-   **more than one match: the request fails.**

The third case is the one to plan for. Upsert does not clean up duplicates that already exist; it refuses to choose between them. De-duplicate the merge field before you switch an existing base over. The merge fields cannot be computed fields, so no formulas, lookups or rollups, and must be a number, text, long text, single select, multiple select or date. An order id or a lowercase email in a plain text field is the usual choice.

PHP — upsert an order row, keyed on the order id

```
function at_upsert_order( $order ) {
    $url = 'https://api.airtable.com/v0/' . AIRTABLE_BASE_ID . '/' . AIRTABLE_TABLE_ID;

    $res = wp_remote_request( $url, [
        'method'  => 'PATCH',   // PATCH leaves unsent fields alone. PUT clears them.
        'timeout' => 10,
        'headers' => [
            'Authorization' => 'Bearer ' . AIRTABLE_PAT,
            'Content-Type'  => 'application/json',
        ],
        'body'    => wp_json_encode( [
            'performUpsert' => [ 'fieldsToMergeOn' => [ 'Order ID' ] ],
            'records'       => [ [
                'fields' => [
                    'Order ID'    => (string) $order->get_id(),
                    'Email'       => strtolower( $order->get_billing_email() ),
                    'Order total' => (float) $order->get_total(),
                    'Status'      => $order->get_status(),
                ],
            ] ],
        ] ),
    ] );

    if ( is_wp_error( $res ) ) {
        return $res;
    }
    $code = wp_remote_retrieve_response_code( $res );
    $body = json_decode( wp_remote_retrieve_body( $res ), true );

    // createdRecords / updatedRecords tell you which branch Airtable took.
    return [ 'code' => $code, 'created' => $body['createdRecords'] ?? [], 'updated' => $body['updatedRecords'] ?? [] ];
}
```

Mind the verb. On this endpoint PATCH updates only the fields you send, while PUT performs what Airtable calls a destructive update and clears every cell you left out. An integration that sends four fields with PUT will blank the notes column your colleague filled in by hand.

/ Limits

## What are Airtable's API limits, and which one will you hit?

There are two, and the one in the API reference is not the one that ends integrations.

The [rate limit](https://airtable.com/developers/web/api/rate-limits) is 5 requests per second per base, plus 50 per second across all traffic from one user's tokens. Exceed it and you receive a 429 and must wait **30 seconds** before requests succeed again. For event-driven writes that is generous: one row per event at 5 a second is 300 a minute.

The monthly cap is the real constraint. Airtable's [API call limits](https://support.airtable.com/articles/7735693959-managing-api-call-limits-in-airtable) are set per workspace plan: **1,000 calls a month on Free and 100,000 on Team**.² Put a real site against the Free number:

-   1,000 ÷ 30 days = **33 calls a day**.
-   A store with 40 orders a day, one upsert each, plus a status update when each order completes: 40 × 2 = 80 calls a day × 30 = **2,400 a month**, 2.4 times the allowance.
-   The same store on Team: 2,400 ÷ 100,000 = **2.4%** of the budget.

What happens at the cap differs by plan, and the Free behaviour is the dangerous one. The first time a Free workspace goes over, a 30-day grace period starts and the API keeps working. That grace period is available once. After it, calls over the limit are blocked until the month resets on the first day of the calendar month. So the integration works through launch, works through the grace month, and then stops for the last third of some later month with nothing changed on your side. On Team, going over slows calls to 2 requests per second instead of blocking them.

Batching is the lever. One request can carry 10 records and still counts as one call, so a worker that drains a queue every few minutes and writes rows in tens turns 2,400 calls into a few hundred.

| Concern | Hand-rolled wp\_remote\_request | Webhook Actions |
| --- | --- | --- |
| Reading Airtable back into WordPress | A different job; use a sync plugin | Not what it does. It sends events out; it does not import rows |
| Batching ten rows per call | Possible, if you build the buffer | Not available. One event is one request, so the monthly budget is spent one call per event |
| The fields envelope | Written in your array | Field mapping with dotted targets such as fields.Email, no code |
| performUpsert and other constants | Written in your array | A short pre-dispatch snippet; mapping moves values and cannot invent one |
| The token | A constant in wp-config.php | An encrypted Bearer credential, redacted in every log |
| A 429 | Lost unless you wrote retry logic | Retried; the first retry lands after a minute, outside the 30-second penalty |
| A 422 from a renamed field | Invisible until someone notices missing rows | Marked permanently failed with Airtable's error body in the delivery log, replayable once fixed |

try\_it

Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.

[Try the live preview →](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) [Install plugin](https://downloads.wordpress.org/plugin/flowsystems-webhook-actions.zip)

/ Failure

## Should the Airtable request run inside the WordPress request?

No, and the 30-second penalty is the reason it matters more here than with most APIs. An inline call that hits a 429 has two options, both bad: fail and lose the row, or sleep and hold a PHP worker, and the visitor, for half a minute. Record the event when the hook fires, return immediately, and let a background worker write to Airtable. The upsert makes the retry safe, so the worker can be simple.

Sort the failures before you retry them. A 429 and a 5xx are temporary; wait and resend. A 422 is a payload Airtable will reject identically forever, a 401 is a revoked token, and a 403 is a token without the base attached. Retrying those spends calls from a monthly budget and changes nothing. Whatever you build, keep the response body: Airtable's error objects name the field and the reason, and that one line is the difference between a two-minute fix and [an integration that failed silently for weeks](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/).

![Cyberpunk illustration of a masked man with a chrome arm stretched sideways off a tipping library ladder in a dark card-index hall, fingertips just short of one open drawer under the last lit mint reading lamp.](https://wpwebhooks.org/blog/wordpress-airtable-integration/og_image.jpg)

/ Exposure

## What does Airtable not protect you from?

**Schema drift by a colleague.** The base is a spreadsheet to everyone else. Renaming a field or changing its type is one click, and your next request returns 422. Field and table ids survive renames; type changes do not, so log the response body and alert on the first 422 rather than the hundredth.

**A public form is a write path into your base.** If an unauthenticated form triggers the request, a bot can spend your monthly call budget in an afternoon, and on Free that also burns the one grace period you will ever get. Rate-limit and spam-filter before the event is queued, not after.

**The token outlives its purpose.** A personal access token has no expiry you did not set. Scope it to the one base, give it only `data.records:write` if the site never reads, and keep it out of the theme and out of the repository.

**Record limits are separate.** The call budget governs requests; each plan also caps records per base. An integration that only ever appends will reach that ceiling eventually, so decide early whether old rows are archived, and where.

The same event can feed other destinations with the same shape of request: [Google Sheets](https://wpwebhooks.org/blog/wordpress-form-to-google-sheets/) for a flat log, [Notion](https://wpwebhooks.org/blog/contact-form-7-to-notion/) for a team wiki, or a published build that [records newly published posts into Airtable](https://wpwebhooks.org/integrations/record-newly-published-posts-into-airtable/) without writing any of the code above.

/Footnotes

¹ Air WP Sync on [WordPress.org](https://wordpress.org/plugins/air-wp-sync/), read 2026-09-19: 1,000+ active installations, version 2.9.0. Its direction is Airtable to WordPress.

² Airtable support, [Managing API call limits](https://support.airtable.com/articles/7735693959-managing-api-call-limits-in-airtable), read 2026-09-19: monthly limits for Free and Team, the one-time 30-day grace period, the reset on the first day of the calendar month, and batching at up to 10 records per request.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WordPress Airtable Integration: API, Upsert and Limits","description":"A WordPress Airtable integration on the Web API: personal access tokens, the fields envelope, performUpsert, and the monthly call cap on Free workspaces.","datePublished":"2026-09-14","dateModified":"2026-09-14","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/wordpress-airtable-integration/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/blog/wordpress-airtable-integration/og_image.jpg","width":1200,"height":630,"caption":"Cyberpunk illustration of a masked man with a chrome arm stretched sideways off a tipping library ladder in a dark card-index hall, fingertips just short of one open drawer under the last lit mint reading lamp."},"keywords":["wordpress airtable integration","wordpress airtable","airtable wordpress integration","woocommerce airtable integration","wordpress to airtable","airtable api wordpress"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WordPress Airtable Integration: API, Upsert and Limits","item":"https://wpwebhooks.org/blog/wordpress-airtable-integration/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is there an official Airtable plugin for WordPress?","acceptedAnswer":{"@type":"Answer","text":"No. Airtable does not publish a WordPress plugin. Third-party sync plugins such as Air WP Sync read an Airtable base and create WordPress content from it, which is the Airtable to WordPress direction. Sending WordPress events into a base is done through the Airtable Web API with a personal access token."}},{"@type":"Question","name":"How do I authenticate to the Airtable API from WordPress?","acceptedAnswer":{"@type":"Answer","text":"Send a personal access token in an Authorization: Bearer header. The token needs the data.records:write scope and the target base added to it as a resource, and the user who created it needs editor access to that base. Legacy Airtable API keys stopped working when their deprecation period ended on 1 February 2024."}},{"@type":"Question","name":"What are the Airtable API rate limits?","acceptedAnswer":{"@type":"Answer","text":"Five requests per second per base, and 50 per second across all traffic from one user or service account using personal access tokens. Exceeding either returns a 429, after which you must wait 30 seconds before requests succeed again. Separately, Free workspaces are capped at 1,000 API calls a month and Team workspaces at 100,000."}},{"@type":"Question","name":"What happens when a Free Airtable workspace exceeds 1,000 API calls a month?","acceptedAnswer":{"@type":"Answer","text":"The first time, a 30-day grace period starts and the API keeps working. That grace period is available only once. After it ends, API calls over the limit are blocked until the limit resets on the first day of the next calendar month. On the Team plan, exceeding the limit slows calls to 2 requests per second instead."}},{"@type":"Question","name":"How do I avoid duplicate Airtable records when a request is retried?","acceptedAnswer":{"@type":"Answer","text":"Use the update multiple records endpoint with performUpsert and one to three fieldsToMergeOn, such as an order id. Airtable creates a record when nothing matches, updates it when one record matches, and fails the request when more than one matches. The merge fields cannot be formulas, lookups or rollups."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/wordpress-airtable-integration.png","caption":"FIG 01 — A WordPress event becomes one upsert: the merge field decides update or create, the budget decides whether it runs at all","description":"A WordPress hook fires and the delivery is queued. The worker sends a PATCH to the Airtable table URL with a bearer personal access token, a performUpsert object naming the merge field, and a records array whose values sit inside a fields object. Airtable looks for records matching the merge field. No match creates a record, one match updates it, and more than one match fails the request. A 429 means the limit of five requests per second per base was exceeded and requires a thirty second wait. A 422 means a field was renamed or a value has the wrong type and should not be retried. Every call also counts against a monthly workspace budget of 1,000 calls on the Free plan or 100,000 on Team.","encodingFormat":"image/png","creator":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org/"},"copyrightHolder":{"@type":"Organization","name":"Flow Systems","url":"https://flowsystems.pl/"},"copyrightNotice":"© Flow Systems","creditText":"WP Webhooks","license":"https://creativecommons.org/licenses/by/4.0/","acquireLicensePage":"https://wpwebhooks.org/image-license/"}
```
