n8n
Webhooks & payloads
The exact requests form2wf sends to n8n, what n8n should send back, and how failures behave.
Four kinds of webhook#
| Webhook | Configured on | Called when | Body |
|---|---|---|---|
| Submit | Form settings | A visitor submits | Wrapped: data + metadata |
| Onload | Form settings | The form loads | params + endUser + metadata |
| Button | A button field | The button is clicked | Raw: onload data + inputs |
| Row action | A table's rowActions | A row button is clicked | Raw: the row |
Setting up the n8n Webhook node#
- Use the Production URL (
/webhook/…) and activate the workflow. Test URLs (/webhook-test/…) only work while the editor is listening. - HTTP method must match: submit and onload default to
POST. - n8n puts the request body under
$json.body— submitted values are at{{ $json.body.data.email }}. - For onload and buttons, set Respond to Using 'Respond to Webhook' node so you control the JSON reply.
Submit payload (JSON)#
POST to the submit webhook with Content-Type: application/json plus your configured headers:
{
"endpointId": "3f1c2b7a-…", // form UUID
"data": { // the visitor's values, nested as in the form
"name": "Ada",
"email": "ada@example.com",
"address": { "city": "Oslo" },
"attendees": [ { "name": "Ada" } ]
},
"metadata": {
"timestamp": "2026-09-29T14:03:11.000Z",
"userAgent": "Mozilla/5.0 …",
"ip": "203.0.113.7",
"origin": "https://your-site.com" // the embedding site
},
"userId": null, "user": null, // set when a signed-in session exists
"endUserId": null, "endUser": null // set on Verified and Protected forms
}Submit payload (multipart, with a file)#
When a file field has a file, the request is multipart/form-data and the shape changes — there is no data wrapper:
| Part | Content |
|---|---|
| each top-level field | Its text value, under its name |
| the file field | The file, under its name (n8n: binary property) |
_metadata | JSON string: endpointId, userId, user, endUserId, endUser, timestamp, userAgent, ip, origin |
In n8n, text values are at {{ $json.body.email }} and the file is in the item's binary data. Multipart requests are not retried, and nested (object/array) values are not included.
What n8n should return#
| n8n replies | Visitor sees | API response |
|---|---|---|
| Any 2xx | Your success message | 200 { success: true, data: <n8n body> } |
| 4xx | An error message | 502 Webhook failed (not retried) |
| 5xx or network error | An error message | 502 after up to 2 retries |
| No reply within 30s | An error message | 502 after retries |
The widget reads four optional keys from the reply — redirectUrl, navigate, refresh and message — to send the visitor to a payment page, open another form, redraw the form, or replace the success message (see Replies). The API passes them back as next, and the SDK and direct API calls also receive the whole reply as data. Other status codes you may see from form2wf: 404 unknown form, 403 inactive form or access denied, 429 monthly quota or rate limit reached.
Retries can deliver a submission twice. If your workflow is slow but eventually succeeds, form2wf may already have retried. Make workflows idempotent — see Best practices.
Securing your webhook#
Anyone who learns a webhook URL can call it. Add a secret header in form2wf and require it in n8n:
- In Form Settings → Submit Webhook, add a header such as
X-Form2wf-Secret: <long random value>. - In the n8n Webhook node, set Authentication to Header Auth with the same name and value.
Headers are stored server-side and never sent to browsers; the MCP server returns them as [redacted]. Button and row-action webhooks take their own headers (buttonWebhookHeaders, webhookHeaders).
Webhooks on a local network#
By default form2wf only calls public internet addresses. Webhook URLs that point at localhost, 192.168.x.x, 10.x.x.x and other private addresses are refused, and the visitor sees "webhook is on a private network". This stops a form from being used to reach machines on the server's own network.
If your n8n runs next to form2wf (for example http://192.168.1.10:5678/webhook/…), the organization owner can turn on Allow LAN webhooks under Settings → Organization Security. The same setting applies to the n8n server connection.
Onload payload#
{
"formId": "3f1c…", "formSlug": "my-orders",
"params": { "status": "open" },
"endUserId": "9a0b…",
"endUser": { "id": "9a0b…", "email": "client@acme.com", "name": "Acme Ops" },
"metadata": { "timestamp": "…", "userAgent": "…", "ip": "…", "origin": "…" }
}With method GET, params become query-string values and metadata is sent as a _metadata JSON string (no endUser). Reply with { "data": { … } }; a failure returns 502 and the form renders without data.
Button and row-action payloads#
These send the raw values, with no wrapper or metadata. The body comes from the visitor's browser, so treat it as untrusted: never let it decide who may change a record.
{ "id": 42, "name": "Acme", "email": "ops@acme.com" }{ "user": { "id": 7 }, "note": "Approved by phone" }Put identifying values in the URL (?id={{row.id}}) or in hidden inputs. A button displays the reply's data field as a notification; row actions redraw the form after success. Both follow reply keys such as redirectUrl and message.
On forms that require sign-in (Verified or Protected), the signed-in person is sent in headers that only form2wf sets: X-F2WF-User-Id and X-F2WF-User-Email, plus X-F2WF-User-Provider (e.g. microsoft) on Verified forms. Use these in n8n to check that the person may act on the row.
Execution log#
Every webhook call — submissions, onload, fetch and row actions — is logged with its status, duration, and n8n's response or error. Query it with the list_submissions MCP tool, filtering by form, type, status and time; for example { "status": "failed", "since": "2026-09-28T00:00:00Z" } finds every failed delivery across your organization.