Skip to content

Webhooks

A webhook sends a POST to your own URL for every submission. Use it to push leads into a CRM, a Slack channel, a spreadsheet, or any system that can accept an HTTP request.

Webhooks live in the form’s config as an array. Each one needs an https URL, an active flag, and a secret:

"webhooks": [
{
"url": "https://hooks.example.com/octaforms",
"active": true,
"secret": "a-long-random-secret-at-least-32-chars"
}
]

A form can have several. The secret must be at least 32 characters, because it signs the payload and a short secret is guessable. The editor blocks saving an active webhook with a weak or missing secret.

Each webhook receives a JSON body:

{
"id": 123,
"form_id": 45,
"form_name": "Contact",
"created": "2026-07-08T10:15:00Z",
"fields": { "name": "Jan", "email": "[email protected]" },
"meta": { "url": "https://example.com/contact" }
}

form_name is the form’s title from the admin, sent for readability. It can change when someone renames the form, so key your logic on form_id, which stays stable.

File fields appear as metadata only, never the file itself:

"cv": [{ "name": "resume.pdf", "mime": "application/pdf", "size": 84213 }]

The bytes are left out on purpose. Base64-encoding a file would bloat the signed payload, and a receiving machine has no WordPress session to fetch a file with. The actual file goes to you as an email attachment and is available in the admin.

Every webhook request carries a signature header:

X-Octa-Forms-Signature: sha256=<hmac_sha256(secret, body)>

Verify it against the raw request body, using the webhook’s secret. If it does not match, reject the request. This is what stops anyone else from posting fake submissions to your endpoint.

Delivery is at-least-once. Under retries, the same webhook can arrive more than once. Your endpoint must deduplicate using the payload’s id, which is stable for a given submission. Treat id as an idempotency key: if you have already processed it, acknowledge and move on.

Each webhook in the form editor has a Test button next to its Active switch. It sends one webhook right away, using the URL and secret you have typed in (you do not need to save first). The body carries sample values but the real keys, the same envelope, and a valid signature, so you can set up and confirm your receiver (n8n, Zapier, a custom endpoint) before any real submission arrives. Save the form once before the first test, so it has an address to send to.

A test payload carries "test": true and a placeholder id like test-<uuid>, so your endpoint can tell a test apart from a real submission. The same marker is used by the full test panel under the form’s Tools.

Outgoing webhook URLs are re-checked for safety before every send, but the gap between resolving the hostname and connecting is not pinned to the resolved address. This is documented for completeness. For normal endpoints you control, it does not affect you.