WP Webhooks / Blog / WordPress integrations
Article · WordPress integrations

Sending Contact Form 7 Submissions Into an Airtable Base

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.

8 min 2026-08-20
#contact-form-7#airtable#integrations

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; 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' => '[email protected]', ... ]

    // 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.³

LimitValueWhat happens at the boundary
Records per create request10Request rejected — split into batches of 10
Requests per second, per base5429 returned
Requests per second, per token50429 returned, across every base
Wait after a 42930 secondsEarlier retries keep failing
Field must already existalways422 — 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. The fix is not a longer timeout, it is moving the send off the request.

ConcernInline wp_remote_postQueued delivery
Visitor waitBlocks until Airtable answersReturns immediately
Airtable downEntry lost — no copy keptStays queued, retried later
429 handlingOne attempt, then goneBacks off and retries
EvidenceNothing unless you log itPer-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 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 — a full WordPress with Webhook Actions already installed, no signup and nothing to install — or install the free plugin.

/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 6.1.6. First-party documentation: contactform7.com/docs.
² Endpoint, bearer auth, request body and the typecast option: Airtable create records API.
³ Five requests per second per base, fifty per token, and the thirty-second wait after a 429: Airtable rate limits.
WordPress HTTP helpers used above: wp_remote_post() and is_wp_error().
FAQ

Things engineers always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Can Contact Form 7 send entries to Airtable without a plugin? +
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.
Why does Airtable return a 422 when I create a record? +
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.
What does typecast do in the Airtable API? +
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.
How many records can I create in one Airtable request? +
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.
What is the Airtable rate limit and what happens if I exceed it? +
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.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate