Why use Airtable as a form backend
Airtable is a spreadsheet that thinks it's a database. For small ops teams it's perfect: structured columns, real types (single-select, linked records, attachments), filterable views, and a much friendlier UX than a raw SQL table. If you're running a content marketplace, a job board, a podcast guest pipeline, or anything where every inbound submission needs to be tracked through a few statuses, Airtable + a custom-domain form is the cheapest path that doesn't look amateur.
The catch is that Airtable's built-in form view is hideous and lives at airtable.com — fine for an internal team, awful as a public lead form. The standard fix: keep your form on your own domain, point the action at splitforms, and forward the webhook into Airtable via the API. This guide walks the full path including the proxy you'll deploy.
Step 1 — Create a Personal Access Token
Airtable retired API keys in 2024. The replacement is the Personal Access Token — same idea, scoped per base.
- Go to
https://airtable.com/create/tokenswhile signed in. - Click Create new token. Name it "splitforms webhook".
- Scopes: tick data.records:write. Add data.records:read if you want to dedupe by querying first. Add schema.bases:read if you want to fetch field metadata at runtime.
- Access: pick the base you'll be writing to. Do not grant access to all bases — there's no benefit and a leaked token would expose your whole workspace.
- Click Create token and copy the value (starts with
pat). You won't see it again.
Step 2 — Find your base ID and table name
The fastest way is the API docs. Go to https://airtable.com/developers/web/api/introduction, click your base in the sidebar — the docs page URL contains the base ID:
https://airtable.com/app1234567890ABCD/api/docs
└────── base ID ──────┘Base IDs always start with app followed by 14 alphanumeric characters. You can also find it in any base URL — the segment after airtable.com/.
The table name is just the human-readable name as displayed in the base — "Leads", "Submissions", etc. URL-encode any spaces. You can also use the table's ID (tbl...) instead, which is more stable if you rename tables.
# Base URL pattern
https://api.airtable.com/v0/{baseId}/{tableNameOrId}
# Concrete example
https://api.airtable.com/v0/appAbC123XyZ456D/Leads
https://api.airtable.com/v0/appAbC123XyZ456D/tbl9oZ2pQ4rT8wY1Step 3 — The Airtable fields object
Airtable's API takes a fields object where keys are column names (exact case, exact spelling, including spaces) and values are typed primitives. The mappings you actually need:
{
"fields": {
"Name": "Ada Lovelace", // single-line text
"Email": "ada@example.com", // email
"Message": "Hi, testing.", // long text
"Status": "New", // single-select (must be an existing option)
"Tags": ["demo", "enterprise"], // multi-select
"Submitted": "2026-05-02T12:00:00.000Z",// date / datetime (ISO 8601)
"Subscribed": true, // checkbox
"Score": 42, // number
"Website": "https://example.com", // url
"Phone": "+1-555-0100", // phone
"Resume": [{ "url": "https://splitforms-cdn.com/uploads/abc.pdf" }]
// attachment — array, must use { url: ... }
}
}Two gotchas burn everyone the first time:
- Single-select options must exist. If your "Status" column has options [New, Qualified, Closed] and you POST
"Status": "new"(lowercase), you get a 422. To auto-create new options, settypecast: trueat the top level of the request. - Linked-record fields take record IDs, not text. If "Source" links to a Campaigns table, you need
"Source": ["recAbc123..."], not"Source": "Black Friday". Either look up the record ID first or use typecast: true to create-or-link by name.
Step 4 — The splitforms → Airtable proxy
splitforms can POST to any HTTPS endpoint, but the Airtable API needs a Bearer token in the Authorization header — and you don't want that token visible in your splitforms config. Drop a stateless proxy in front. Cloudflare Workers, Vercel Edge, Deno Deploy, or any Node/Bun server work; here's the Worker version (~40 lines):
export default {
async fetch(req, env) {
if (req.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
const submission = await req.json();
const f = submission.fields ?? submission.data ?? {};
const fields = {
Name: f.name,
Email: f.email,
Message: f.message,
Status: "New",
Submitted: submission.submitted_at || new Date().toISOString(),
Source: f.source,
};
// Strip undefined / empty so Airtable doesn't reject the column.
Object.keys(fields).forEach(
(k) => (fields[k] === undefined || fields[k] === "") && delete fields[k]
);
const url = `https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent(env.AIRTABLE_TABLE)}`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${env.AIRTABLE_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ fields, typecast: true }),
});
if (!res.ok) {
const err = await res.text();
console.error("Airtable error", res.status, err);
return new Response(err, { status: 502 });
}
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
},
};Set the secrets:
wrangler secret put AIRTABLE_TOKEN # paste pat...
wrangler secret put AIRTABLE_BASE_ID # paste app...
wrangler secret put AIRTABLE_TABLE # e.g. "Leads"wrangler deploy, copy the resulting URL, and paste it into your splitforms dashboard under the form's Webhooks → Add → Generic JSON. typecast: truetells Airtable to coerce strings into select options or linked records by name — so "New" gets created if it doesn't exist, and a campaign name gets linked-or-created instead of throwing.
Test from your terminal:
curl -X POST https://splitforms-airtable.your.workers.dev \
-H "Content-Type: application/json" \
-d '{
"submitted_at": "2026-05-02T12:00:00Z",
"fields": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"message": "Hi — testing.",
"source": "Black Friday"
}
}'You should see {"ok":true}in 300-500ms and a new row in your Leads table. Most failures are field-name typos — Airtable's case-sensitivity is unforgiving. Use the API docs page for your base to copy field names exactly.
Single-record vs batch creation
The proxy above creates one record per submission, which is correct for a contact form. If you're importing a backlog or you have a high-traffic form (think: lead-gen funnel pushing 100 submissions/minute), Airtable's batch endpoint takes up to 10 records per request and counts as one against the rate limit.
POST /v0/{baseId}/{tableName}
Authorization: Bearer pat...
Content-Type: application/json
{
"records": [
{ "fields": { "Name": "Ada", "Email": "ada@example.com" } },
{ "fields": { "Name": "Bob", "Email": "bob@example.com" } }
],
"typecast": true
}The 5 req/sec rate limit is per base, not per token. Hitting it returns a 429 with a Retry-Afterheader — splitforms' webhook delivery layer handles that automatically with exponential backoff, but if you're calling Airtable directly from your code, queue and respect the header.
The no-code alternative: Make / Zapier
If you don't want to deploy a proxy, the same flow works through Make.com or Zapier:
- Trigger: "Webhooks" in Make, or "Webhooks by Zapier" — both give you a custom inbound URL you paste into splitforms.
- Action: "Airtable → Create a Record". Map the splitforms fields onto the Airtable columns through the visual mapper.
- Cost: Make's free tier (1,000 ops/mo) handles small forms. Zapier requires the $20/mo Starter plan for multi-step or filter logic.
The trade-off vs the proxy: Make and Zapier add 2-15 seconds of latency, can fail silently when their queue backs up, and become a recurring bill. The 40-line Worker is free up to 100k requests/day and runs in ~30ms. For one-off forms either path is fine; for anything mission-critical, write the proxy. The full webhook signature spec, retry behavior, and HMAC verification are in the splitforms docs.
And if you don't already have a contact form, grab a copy-paste starter from splitforms templates — works with React, Vue, plain HTML, and any framework that can render a <form>. Pair this Airtable wiring with the rest of your contact form stackand you've replaced a $24/user/mo Airtable Pro subscription with a free splitforms form, a free Worker, and a free Airtable base.
Tech support and troubleshooting
Five errors account for almost every Airtable webhook failure we see in the wild. Quick fixes:
- 422 INVALID_VALUE_FOR_COLUMN — Field name casing or the value type doesn't match. Copy the exact column header from the API docs page and re-check single-select option strings.
- 401 AUTHENTICATION_REQUIRED — Personal Access Token expired or wasn't granted access to that base. Generate a new PAT and re-scope it to the target base only.
- 429 rate-limit hit — Airtable caps each base at 5 req/sec. Switch the proxy to batch mode (10 records per call) or let splitforms' built-in retry queue back off.
- Attachments uploaded as broken URLs — Airtable downloads attachment URLs server-side. The CDN link must be public and reachable; signed URLs that expire in seconds will not import.
- Linked-record cell stays blank — Linked fields require an array of recXXX IDs, not text. Either pre-fetch the record ID or pass typecast: true so Airtable can match-or-create by name.
Still stuck? The full webhook payload schema, HMAC signature header, and retry timing are in the splitforms docs, and the raw endpoint reference is in the API reference. For account or billing questions, see splitforms FAQ.
FAQ
What's wrong with Airtable's own forms?
Nothing, if you don't care about your own brand. Airtable forms force visitors onto an airtable.com URL, ship with an Airtable logo unless you pay for the Pro plan ($24/user/mo), can't be styled beyond a banner image and a color, and don't support custom domains. For a real product page, an inline form on your own domain that POSTs to splitforms and forwards to Airtable looks 10x more credible and converts noticeably better.
Do I need a paid Airtable plan to use the API?
No. The Airtable API is on every plan including Free. Free workspaces are capped at 1,000 records per base, which is the real ceiling — at scale you'll need Team ($20/user/mo, 50,000 records) or higher. For most contact forms 1,000 records is enough for a year, especially if you archive old leads.
Personal access token vs OAuth — which one?
Use a Personal Access Token for any setup where you control the destination base. PATs are simpler (one token, no refresh dance), they support fine-grained scopes (data.records:write only), and you can scope them to a single base. OAuth is only the right call if you're shipping a multi-tenant product where end users connect their own Airtable.
Why does my request get a 422 INVALID_VALUE_FOR_COLUMN error?
Airtable field names in the API request are case-sensitive and must match the column name exactly — including spaces. 'email' will fail if the column is 'Email'. Single-select fields require the option name verbatim ('New', not 'new'). Linked-record fields take an array of record IDs (recXXX...), not strings. The API error message tells you which column choked, but the underlying cause is almost always one of these three.
Can I send file uploads to an Airtable attachment field?
Yes, but the API doesn't accept binary uploads — you have to give Airtable a public URL it can fetch. splitforms hosts uploaded files at a CDN URL that's included in the webhook payload; pass that URL into the attachment field as { url: "https://..." }. Airtable will download and re-host the file within a few seconds.
How fast is the Airtable API?
Single-record creates land in 200-500ms typically. The hard limit is 5 requests per second per base. If you have a high-traffic form, batch up to 10 records per request — that's still one API call, well within the rate limit, and gives you ~5,000 submissions/sec of headroom on paper.
Can I dedupe submissions by email automatically?
The Airtable API doesn't support upsert as a single operation on every plan, but you can simulate it: GET /records?filterByFormula=({Email}='ada@example.com'), then PATCH the existing record if found, or POST a new one if not. The proxy code below shows the pattern. Heads up — filterByFormula has a 16,000-character limit, which matters at scale but never for a single lookup.
Should I use Make or Zapier instead?
If you don't want to write code, yes. Make.com's Airtable module is the cheapest at $9/mo for 10,000 ops; Zapier's Airtable integration is more polished but costs $20+/mo. The trade-off is one more vendor in the chain and a few seconds of latency vs the proxy below, which runs in 30ms on Cloudflare's free tier.
Next steps
You now have a typed Airtable pipeline behind your form. From here, the highest-leverage follow-ups:
- Layer Slack notifications on top — see Send form submissions to Slack.
- Mirror the same payload to Notion or a spreadsheet — Notion or Google Sheets.
- Lock the proxy down with HMAC signature verification from the splitforms webhook guide.
- Compare with what alternatives offer at splitforms vs Formspree.