# 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`](#submit) |
| Onload | Form settings | The form loads | [`params` + `endUser` + `metadata`](#onload) |
| Button | A `button` field | The button is clicked | [Raw: onload data + inputs](#actions) |
| Row action | A table's `rowActions` | A row button is clicked | [Raw: the row](#actions) |

## 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](#headers):

body:

```
{
  "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](https://form2wf.com/docs/data-pages#replies)). The API passes them back as `next`, and the [SDK](https://form2wf.com/docs/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](https://form2wf.com/docs/best-practices#idempotency).

## Securing your webhook

Anyone who learns a webhook URL can call it. Add a secret header in form2wf and require it in n8n:

1.  In Form Settings → Submit Webhook, add a header such as `X-Form2wf-Secret: <long random value>`.
2.  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

POST body:

```
{
  "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.

row action body — the row:

```
{ "id": 42, "name": "Acme", "email": "ops@acme.com" }
```

button body — onload data merged with current inputs:

```
{ "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](https://form2wf.com/docs/data-pages#replies) 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](https://form2wf.com/docs/mcp-tools), filtering by form, type, status and time; for example `{ "status": "failed", "since": "2026-09-28T00:00:00Z" }` finds every failed delivery across your organization.

---
Source: https://form2wf.com/docs/webhooks — Form2Workflow documentation (n8n)
