---
title: "Contact Form 7 to Airtable: Send Entries With the API"
description: "Send Contact Form 7 entries to Airtable with the REST API: personal access token auth, field mapping, typecast, the 10-record cap and 5 req/s limit."
url: "https://wpwebhooks.org/blog/contact-form-7-to-airtable/"
date: "2026-08-20"
---

# Contact Form 7 to Airtable: Send Entries With the API

**TL;DR:**

-   `wpcf7_mail_sent` hands you the form object, not the data — read values with `WPCF7_Submission::get_instance()->get_posted_data()`.
-   Airtable creates records at `POST https://api.airtable.com/v0/{baseId}/{tableIdOrName}` with a personal access token as a bearer credential.
-   Every key you send must already exist as a field in the table. Airtable rejects the whole request otherwise — it never creates columns for you.
-   The hard ceilings: **10 records per request**, **5 requests per second per base**, and a **30-second** wait after a 429.
-   Sending inline makes the visitor wait for Airtable and loses the entry if it is down. Queue it.

/ Overview

## How do you send Contact Form 7 entries to Airtable?

Hook `wpcf7_mail_sent`, read the submitted values out of `WPCF7_Submission`, map each form-tag name onto the matching Airtable field name, and POST the result to Airtable's create-records endpoint with a personal access token. There is no official Contact Form 7 add-on for this and no Airtable-side listener — the whole integration is one hook and one HTTP request.

The part that catches people is not the request. It is that **Airtable will not invent columns**. A form tag called `your-message` does not become a field called `your-message`; if that field is not already in the table, the API answers 422 and nothing is stored. The mapping step is the integration.

/ The hook

## Which Contact Form 7 hook gives you the submitted data?

`wpcf7_mail_sent` fires after the mail component has successfully sent, and it receives **one** argument — the `WPCF7_ContactForm` object. That object describes the form, not the submission, which is why reading `$contact_form` for field values returns nothing useful.

