How to Send Form Submissions to Notion or Airtable (Without Zapier)
Get every form submission into a Notion database or an Airtable table with no Zapier: a managed connector that maps fields to properties by name, or a free webhook route with a 20-line worker. Setup, field mapping, and the gotchas.
The ShipMyForm team
· 5 min read
To get a form on a static site writing into Notion or Airtable, point the form at a form backend and add the Notion or Airtable connector: submissions are matched to your database properties or table columns by name and written through the official APIs, with no Zapier in between. If you would rather own the code, the same backend's free webhook can call a 20-line worker that does the write. This guide covers both routes, exactly how fields map, and the two gotchas that account for most "it didn't show up" questions.
Full disclosure: ShipMyForm is our product, and the managed connectors below are ours. The free route works with any backend that can POST JSON to a URL.
The two routes at a glance
| Managed connector | Webhook + your own worker | |
|---|---|---|
| Setup | Sign in to Notion, or paste an Airtable token | Deploy a small script, paste its URL |
| Field mapping | Automatic, by name and type | Whatever you write |
| Plan | Paid (Starter and up) | Free plan works |
| Retries on failure | Automatic, with backoff; failures visible in the inbox | Whatever you write |
| Best for | Working in a minute | Free tier, or custom mapping logic |
Either way ShipMyForm sits in front of the form: it stores the submission, runs spam filtering, and only then delivers. Bots never reach your database, and a delivery that fails is retried and shown as failed in your inbox rather than silently dropped.
Route A: the Notion connector
Step 1: make the database
Create a database in Notion (a full-page table is simplest). It needs a
Title property, and then one property per form field, named exactly
like the field's name attribute (case does not matter). For the contact
form below that is Name, Email and Message. Pick the property types you
actually want: Email as an email property, Message as text, and, if the
form sends a plan field, a select property called Plan whose options are
the values your form sends.
Step 2: point the form at ShipMyForm
<form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
<input name="name" required autocomplete="name">
<input name="email" type="email" required autocomplete="email">
<select name="plan">
<option>Starter</option>
<option>Pro</option>
</select>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>Step 3: connect Notion and pick the database
In the dashboard, open the form, go to Connectors, choose Notion and click Connect Notion. Notion's consent screen asks which pages the integration may access: tick the database from step 1 (and only that). Back in ShipMyForm, pick it from the dropdown. Send the connector's test delivery; a page called after your form appears in the database.
How fields become properties
On each submission the connector reads the database schema, then, for every property, looks for a submitted field with the same name and writes it according to the property's type:
| Notion property | What gets written |
|---|---|
| Title | The matching field, or the form's name when no field matches, so the page is always valid |
| Text | The value, up to 2,000 characters |
| Email, Phone, URL | The value, in the native type |
| Number | The value parsed as a number |
| Checkbox | true for yes, true, on or 1; otherwise false |
| Select | The value as the option name (an option is created if it does not exist) |
| Anything else (date, multi-select, relation…) | The value as text, when the property accepts it |
Fields with no matching property are skipped, and properties with no matching field are left empty. Rename either side to line them up; nothing needs re-connecting.
If the Title property is called Name and the form sends
name, every page in the database is titled by the person who
wrote in, which is what you want in a board or list view. Leave it as
Name with no matching field and every page is titled after the
form instead.
Route B: the Airtable connector
Step 1: make the table
In a base, create a table with one column per form field, named exactly
like the field. Airtable is stricter than Notion here: a record that
contains a field the table has no column for is rejected outright
(UNKNOWN_FIELD_NAME), so add a column for every name in your form,
including optional ones. Reserved fields such as _gotcha never reach the
connector, so they need no column.
Column types are up to you. The connector sends values with Airtable's
typecast option on, so a text value lands correctly in a number, checkbox,
date or single-select column when it can be parsed as one.
Step 2: create a token and copy the base ID
At airtable.com/create/tokens create a personal access token with the
data.records:write scope, and grant it access to just this base. The base
ID is the app… segment in the base's URL (airtable.com/appXXXXXXXX/…).
Step 3: add the connector
In the form's Connectors tab choose Airtable, paste the token, the base ID and the table's name, and save. Run the test delivery; a record appears with the test values.
Route C: free, with the webhook connector and a small worker
The webhook connector is on the free plan. It POSTs a JSON payload for each clean submission to a URL you choose, signed so you can verify it came from ShipMyForm. Any place that can run a few lines of JavaScript works; a Cloudflare Worker is used below because its free tier is ample for a form.
The payload looks like this:
{
"id": "sub_…",
"formId": "frm_…",
"formName": "Contact form",
"submittedAt": "2026-09-30T08:12:41.000Z",
"status": "ok",
"spamScore": 0.04,
"data": { "name": "Ada Lovelace", "email": "[email protected]", "message": "…" }
}A worker that appends the submission to Airtable, then to Notion, in that order (keep whichever half you need):
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
// Verify the signature before trusting anything in the body.
const raw = await request.text();
const sig = request.headers.get("X-ShipMyForm-Signature") || "";
if (!(await verify(raw, sig, env.SIGNING_SECRET))) return new Response("Bad signature", { status: 401 });
const { data, submittedAt } = JSON.parse(raw);
// Airtable: one record, columns named like the fields.
await fetch(`https://api.airtable.com/v0/${env.AIRTABLE_BASE}/${encodeURIComponent(env.AIRTABLE_TABLE)}`, {
method: "POST",
headers: { Authorization: `Bearer ${env.AIRTABLE_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ typecast: true, records: [{ fields: { ...data, "Submitted at": submittedAt } }] }),
});
// Notion: one page, properties written explicitly.
await fetch("https://api.notion.com/v1/pages", {
method: "POST",
headers: {
Authorization: `Bearer ${env.NOTION_TOKEN}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: JSON.stringify({
parent: { database_id: env.NOTION_DATABASE },
properties: {
Name: { title: [{ text: { content: data.name || "Website form" } }] },
Email: { email: data.email || null },
Message: { rich_text: [{ text: { content: (data.message || "").slice(0, 2000) } }] },
},
}),
});
return new Response("ok");
},
};
async function verify(raw, header, secret) {
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(raw));
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
return header === `sha256=${hex}`;
}Deploy it, put the worker URL into the Webhook connector, and paste the
connector's signing secret into the worker's SIGNING_SECRET variable. For
Notion on this route you also create an internal integration at
notion.so/my-integrations, copy its token, and share the database with it
from the page's ⋯ → Connections menu; the database ID is the 32-character
run in the database URL before ?v=.
The trade-off is the same as with any DIY route: it is free and entirely yours, and it is yours to keep working when a token expires or an API version changes. The managed connectors exist so that is not your problem.
Which route should you use?
- Paid plan, want it working now: the managed connector for whichever tool your team lives in. Add both if you use both; connectors are per form and independent.
- Free plan, or you need mapping logic the connector does not do (write to two databases, compute a property, set a relation): the webhook and a worker.
In every case the submission is also stored in your ShipMyForm inbox, so a failed write is a retry, not a lost lead. The inbox's Delivery failed filter shows exactly which ones need a look.
Next steps
- Same idea, different destination: send form submissions to Google Sheets
- Notify people as well as storing: send form submissions to Slack
- Multi-step automations on the free plan: connect a form to n8n
- Connector reference: Notion · Airtable · Webhook
Frequently asked questions
- Do I need Zapier or Make for this?
- No. ShipMyForm writes to Notion and Airtable directly through their APIs, so there is no third automation tool, no task quota, and no per-submission delay. Zapier still makes sense if you already run a multi-step zap that should start from the submission.
- Which plan are the Notion and Airtable connectors on?
- Both managed connectors are on the paid plans (Starter and up). The webhook connector is on the free plan, and the free route in this guide uses it with a small worker you host yourself.
- What happens to a form field that has no matching Notion property or Airtable column?
- Notion: the field is skipped and the page is still created. Airtable: the whole record is rejected with an UNKNOWN_FIELD_NAME error, so every submitted field needs a column with the same name. Reserved fields such as _gotcha never reach either.
- Can a form field fill a Notion select, checkbox or number?
- Yes. Properties are matched by name, then written according to the property's type: select takes the submitted text as the option name, checkbox is true for yes, true, on or 1, number is parsed, email, phone and URL use their native types, and anything else becomes rich text.
- Does the honeypot or the spam end up in my database?
- No. Reserved fields (_gotcha, _subject, _redirect) are stripped before delivery, and connectors run only for submissions that pass spam filtering. Spam is kept in your ShipMyForm inbox, not forwarded.
- Can I send the same submission to Notion and Airtable, and email too?
- Yes. Connectors are added per form and you can attach as many as you like; each delivery is attempted and retried independently, and every submission is stored in your inbox regardless.
Related guides
How to Send Form Submissions to Google Sheets
Turn a static-site form into a live Google Sheet. Connect Google once and every submission appends as a row — or do it free with a webhook and Apps Script.
How to Send Form Submissions to Slack
Get each form submission into Slack as a clean, formatted message. Use the managed Slack connector, or wire it up free with the webhook connector and an automation.
How to Connect Your Form to n8n
Send every form submission straight into an n8n workflow with the free webhook connector. No serverless glue, no polling.