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:00+00:00",
"fields": { "name": "Jan", "email": "jan@example.com" },
"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.

If you collect the visit path, you can send it to a webhook. It is off by default on every webhook — turn on Send the visit path on the webhook in the form editor. Collecting the path is a separate switch in Privacy settings, so the webhook toggle alone sends nothing.

When it is on, the path arrives inside meta:

"meta": {
"url": "https://example.com/contact",
"user_path": [
{ "t": "2026-07-11T10:20:30.000Z", "path": "/" },
{ "t": "2026-07-11T10:22:40.000Z", "path": "/contact" }
]
}

If you add user_path to a webhook’s Anonymize map, the whole key is removed from that webhook’s payload (masking a path is meaningless, so it is dropped rather than half-hidden).

One note if you use the octa_forms_webhook_payload filter: it runs once, before the per-webhook step, so your filter code always sees the full meta including user_path, regardless of any webhook’s toggle. That is your own server code, not a leak, but worth knowing.

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.