Blog

n8n8 min read

Send form submissions to n8n

Trigger an n8n workflow from every Ikiform submission, verify the webhook signature in a Code node, and route the data anywhere n8n connects.

Preet SutharFounder, Ikiform

n8n is a good first stop for Ikiform submissions. One workflow can verify the delivery, then send it to Slack, a spreadsheet, a CRM and an email list at once, with no server of your own. This guide works for n8n Cloud and for self-hosted n8n.

How a submission gets from your form to n8nyour formany html form,framework orserverIkiformstores it,signs a webhookn8nwebhook, then acode nodechecks itany appSlack, Sheets,a CRM, emailpostsignednodesHow a submission gets from your form to n8nyour formany html form, framework orserverIkiformstores it, signs a webhookn8nwebhook, then a code node checksitany appSlack, Sheets, a CRM, emailpostsignednodes
  1. A visitor submits your form, and it posts to Ikiform.
  2. Ikiform validates it, filters spam, stores it and posts a signed webhook to the n8n Webhook node.
  3. A Code node checks the signature before anything else runs.
  4. Later nodes send the fields to Slack, a sheet, a CRM or email.
Your form posts to Ikiform. Ikiform signs each submission and posts it to the n8n Webhook node.

What you'll build

  • A Webhook trigger that receives Ikiform's signed POST.
  • A Code node that checks the signature and stops anything that wasn't sent by Ikiform.
  • A branch that ignores test events and passes real submissions on to the rest of the workflow.
The n8n workflow, node by nodeWebhookPOST, raw bodyonCodechecks thesignatureRespond toWebhook200, no dataIftype issubmission.createdthrowsn8n answers 500Ikiformretrieson its schedulebad signatureThe n8n workflow, node by nodeWebhookPOST, raw body onCodechecks the signatureRespond toWebhook200, no dataIftype issubmission.createdthrowsn8n answers500Ikiformretrieson itsschedulebad signature
  1. The Webhook node receives Ikiform's POST with the raw body.
  2. The Code node checks the signature. A bad signature throws, n8n answers 500, and Ikiform retries later.
  3. Respond to Webhook answers 200 as soon as the check passes.
  4. The If node lets submission.created events through to your actions and stops test events.
Respond only after the check, so a failed check shows up in Ikiform's delivery log.

Before you start

  • An Ikiform form that already receives submissions. The quickstart takes a few minutes.
  • An n8n Cloud account, or a self-hosted n8n that's reachable on a public https:// URL on port 443.
  • On self-hosted n8n, the Code node needs the built-in crypto module. Set NODE_FUNCTION_ALLOW_BUILTIN=crypto on the n8n process, or on the task runner if you use external runners. n8n Cloud allows crypto already.

Ikiform only delivers to public https:// URLs on port 443. IP addresses, localhost, internal hostnames, workers.dev and pages.dev are refused when you save the endpoint.

What Ikiform sends

Every delivery is a POST with a JSON body. The body has the same shape for every endpoint, and you can't change it. A submission looks like this:

submission.created
{
  "id": "dlv_01k7v2d4n8r2p6s0w3y5b7c9e1",
  "type": "submission.created",
  "timestamp": "2026-10-23T12:00:00.000Z",
  "api_version": "2026-10-17",
  "data": {
    "submission_id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
    "form": { "id": "frm_xxxxxxxxxxxxxxxx", "name": "Contact" },
    "fields": { "name": "Ada", "email": "ada@example.com", "message": "Hello" },
    "meta": {
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
      "origin": "https://example.com",
      "referrer": "https://example.com/contact",
      "country": "GB",
      "subject": null,
      "reply_to": "ada@example.com"
    },
    "created_at": "2026-10-23T12:00:00.000Z",
    "truncated": false
  }
}

The form's values are in data.fields, keyed by the input name. A test from the dashboard sends an endpoint.test event instead, with data.endpoint_id and data.message and no fields. The full reference is on Webhooks.

Ikiform can't reshape this body, and n8n doesn't need it to. Every value is available to later nodes as an expression.

