Forms webhooks

Send form responses to n8n

An n8n Webhook node has two addresses, one for building and one for running. Both answer 404 until n8n is ready for them, and a 404 switches a webhook off, so the order of these steps matters.

Set it up

  1. Add the Webhook node, and read this before you paste anything

    In n8n, create a workflow and add a Webhook node as the trigger. Set the method to POST.

    The node shows two addresses: a test URL and a production URL. The test URL only answers while the node is listening. The production URL only answers once the workflow is active.

    When neither is true, n8n answers 404. We treat a 404 as a deleted endpoint and switch the webhook off, because an address that reports itself gone is a decision rather than an outage. So make n8n ready first, and paste second. If it does get switched off, step 5 turns it back on.

    Keep both addresses private: anyone who has one can post into your workflow.

  2. Put n8n into listening mode, then add the webhook with the test URL

    In n8n, press Listen for test event on the Webhook node. Leave it listening.

    Now open the form, go to Webhooks and choose Add a webhook.

    Give it a name, such as n8n. The name cannot be changed later.

    Paste the test URL. Under What we send, choose One level, for Zapier, Make and n8n.

    Leave Completed response ticked. Tick the partial event as well only if you want a delivery when somebody starts and stops.

    Save, then copy the signing secret. It is shown once and we cannot show it again.

  3. Send a test and build the rest of the workflow

    While n8n is still listening, press Send test in the form. We post one sample built from your own questions, and n8n shows it as the node output.

    If n8n stopped listening first, the sample gets a 404 and the webhook is switched off. That is recoverable: step 5.

    Build the rest of the workflow against those field names: your_name, phone, and so on.

  4. Activate the workflow, THEN move the form to the production URL

    Activate the workflow in n8n first. Until it is active, the production URL answers 404.

    Then edit the webhook in the form and replace the test URL with the production URL. Editing the URL does not change the signing secret.

    Press Send test once more to prove the live path end to end.

  5. If the webhook was switched off, turn it back on

    A webhook we switched off shows Disabled in the list, and we stop sending to it.

    Fix the n8n side first, so the address answers. Then press Edit on the webhook, tick Send deliveries to this URL, and save.

    Send another test to confirm before you rely on it. Nothing was lost: the switch is the only thing that changed.

If you want to verify the signature inside n8n, add a Code node that recomputes the HMAC over the raw body. Keep the signing secret in an n8n credential or environment variable rather than in the workflow itself.

What one delivery looks like

This is the flat shape. Every question is one key with one value, so a field picker can list them.

{
  "event_id": "0f0a2e5c-3b1e-4a55-9f0e-6f1f4d2b8a11",
  "event_type": "faeform.response",
  "response_id": "9c1b2f77-2a4e-4f2f-9a3a-1d6b0d3f88c2",
  "status": "completed",
  "test": false,
  "form_ref": "quote-request",
  "form_title": "Get a quote",
  "form_version": 4,
  "submitted_at": "2026-08-27T15:04:11.000Z",
  "ending_ref": "thanks",
  "ending_title": "Thanks, we will call you back",
  "revision_id": "6b2c0f19-9d3a-4c58-9a10-2f7f0b1c4d55",
  "revision_fingerprint": "9f86d081884c7d659a2feaa0c55ad015d7f4b1a1b0d0a5f0c3e7a9b2c4d6e8f0",
  "definition_json": "{\"title\":\"Get a quote\", ... }",
  "definition_json_omitted": false,
  "your_name": "Ada Lovelace",
  "phone": "+13035550142",
  "what_do_you_need": "Roof, Gutters",
  "how_soon": "2026-09-02",
  "budget": 1800,
  "hidden_source": "google",
  "variable_rating": 40
}

The definition_json value is shortened here. A real delivery carries the whole published form as JSON text, unless that text is larger than about 20 KB: past that we send definition_json empty and set definition_json_omitted to true, so you can tell a dropped definition from a form with nothing in it. The answers are unaffected either way, because each one is already a field of its own.

JSON objects have no order, so your endpoint may receive these keys in a different order from the one shown here. Match on the names.

The other shape, called Everything in full, keeps the answers nested with their types beside them. Pick it when your own code reads the delivery. The answers, the definition snapshot and the calculated block are all shortened in this example.

{
  "event_id": "0f0a2e5c-3b1e-4a55-9f0e-6f1f4d2b8a11",
  "event_type": "faeform.response",
  "form_response": {
    "form_ref": "quote-request",
    "response_id": "9c1b2f77-2a4e-4f2f-9a3a-1d6b0d3f88c2",
    "status": "completed",
    "version": 4,
    "submitted_at": "2026-08-27T15:04:11.000Z",
    "answers": {
      "your_name": { "type": "text", "text": "Ada Lovelace" },
      "budget": { "type": "number", "number": 1800 }
    },
    "hidden": { "source": "google" },
    "variables": { "rating": 40 },
    "calculated": null,
    "ending": "thanks",
    "definition_snapshot": { "title": "Get a quote", "fields": [] },
    "revision_id": "6b2c0f19-9d3a-4c58-9a10-2f7f0b1c4d55",
    "revision_fingerprint": "9f86d081884c7d659a2feaa0c55ad015d7f4b1a1b0d0a5f0c3e7a9b2c4d6e8f0"
  }
}

How the field names are chosen

A key comes from the question's reference name, which is made from the question wording the first time you write it and then stays put. So you can rewrite a question and the field your scenario is mapped to does not move.

When two reference names would reduce to the same key, the one that is already in that form keeps it and the other carries a short code taken from its own reference name, for example my_field and my_field_9c3f1a. The code is worked out from that one question and nothing else, so it does not change when you reorder your questions, and deleting a question never hands its field name to another one.

The same short code is added to a question whose reference name is one the payload uses for itself, such as score, status or submitted_at, and to any reference name starting with hidden_ or variable_. Those names belong to the payload and to your URL parameters, so a question can never take one.

URL parameters arrive as hidden_ plus the parameter name. Variables arrive as variable_ plus the variable name. Every question on the form is present on every delivery, and a question nobody answered is an empty string rather than a missing key, so a mapping cannot break on the first response that skipped it.

Check the delivery really came from us

Every request carries an X-FAEForm-Signature header that looks like t=1753500000,v1=9f86d0. The v1 value is an HMAC-SHA256 of the string timestamp.body, keyed with the signing secret we showed you once when the webhook was made. Recompute it over the raw body you received and compare.

Check the timestamp too. It is inside the signature, so an old delivery cannot be replayed at you with a fresh one. This is the same format Stripe uses, so an off-the-shelf verifier works.

The signature is the same in both shapes. Choosing the flat shape changes what the body says, never how it is signed.

Retries, tests and switching off

Answer with any 2xx and we call it delivered. Anything else is retried five times over about fourteen hours, then dropped. If your endpoint answers 404 or 410 we switch the webhook off, because a deleted endpoint is a decision rather than an outage.

The Send test button posts one sample, marked test: true, built from your own questions. It does not create a history row, is not retried and is not counted. There is a small limit on how fast tests can be sent, six a minute per account, so a run of them in a few seconds will be refused for a moment. A real response never carries that mark.

A test is built from the PUBLISHED version of your form. A question you have added but not published yet is not in it, and will not be in a real delivery either until you publish.

Recent deliveries in the webhooks panel shows the last ten attempts with what your endpoint answered. Test results appear beside the Send test button instead.

Other ways to work with a form

To read forms and responses from your own server instead of receiving them, see the Forms developer API. To put the form itself on your website, see the embed contract.