Webhook payload reference
FormNode emits webhooks for two events: a form submission and an approval decision. Both follow the same delivery contract — POST, JSON body, exponential-backoff retries, and a delivery log per attempt. This page is the reference for the exact shape and behavior.
Form submission webhook
Fired immediately after a successful form submission, when the form has a webhook configured. The receiving endpoint can be an n8n webhook node or any HTTPS endpoint that accepts JSON.
Headers
POST /webhook/onboard HTTP/1.1
Host: your-n8n.example.com
Content-Type: application/json
User-Agent: FormNode/1.0Form submission webhooks send a single FormNode-specific header, User-Agent: FormNode/1.0. Use submissionId from the body as your idempotency key — see retry behavior below.
Webhook subscriptions (a separate, signed-delivery system you can configure per-form for fan-out to many endpoints) send these additional headers:
POST /webhook/subscriber HTTP/1.1
Host: your-endpoint.example.com
Content-Type: application/json
X-FormNode-Event: form.submitted
X-FormNode-Signature: sha256=<hex digest>X-FormNode-Signatureis an HMAC-SHA256 of the body using the subscription's shared secret — verify it on receipt to confirm the request actually came from FormNode.
Body
{
"eventType": "form.submitted",
"formId": "5a8b3c2d-1e4f-4a9b-8c7d-6e5f4a3b2c1d",
"organizationId": "9c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
"submittedByUserId": null,
"slug": "acme",
"organizationName": "Acme",
"integrationMappings": {
"cw.companyId": "192837"
},
"submissionId": "8f2a4e1c-6b7d-4f3a-9e8d-1c2b3a4d5e6f",
"submittedAt": "2026-04-25T18:42:11.337Z",
"data": {
"first_name": "Alex",
"last_name": "Morgan",
"email": "alex@acme.example",
"title": "Senior Field Tech",
"manager": "Sarah Chen",
"start_date": "2026-05-15",
"license_sku": "SPB"
}
}Field-by-field:
eventType— alwaysform.submittedfor this eventformId— the FormNode form ID (UUID). Use this when you need to fetch the form definition back via the REST API.organizationId,slug, andorganizationName— three views of the same tenant. Useslugfor human-readable routing in n8n; useorganizationIdwhen you need to call back into the FormNode API.submittedByUserId— the FormNode user ID when the submission came from an authenticated portal user, otherwisenull.integrationMappings— the resolved organization mapping values, including inherited parent-organization mappings. Child values override parent values when keys overlap.submissionId— unique per submission (UUID). Idempotency key.submittedAt— ISO 8601 timestamp at the moment of submission. Use this rather than your local clock for any time-sensitive logic.data— the form values, keyed by field name (not label). File fields are URLs to a temporary signed download. Tables are arrays of row objects.
All FormNode IDs are UUIDs without prefixes — there's no frm_, sub_, etc.
Approval responded webhook
Fired when an approver clicks Approve or Reject in the email or in the portal. Posts to the callback URL configured on the approval at create time (or to the form's automation webhook if callbackUrl wasn't passed). Event type is approval.responded.
{
"eventType": "approval.responded",
"approvalId": "8f2a4e1c-6b7d-4f3a-9e8d-1c2b3a4d5e6f",
"formId": "5a8b3c2d-1e4f-4a9b-8c7d-6e5f4a3b2c1d",
"status": "responded",
"decision": "approved",
"comments": null,
"contextData": { /* whatever you sent when creating the approval */ },
"responseData": { "decision": "approved" },
"respondedAt": "2026-04-25T18:42:11.337Z",
"respondedBy": null,
"respondentInput": {
"email": "client@example.com",
"name": null
}
}For decision-table approvals, the per-row decisions are in responseDatakeyed by the decision-table field's name. See Approvals for the full decision-table walkthrough.
Retry behavior
FormNode considers any non-2xx response (including 3xx) a delivery failure. Retries follow exponential backoff with a max of six attempts total (one immediate + five retries):
- Attempt 1: immediate, at submission time
- Attempt 2: after 5 minutes
- Attempt 3: after 30 minutes
- Attempt 4: after 2 hours
- Attempt 5: after 12 hours
- Attempt 6: after 24 hours
- After 6 attempts: marked permanently failed
The submission record stores the cached request body (encrypted) until delivery succeeds or attempts exhaust. Once delivered, the cached body is wiped immediately.
How to make your endpoint retry-safe
Every retry replays the same body with the same submissionId for form submissions or approvalIdfor approval callbacks. Treat that ID as your idempotency key — if you've already processed it, return 200 anyway and skip the side effects.
Retention
Submission records are retained based on delivery state. Once a webhook is delivered, the cached request body is cleared and the delivered submission is retained for 24 hours. Failed, pending, and no-webhook submissions are retained for 7 days. Failed submissions retry during that retention window until the six-attempt limit is reached.
n8n integration tips
For the n8n-specific webhook form reference, see n8n webhook form builder. This page remains the general payload contract for every webhook destination.
Reading the payload
In an n8n Webhook node, the entire body is available at $json. The form values are at $json.data.field_name. Common shape — pull a license SKU and an email out of an onboarding submission:
{
"license_sku": "{{ $json.data.license_sku }}",
"user_email": "{{ $json.data.email }}"
}Routing by organization
For workflows that handle multiple customers off a single webhook endpoint, switch on $json.organizationId or $json.slugat the top of your workflow. Most teams keep per-customer config (CIPP tenant ID, ConnectWise company ID, etc.) in the FormNode organization's integration mappings and read it back via the API rather than duplicating it in n8n.
The official community node
Install @joshuanode/n8n-nodes-formnode from npm and you get native nodes for organizations, forms, submissions, and approvals — no more raw HTTP Request nodes when calling FormNode back. The webhook payload shape is the same; the community node is for the outbound API calls from your workflow.
Troubleshooting
Webhook fires but the data object is empty
The form has fields but the submitter didn't fill anything that was visible. Conditional fields evaluated to hidden are not included in the payload. If you expect specific keys to always be present, set those fields to required and uncondition them.
Field appears in the form but not in the payload
Check the field's namein the form builder settings panel. Auto-generated names from the label can drift if you renamed the label later. The payload key is whatever the field's name is at submit time.
Same submission ID appearing twice
Almost always a retry. Check the submission's delivery log in the FormNode dashboard — if attempt 1 returned a 2xx but you didn't process it, that's a bug to fix on your side. If attempt 1 timed out before you returned, FormNode treats it as failed and retries. Idempotency on your handler is the right fix in either case.