Gareth Wilson Gareth Wilson

Guide to Airtable Webhooks: Features and Best Practices

Published


Airtable webhooks notify your application when data or schema changes in a base: a record is created, a cell is updated, a field is added. What they don't do is tell you what changed. The notification is a thin ping, and you fetch the actual changes from a separate payloads API. That two-step model, plus a hard 7-day expiry, shapes how you build an Airtable integration.

This guide covers how Airtable webhooks work, the thin-ping-plus-payloads model, how to verify the X-Airtable-Content-MAC, the expiry and cursor mechanics, and the best practices for running them in production.

What are Airtable webhooks?

An Airtable webhook sends a lightweight POST to your notification URL when a change matching its specification occurs in a base. The ping contains only base.id, webhook.id, and a timestamp, so it tells you that something changed, not what. To get the changes you call the list-payloads endpoint with a cursor and page through the transactions since you last checked.

Webhooks are created through the Airtable Web API, and each one returns a MAC secret once at creation that you use to verify the pings.

Airtable webhook features

FeatureDetails
ConfigurationPOST /v0/bases/{baseId}/webhooks (Web API)
NotificationThin ping: base.id, webhook.id, timestamp only
Fetching changesGET /v0/bases/{baseId}/webhooks/{webhookId}/payloads with a cursor
Verification headerX-Airtable-Content-MAC
Signature schemehmac-sha256= + hex HMAC-SHA256 over the raw body, keyed with the base64-decoded macSecretBase64
SecretmacSecretBase64 returned once at creation; not retrievable later
Ping response200 or 204 with an empty body, within ~25 seconds
ExpiryPAT/OAuth webhooks expire after 7 days (refresh or list-payloads extends)
Payload retentionDeleted server-side after ~1 week
Rate limit5 requests/sec per base (shared); 429 -> wait 30s
SDKpyairtable (community) supports webhooks; npm airtable does not

The thin-ping-plus-payloads model

The notification ping is deliberately minimal. Your handler's job on receipt is to verify the ping and then fetch the changes:

  1. A change happens in the base; Airtable POSTs a ping (base.id, webhook.id, timestamp).
  2. You verify the X-Airtable-Content-MAC.
  3. You call the list-payloads endpoint with your stored cursor.
  4. Airtable returns the payloads plus a new cursor and mightHaveMore (page again while true, up to the per-page limit of 50).
  5. You persist the new cursor for next time.

A webhook's specification (set at creation) controls what changes it reports: dataTypes (tableData, tableFields, tableMetadata), changeTypes (add, remove, update), and scoping like recordChangeScope.

Setting up Airtable webhooks

Create a webhook with the Web API, providing a notificationUrl and a specification:

curl -X POST "https://api.airtable.com/v0/bases/{baseId}/webhooks" \
  -H "Authorization: Bearer $AIRTABLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "notificationUrl": "https://your-app.example.com/webhook",
    "specification": {
      "options": {
        "filters": {
          "dataTypes": ["tableData"],
          "changeTypes": ["add", "update"]
        }
      }
    }
  }'

The response includes macSecretBase64. Capture it immediately; it's shown only once, and you can't retrieve it later. Respond to every ping with a 200 or 204 and an empty body.

Securing Airtable webhooks

Each ping carries an X-Airtable-Content-MAC header: the string hmac-sha256= followed by a hex-encoded HMAC-SHA256 of the raw request body. The key is the base64-decoded macSecretBase64, not the string as returned. Recompute and compare in constant time.

const crypto = require("crypto");

// macSecretBase64 is base64; decode it to raw bytes for the HMAC key
const MAC_KEY = Buffer.from(process.env.AIRTABLE_MAC_SECRET_BASE64, "base64");