The submitted values live on the submission singleton instead. We cover the hook itself in depth in the [wpcf7\_mail\_sent reference](https://wpwebhooks.org/blog/wordpress-wpcf7-mail-sent-hook/); the short version is below, verified against Contact Form 7 6.1.6.¹

PHP — reading the submission

```
add_action( 'wpcf7_mail_sent', function( $contact_form ) {

    $submission = WPCF7_Submission::get_instance();
    if ( ! $submission ) {
        return; // no submission in scope — nothing to send
    }

    $data = $submission->get_posted_data();
    // $data is a flat array keyed by form-tag name:
    // [ 'your-name' => 'Ada', 'your-email' => 'ada@example.com', ... ]

    // Only act on the form you mean — get_posted_data() is shared.
    if ( 42 !== $contact_form->id() ) {
        return;
    }

    my_queue_airtable_record( $data );
} );
```

Two details matter. `get_posted_data()` returns values that Contact Form 7 has already sanitised, but it returns _every_ field including hidden ones and the CF7 internals — pass through only the keys you mean. And the callback runs for every form on the site, so gate on `$contact_form->id()` unless you genuinely want all of them.

FIG 01 — One Contact Form 7 submission becoming an Airtable record

/ The request

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

One POST, one bearer token, one JSON body. The endpoint is `https://api.airtable.com/v0/{baseId}/{tableIdOrName}` — the base ID starts with `app`, and the table segment accepts either the table ID (starting `tbl`) or its display name, URL-encoded.²

PHP — creating the record

```
function my_send_to_airtable( array $fields ) {

    $base  = 'appXXXXXXXXXXXXXX';
    $table = rawurlencode( 'Leads' );

    $response = wp_remote_post(
        "https://api.airtable.com/v0/{$base}/{$table}",
        [
            'timeout' => 15,
            'headers' => [
                'Authorization' => 'Bearer ' . MY_AIRTABLE_PAT,
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode( [
                'records'  => [ [ 'fields' => $fields ] ],
                'typecast' => true,
            ] ),
        ]
    );

    if ( is_wp_error( $response ) ) {
        return $response; // transport failure — retry this one
    }

    $code = wp_remote_retrieve_response_code( $response );
    return 200 === $code ? true : $code;
}
```

`typecast: true` is the flag worth understanding. With it off — the default — Airtable requires values in exactly the right shape, so posting the string `"3"` to a number field fails. With it on, Airtable does "best-effort automatic data conversion from string values", which is what you want when the source is a web form, because **every value from an HTML form arrives as a string**. It will also create new select options on the fly rather than rejecting them.

/ Mapping

## How do you map form-tag names onto Airtable fields?

Explicitly, in one array, and nowhere else. The temptation is to name the CF7 form tags after the Airtable columns and pass the array straight through. That breaks the first time somebody renames a column in Airtable or adds a field to the form, and it fails silently in the direction that loses data.

PHP — an explicit map

```
// form-tag name  =>  Airtable field name
$map = [
    'your-name'    => 'Name',
    'your-email'   => 'Email',
    'your-subject' => 'Subject',
    'your-message' => 'Message',
];

$fields = [];
foreach ( $map as $tag => $column ) {
    if ( ! isset( $data[ $tag ] ) ) {
        continue;
    }
    $value = $data[ $tag ];

    // CF7 gives arrays for checkboxes and multi-selects
    $fields[ $column ] = is_array( $value )
        ? implode( ', ', $value )
        : (string) $value;
}

$fields['Submitted'] = current_time( 'c' ); // ISO 8601 with offset
```

Checkbox and multi-select tags come back as PHP arrays. Airtable will accept an array for a multiple-select field, but a plain text field wants a string — flattening with `implode()` is the safe default until you know the column type.

> Airtable never creates a column for you. If the field name in your payload is not already in the table, the entire record is rejected — not the field.

/ Limits

## What are Airtable's real limits?

These are the numbers that decide your architecture, all from Airtable's own API reference.³

| Limit | Value | What happens at the boundary |
| --- | --- | --- |
| Records per create request | 10 | Request rejected — split into batches of 10 |
| Requests per second, per base | 5 | 429 returned |
| Requests per second, per token | 50 | 429 returned, across every base |
| Wait after a 429 | 30 seconds | Earlier retries keep failing |
| Field must already exist | always | 422 — the whole record is dropped |

Run the arithmetic before you decide this is generous. Five requests per second per base is 5 × 60 = **300 records a minute** if you send one record per request, or 3,000 a minute batched at 10. A contact form will never approach that. A WooCommerce store replaying 8,000 historical orders will hit it in the first 30 seconds — 8,000 ÷ 10 = 800 requests, and at 5/s that is 160 seconds of sustained traffic with no headroom for anything else touching the same base.

/ Gaps

## What does Airtable not protect you from?

The API is well behaved. The gaps are all on your side of the wire:

-   **No idempotency key.** There is no request header that says "this is the same record as before". Post twice and you get two records. Any retry you write must decide for itself whether the previous attempt landed — the cheapest fix is a deterministic key column you can search before inserting.
-   **The token is a bearer credential with broad reach.** A personal access token scoped to a base can read every table in it, not just the one you write to. Scope it to the single base, give it only `data.records:write`, and keep it out of the database and out of version control.
-   **A 200 is not a validated record.** With `typecast` on, a malformed date can be coerced into something Airtable accepts but you did not mean. Validate before sending, not after.
-   **Schema drift is silent until it is fatal.** Nobody tells WordPress that a column was renamed. The first sign is a run of 422s in a log you are probably not reading.

/ Delivery

## Should the request run inline or on a queue?

Queue it. The inline version — calling Airtable directly inside `wpcf7_mail_sent` — ties the visitor's thank-you message to a third party being fast and reachable. With `'timeout' => 15`, an Airtable incident adds up to 15 seconds to a form submission, and if the request fails there is no second attempt: the entry is gone, because the only copy was in a PHP variable.

This is the same failure shape we walked through in [why WordPress webhooks silently fail in production](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/). The fix is not a longer timeout, it is moving the send off the request.

| Concern | Inline wp\_remote\_post | Queued delivery |
| --- | --- | --- |
| Visitor wait | Blocks until Airtable answers | Returns immediately |
| Airtable down | Entry lost — no copy kept | Stays queued, retried later |
| 429 handling | One attempt, then gone | Backs off and retries |
| Evidence | Nothing unless you log it | Per-attempt request and response log |

/ Retries

## How do you handle a 429 without losing the entry?

Treat 429 and 5xx as retryable and everything else as final. Airtable is explicit that after a rate-limit breach you must **wait 30 seconds** before requests succeed again, so a retry one second later is guaranteed to fail and burns an attempt.

A 422 is the opposite case: the field name is wrong or the value is invalid, and retrying the identical payload will fail identically forever. Send it to a dead-letter list a human reads. The general shape of a backoff that behaves — and why capping it matters — is covered in the [retry policy and exponential backoff](https://wpwebhooks.org/blog/webhook-retry-policy-exponential-backoff/) write-up.

**Try it without writing the plumbing.** If you would rather map the fields in an admin screen than maintain the hook, [open the live preview](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) — a full WordPress with Webhook Actions already installed, no signup and nothing to install — or [install the free plugin](https://wordpress.org/plugins/flowsystems-webhook-actions/).

/Footnotes

¹ Hook signature `do_action( 'wpcf7_mail_sent', $contact_form )` and the `get_posted_data()` / `get_instance()` methods verified directly in `includes/submission.php` of [Contact Form 7](https://wordpress.org/plugins/contact-form-7/) 6.1.6. First-party documentation: [contactform7.com/docs](https://contactform7.com/docs/).

² Endpoint, bearer auth, request body and the `typecast` option: [Airtable create records API](https://airtable.com/developers/web/api/create-records).

³ Five requests per second per base, fifty per token, and the thirty-second wait after a 429: [Airtable rate limits](https://airtable.com/developers/web/api/rate-limits).

⁴ WordPress HTTP helpers used above: [wp\_remote\_post()](https://developer.wordpress.org/reference/functions/wp_remote_post/) and [is\_wp\_error()](https://developer.wordpress.org/reference/functions/is_wp_error/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"Contact Form 7 to Airtable: Send Entries With the API","description":"Send Contact Form 7 entries to Airtable with the REST API: personal access token auth, field mapping, typecast, the 10-record cap and 5 req/s limit.","datePublished":"2026-08-20","dateModified":"2026-08-20","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/contact-form-7-to-airtable/","image":"https://wpwebhooks.org/og_image.jpg","keywords":["cf7 airtable","contact form 7 airtable","contact form 7 to airtable","wordpress airtable integration","airtable api wordpress","wordpress airtable plugin"]}

{"@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":"Contact Form 7 to Airtable: Send Entries With the API","item":"https://wpwebhooks.org/blog/contact-form-7-to-airtable/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Can Contact Form 7 send entries to Airtable without a plugin?","acceptedAnswer":{"@type":"Answer","text":"Yes. Hook wpcf7_mail_sent, read the values with WPCF7_Submission::get_instance()->get_posted_data(), map them onto your Airtable field names and POST to https://api.airtable.com/v0/{baseId}/{tableIdOrName} with a personal access token. No add-on is required on either side."}},{"@type":"Question","name":"Why does Airtable return a 422 when I create a record?","acceptedAnswer":{"@type":"Answer","text":"Almost always because a key in your payload is not an existing field in that table. Airtable never creates columns for you, and it rejects the entire record rather than the unknown field. Check for typos and for columns that were renamed in the Airtable UI after the integration was written."}},{"@type":"Question","name":"What does typecast do in the Airtable API?","acceptedAnswer":{"@type":"Answer","text":"With typecast set to true, Airtable performs best-effort automatic conversion from string values — so the string \"3\" can go into a number field and a new select option is created rather than rejected. It defaults to false. Because every value from an HTML form arrives as a string, typecast is normally what you want for form data."}},{"@type":"Question","name":"How many records can I create in one Airtable request?","acceptedAnswer":{"@type":"Answer","text":"Ten. Larger imports must be split into batches of 10, and those batches are still bound by the rate limit of 5 requests per second per base."}},{"@type":"Question","name":"What is the Airtable rate limit and what happens if I exceed it?","acceptedAnswer":{"@type":"Answer","text":"Five requests per second per base, and 50 per second across all traffic from one personal access token. Exceeding it returns HTTP 429, after which you must wait 30 seconds before requests succeed again — retrying sooner keeps failing."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/contact-form-7-to-airtable.png","caption":"FIG 01 — One Contact Form 7 submission becoming an Airtable record","description":"Contact Form 7 fires wpcf7_mail_sent after the mail component succeeds, passing only the contact form object. The handler calls WPCF7_Submission::get_instance and get_posted_data to read the submitted values, then maps each form tag name onto an Airtable field name, because Airtable rejects any field it does not recognise. The record is posted to the create-records endpoint with a personal access token. Airtable allows five requests per second per base and answers a breach with 429, after which the caller must wait thirty seconds. Sending inline blocks the visitor and loses the record on failure, so the mapped payload belongs on a queue that retries.","encodingFormat":"image/png","creditText":"WP Webhooks","license":"https://wpwebhooks.org/"}
```
