# Guide to Airtable Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | `POST /v0/bases/{baseId}/webhooks` (Web API) |
| Notification | Thin ping: `base.id`, `webhook.id`, `timestamp` only |
| Fetching changes | `GET /v0/bases/{baseId}/webhooks/{webhookId}/payloads` with a cursor |
| Verification header | `X-Airtable-Content-MAC` |
| Signature scheme | `hmac-sha256=` + hex HMAC-SHA256 over the raw body, keyed with the base64-decoded `macSecretBase64` |
| Secret | `macSecretBase64` returned once at creation; not retrievable later |
| Ping response | 200 or 204 with an empty body, within ~25 seconds |
| Expiry | PAT/OAuth webhooks expire after 7 days (refresh or list-payloads extends) |
| Payload retention | Deleted server-side after ~1 week |
| Rate limit | 5 requests/sec per base (shared); 429 -> wait 30s |
| SDK | `pyairtable` (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`:

```bash
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.

```javascript
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:

```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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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.

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Airtable webhooks reliably in minutes.