function verify(rawBody, macHeader) {
  const expected = "hmac-sha256=" +
    crypto.createHmac("sha256", MAC_KEY).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(macHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verify(req.body, req.headers["x-airtable-content-mac"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(204); // empty-body ack; do NOT block on fetching payloads
  enqueue(async () => {
    // fetch changes with the stored cursor, page while mightHaveMore, save cursor
  });
});

The same verification in Python:

import base64
import hashlib
import hmac
import os

MAC_KEY = base64.b64decode(os.environ["AIRTABLE_MAC_SECRET_BASE64"])


def verify(raw_body: bytes, mac_header: str) -> bool:
    expected = "hmac-sha256=" + hmac.new(MAC_KEY, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, mac_header or "")

Airtable webhook limitations and pain points

Webhooks expire after 7 days

The Problem: Webhooks created with a personal access token or OAuth token expire and are disabled after 7 days. If you don't extend them, notifications simply stop.

Why It Happens: Airtable bounds the lifetime of token-created webhooks.

Workarounds:

  • Refresh the webhook (or call list-payloads) regularly; both extend the lifecycle by 7 days.
  • Track each webhook's expiry and renew from a scheduled job well ahead of the deadline.

How Hookdeck Can Help: Hookdeck's observability shows a drop in delivery volume immediately, so an expired webhook surfaces as an alert rather than as silently missing changes.

Thin pings mean a required follow-up call

The Problem: The ping carries no change data. Every notification requires a call to the payloads API, with cursor management, pagination while mightHaveMore, and the per-base rate limit.

Why It Happens: Airtable keeps notifications tiny and makes you pull the actual changes on your schedule.

Workarounds:

  • Persist the cursor durably and page until mightHaveMore is false.
  • Debounce: a burst of pings can be served by a single catch-up read from the last cursor.
  • Respect the 5 req/s per-base limit and back off on 429.

How Hookdeck Can Help: Hookdeck durably queues each ping so your worker can fetch payloads at a controlled rate, and can rate-limit forwarding to stay within the per-base ceiling.

Payloads are retained for only about a week

The Problem: Airtable deletes webhook payloads server-side after roughly a week. Fall far enough behind and the changes you never fetched are gone.

Why It Happens: Airtable doesn't retain the change log indefinitely.

Workarounds:

  • Fetch and persist payloads promptly rather than letting them accumulate.
  • Reconcile against the base's current state if you suspect a gap.

How Hookdeck Can Help: Hookdeck reliably accepts and queues every ping, so a downstream backlog doesn't push you past Airtable's retention window before you've pulled the changes.

Duplicate pings and the once-only secret

The Problem: The same change can generate more than one ping, and the macSecretBase64 is shown only at creation. Lose the secret and you can't verify pings without recreating the webhook.

Why It Happens: At-least-once notification plus a create-time-only secret.

Workarounds:

  • Drive processing off the cursor, not the ping count, so duplicate pings collapse into one catch-up read.
  • Store macSecretBase64 securely the moment you create the webhook.

How Hookdeck Can Help: Hookdeck deduplicates pings at the edge, so repeated notifications don't trigger redundant payload fetches. See our guide to webhook idempotency.

Best practices

Verify with the base64-decoded secret

Compute hmac-sha256= + hex HMAC-SHA256 over the raw body, keyed with the decoded macSecretBase64, and compare in constant time.

Respond empty and fetch payloads asynchronously

Return 200/204 with an empty body immediately, then fetch changes off a queue using the stored cursor. Never block the ping response on the payloads call. See why to process webhooks asynchronously.

Drive off the cursor and page fully

Persist the cursor, page while mightHaveMore, and treat pings as a nudge to catch up from the last cursor rather than as the change itself.

Renew before the 7-day expiry

Track expiry and refresh (or list payloads) on a schedule so token-created webhooks never lapse.

Store the MAC secret at creation

Capture macSecretBase64 immediately and keep it in a secrets manager; it can't be retrieved later.

Make Airtable webhooks production-ready

Hookdeck verifies X-Airtable-Content-MAC, deduplicates pings, and durably queues so you never miss the payloads window

Conclusion

Airtable webhooks pair a thin notification ping with a cursor-based payloads API, so receiving a change is really two steps: verify the ping, then pull the payloads. Layer on a 7-day expiry for token-created webhooks, roughly a week of payload retention, a once-only MAC secret, and a per-base rate limit, and there's a fair amount to get right.

That leaves you owning verification with the decoded secret, cursor management, timely payload fetches, renewal, and deduplication. Hookdeck verifies X-Airtable-Content-MAC, deduplicates pings, and durably queues them so your worker fetches payloads reliably and on time, and a lapsed webhook or a backlog never quietly costs you data.

Get started with Hookdeck for free and handle Airtable webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.