Set it up

  1. Add a Webhook trigger

    1. Create a workflow and add the Webhook node as its trigger.
    2. Set HTTP Method to POST and Path to something readable, like ikiform.
    3. Leave Authentication on None. Ikiform can't send n8n's auth headers; the signature check in the next step does that job.
    4. Set Respond to Using 'Respond to Webhook' Node.
    5. Under Options, add Raw Body and turn it on. The signature covers the exact bytes Ikiform sent.

    The node shows two URLs. The Test URL works only while the editor is listening for a test event. The Production URL works once the workflow is published. Ikiform needs the production one in the end.

  2. Verify the signature in a Code node

    Add a Code node after the Webhook. Set Mode to Run Once for All Items and Language to JavaScript, then paste this. Replace the placeholder with your endpoint's signing secret after you create it in step 5.

    Code node
    const crypto = require("crypto");
    
    const SECRET = "whsec_paste_your_signing_secret_here";
    
    const item = $input.first();
    const headers = item.json.headers;
    const raw = item.binary?.data
      ? (await this.helpers.getBinaryDataBuffer(0, "data")).toString("utf8")
      : JSON.stringify(item.json.body);
    
    const id = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];
    const signatures = headers["webhook-signature"] ?? "";
    const age = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (!id || !timestamp || !(age <= 300)) throw new Error("Missing or stale Ikiform webhook headers");
    
    const key = Buffer.from(SECRET.replace(/^whsec_/, ""), "base64");
    const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${raw}`).digest();
    const valid = signatures.split(" ").some((entry) => {
      const [version, value] = entry.split(",");
      const received = Buffer.from(value ?? "", "base64");
      return version === "v1" && received.length === expected.length && crypto.timingSafeEqual(received, expected);
    });
    if (!valid) throw new Error("Invalid Ikiform webhook signature");
    
    return [{ json: JSON.parse(raw) }];
    How the Code node checks an Ikiform signaturewebhook-idwebhook-timestampwithin 5 minraw bodyexact bytessecretbase64 after whsec_HMAC-SHA256of id.timestamp.bodywebhook-signaturev1,… one per secretany v1 match?continuereturns theparsed eventthrown8n answers500keyexpectedyesnofrom the requestyours, never sentHow the Code node checks an Ikiform signaturewebhook-idwebhook-timestampwithin 5 minraw bodyexact bytessecretbase64 after whsec_HMAC-SHA256ofid.timestamp.bodywebhook-signaturev1,… one per secretany v1match?continuereturns the parsedeventthrown8n answers 500
    1. Read webhook-id, webhook-timestamp and the raw body exactly as received.
    2. Reject the request if the timestamp is more than 5 minutes from now.
    3. Base64-decode the part of the secret after whsec_ and use it as the HMAC key.
    4. Compute HMAC-SHA256 of id.timestamp.body and base64-encode it.
    5. Compare it, in constant time, with each v1 value in webhook-signature. There are two while a secret rotates.
    6. If one matches: continue, returns the parsed event.
    7. If none match: throw, n8n answers 500.

    With Raw Body on, n8n keeps the original bytes in the item's data binary field. If your n8n version doesn't add it, the code falls back to JSON.stringify of the parsed body. That gives the same bytes, because Ikiform's body is compact JSON written by JSON.stringify.

    A bad signature throws, the workflow stops before it responds, and n8n answers 500. Ikiform then retries on its schedule, so a wrong secret costs you nothing: fix it and the next retry goes through.

    Anyone who can edit this workflow can read the secret. On plans with n8n Variables you can keep it there instead and read it with $vars.
  3. Answer Ikiform right away

    Add a Respond to Webhook node after the Code node and set Respond With to No Data. It answers 200 as soon as the signature checks out. Ikiform needs a 2xx within 15 seconds, so answer before the slow work, not after it.

  4. Skip test events and use the data

    Add an If node with the condition {{ $json.type }} is equal to submission.created. Connect your real actions to its true branch. These expressions pick out the useful parts:

    Expressions
    {{ $json.data.form.name }}
    {{ $json.data.fields.email }}
    {{ $json.data.fields.message }}
    {{ $json.data.submission_id }}

    Field names match the name attribute of each input on your form. A form with a company input gives you {{ $json.data.fields.company }}.

  5. Add the endpoint in Ikiform

    • In the Ikiform dashboard, open Webhooks and choose Add endpoint.
    • Paste the Production URL from the Webhook node into URL.
    • Under Forms, pick one form, or All forms in the workspace.
    • Add a Description if you like, such as "n8n", then choose Add endpoint.
    • Copy the signing secret. It starts with whsec_ and is shown in full only once.

    There's no event picker. An endpoint gets submission.created for every accepted submission on the forms you chose, and endpoint.test when you send a test. A workspace on Pro can have up to 3 endpoints.

  6. Paste the secret and publish

    Paste the whsec_ secret into the Code node, save, and publish the workflow. Older n8n versions call this turning the workflow Active. The production URL only answers while the workflow is published.

Map fields with real data first

To see real field names in the editor before you publish, point the Ikiform endpoint at the Test URL, choose Listen for test event in n8n, and send one submission from your form. Then Edit the endpoint in Ikiform and switch it to the Production URL. Editing keeps the same signing secret.

Test it

Open the endpoint in Ikiform and choose Send test. Ikiform sends one endpoint.test event with no retries, and the result shows up in the endpoint's delivery log within a few seconds. Then send a real submission from your form, or from a terminal:

Terminal
curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada","email":"ada@example.com","message":"Hello from curl"}'

A 201 with "ok": true means Ikiform stored it. The delivery appears on the endpoint page, and the message lands in n8n's Executions list.

If the form has a human check or allowed domains turned on, a post from curl is refused or marked as spam, and spam never sends a webhook. Test from your real site instead.

The test event runs the Webhook, Code and Respond nodes, then stops at the If node. A real submission continues down the true branch.

How delivery works

The receiver answersIkiform
Any 2xx within 15 secondsMarks the delivery succeeded
A redirectDoesn't follow it, and counts a failure
429 or 503 with Retry-AfterWaits that long, up to the next step of the schedule
410 GoneFails the delivery and disables the endpoint
Anything else, or no answer in 15 secondsRetries

Failed deliveries are retried 8 times over about 3 d 6 h, with the same webhook-id each time. If every delivery to an endpoint fails for 5 days, Ikiform disables it and emails the workspace owners.

Because the Respond to Webhook node runs only after the signature check, a failing check shows up in Ikiform's delivery log as a 500, and n8n shows the error in Executions.

Troubleshooting

ProblemWhat to do
404 in the delivery logThe workflow isn't published, or the endpoint still uses the Test URL. Publish the workflow and use the Production URL.
500 and Invalid Ikiform webhook signature in ExecutionsThe secret in the Code node doesn't belong to this endpoint, or it has a typo. Copy it again, or use Roll secret and paste the new one.
Cannot find module 'crypto'Self-hosted n8n blocks built-in modules by default. Set NODE_FUNCTION_ALLOW_BUILTIN=crypto and restart n8n.
Missing or stale Ikiform webhook headersThe request didn't come from Ikiform, or the n8n server clock is more than 5 minutes off. Ikiform signs every retry again, so a real retry is never stale.
400 timestamp_too_oldThe signature is more than 5 minutes old. Check the server clock, and don't replay old deliveries by hand; use Retry now on the delivery instead, which signs it again.
Delivery log shows redirect_not_followedThe URL answers with a 3xx. Use the final URL, with the exact path and trailing slash, so it answers 2xx directly.
Endpoint is disabledEvery delivery failed for 5 days, the URL answered 410, or its hostname started resolving to a private address. Fix the cause, then turn Enabled back on and Retry now the failed deliveries.
No delivery at allCheck that the endpoint covers this form, that it's enabled, and that the submission wasn't marked as spam or held because the workspace is over its monthly limit.

Avoid duplicate rows

Ikiform retries when it doesn't get a 2xx in time, with the same id each time. If a later node writes rows or sends emails, drop repeats with n8n's Remove Duplicates node keyed on {{ $json.id }}, set to remove items processed in previous executions.

Next steps

Want toRead
Post each submission to a channelSend form submissions to Slack
Add rows to a spreadsheetSend form submissions to Google Sheets
Understand every header and retryWebhooks
Check a signature by handSignature verifier

n8n's own reference for the nodes used here: Webhook, Code, Respond to Webhook and built-in module settings.

Stuck on a step, or want a guide for another tool? Email hello@ikiform.com.