Webhooks
On paid plans, FormDoc can send each accepted submission to your own URL. Add an endpoint from your form’s Integrations tab.
What we send
POST <your URL> with Content-Type: application/json:
{
"event": "submission.created",
"submission": {
"id": "01J…",
"form_id": "01J…",
"form_name": "Contact",
"received_at": "2026-10-29T10:00:00.000Z",
"fields": { "name": "Asha", "email": "asha@example.com", "message": "Hi!" },
"origin": "https://example.com",
"country": "IN"
}
}
The Send test button sends the same shape with "event": "test".
Verifying the signature
Every request has an X-Formdoc-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256>. The signature is computed over "<t>.<raw body>" with your endpoint’s secret (whsec_…). The secret is shown once when you add the endpoint, and you can rotate it.
import crypto from "node:crypto";
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds;
return (
fresh &&
parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
);
}
Delivery and retries
- Any
2xxresponse within 10 seconds counts as delivered. Redirects aren’t followed. - Timeouts,
408,429and5xxresponses are retried: 6 attempts in total, after about 1 minute, 5 minutes, 15 minutes, 1 hour and 3 hours. Other responses fail at once. - Every attempt appears in the delivery log, which has a Retry button.
- Endpoints must be public
https://addresses on the default port. IP addresses and internal hostnames are refused.