All resources
Automations & integrations

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 connectorWebhook + your own worker
SetupSign in to Notion, or paste an Airtable tokenDeploy a small script, paste its URL
Field mappingAutomatic, by name and typeWhatever you write
PlanPaid (Starter and up)Free plan works
Retries on failureAutomatic, with backoff; failures visible in the inboxWhatever you write
Best forWorking in a minuteFree 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

html
<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 propertyWhat gets written
TitleThe matching field, or the form's name when no field matches, so the page is always valid
TextThe value, up to 2,000 characters
Email, Phone, URLThe value, in the native type
NumberThe value parsed as a number
Checkboxtrue for yes, true, on or 1; otherwise false
SelectThe 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.

Tip: name a Title property after your most useful field:

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:

json
{
  "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):

js
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

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