Webhooks
Webhooks POST each submission to an endpoint you control, so you can pipe form data into anything: a CRM, a database, an automation platform. Enable them per form in the dashboard’s Integrations tab.
Payload
Section titled “Payload”Your endpoint receives a JSON body:
{ "formId": "a1b2c3…", "submissionId": "sub_…", "createdAt": "2026-07-08T09:14:02.000Z", "data": { "name": "Jamie Rivera", "message": "We need 12 forms for a client project…" }, "meta": { "isSpam": false, "origin": "https://yoursite.com" }}data contains the submitted fields (reserved _ fields excluded). meta.isSpam lets you decide whether to process filtered submissions; by default only clean submissions are delivered.
Headers
Section titled “Headers”| Header | Value |
|---|---|
Content-Type | application/json |
x-form-signature | sha256=<hex HMAC> of the raw request body, keyed with your webhook secret |
idempotency-key | Stable per delivery job — dedupe retries with it |
Verifying the signature
Section titled “Verifying the signature”When you create the integration you get a secret (prefixed whsec_). Verify every request by computing an HMAC-SHA256 of the raw body and comparing constant-time:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, signatureHeader, secret) { const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`; const a = Buffer.from(expected); const b = Buffer.from(signatureHeader ?? ""); return a.length === b.length && timingSafeEqual(a, b);}
// Express example — note express.raw() so the body isn't re-serializedapp.post("/hooks/formwire", express.raw({ type: "application/json" }), (req, res) => { if (!verify(req.body, req.get("x-form-signature"), process.env.FORMWIRE_WEBHOOK_SECRET)) { return res.status(401).end(); } const submission = JSON.parse(req.body); // …do something useful res.status(200).end();});Compute the HMAC over the raw bytes you received. Parsing and re-stringifying JSON can reorder keys and will produce a different digest.
Delivery behaviour
Section titled “Delivery behaviour”- Success is any
2xxresponse from your endpoint. - Timeout is 10 seconds; redirects are not followed.
- Retries: failed deliveries are retried with backoff. Use the
idempotency-keyheader to deduplicate — it stays the same across retries of one delivery. - Nothing is lost: the submission is stored before delivery is attempted. If your endpoint is down, the record stays in your dashboard regardless.
- You can send a test delivery from the dashboard to check your endpoint end to end.