Forms webhooks

Send form responses to Make

Make learns the shape of a webhook from a real request. Send it one sample in the flat shape and every question on your form becomes a field you can map.

Set it up

  1. Make the scenario and get the address

    In Make, create a scenario and add the Webhooks module as the first step. Choose Custom webhook.

    Add a new webhook, give it a name, and copy the address Make shows you.

    Keep that address private: anyone who has it can post into your scenario.

  2. Add the webhook to your form

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

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

    Paste the Make address. 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 so Make can read the structure

    In Make, the webhook module waits for data while it is determining the data structure.

    In the form, press Send test on the webhook you just added. We post one sample built from your own questions.

    Make reads that sample and remembers the fields. The result of the test is shown in the form as well, so you can see what Make answered.

  4. Map the fields and switch the scenario on

    Add the rest of your scenario and map the fields by name: your_name, phone, and so on.

    Switch the scenario on. Make queues incoming requests, so a scenario that is running behind still receives everything.

If you add a question later, publish the form first. Make then needs to read a request that carries the new field: press Send test again and re-determine the data structure.

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.