Skip to content

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.

Your endpoint receives a JSON body:

{
"formId": "a1b2c3…",
"submissionId": "sub_…",
"createdAt": "2026-07-08T09:14:02.000Z",
"data": {
"name": "Jamie Rivera",
"email": "[email protected]",
"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.

HeaderValue
Content-Typeapplication/json
x-form-signaturesha256=<hex HMAC> of the raw request body, keyed with your webhook secret
idempotency-keyStable per delivery job — dedupe retries with it

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-serialized
app.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.

  • Success is any 2xx response from your endpoint.
  • Timeout is 10 seconds; redirects are not followed.
  • Retries: failed deliveries are retried with backoff. Use the idempotency-key header 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.