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:
- A change happens in the base; Airtable POSTs a ping (
base.id,webhook.id,timestamp). - You verify the
X-Airtable-Content-MAC. - You call the list-payloads endpoint with your stored
cursor. - Airtable returns the payloads plus a new
cursorandmightHaveMore(page again while true, up to the per-page limit of 50). - 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
cursordurably and page untilmightHaveMoreis 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
macSecretBase64securely 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.