Webhooks
After an accepted submission is saved, FormBeam POSTs signed JSON to your HTTPS endpoint. The submitter is never blocked on your server.
1. Open the form → Webhook.
2. Paste an HTTPS URL (webhook.site works for a smoke test).
3. Save. Copy the whsec_ secret once. Rotate later if it leaks.
4. Use Send test ping, then watch the delivery log.
id and the same bytes.{"id": "evt_01J...","type": "submission.accepted","created_at": "2026-09-11T21:04:00.000Z","livemode": true,"data": {"form": { "id": "...", "slug": "contact", "name": "Contact" },"submission": {"id": "42","created_at": "2026-09-11T21:03:59.000Z","fields": { "name": "Ada", "email": "ada@example.com" },"files": [{ "name": "ada.pdf", "size": 18221 }],"meta": { "ip": "203.0.113.10", "user_agent": "...", "referrer": "..." }}}}
File URLs are omitted unless FormBeam already has a safe signed URL. Test pings use type: "ping" and do not create a submission.
Content-Type: application/json; charset=utf-8 User-Agent: FormBeam-Webhook/1.0 X-FormBeam-Event: submission.accepted X-FormBeam-Delivery: <uuid> X-FormBeam-Event-Id: evt_... X-FormBeam-Timestamp: 1770000000 X-FormBeam-Signature: t=1770000000,v1=<hex>
signed_payload = {timestamp}.{raw_body}. HMAC key is the full whsec_... string. Compare with a constant-time function. Reject if |now - timestamp| > 300s. Deduplicate on X-FormBeam-Event-Id / payload id.
Node
const crypto = require("crypto");function verifyFormBeamWebhook({ secret, rawBody, signatureHeader, timestampHeader }) {const skew = 300;const ts = Number(timestampHeader);if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > skew) {throw new Error("timestamp outside window");}const signed = `${ts}.${rawBody}`;const expected ="t=" + ts + ",v1=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");const a = Buffer.from(expected);const b = Buffer.from(signatureHeader);if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {throw new Error("bad signature");}}// HMAC key is the full whsec_... string (not base64-decoded).
Go
mac := hmac.New(sha256.New, []byte(secret)) // full whsec_... stringfmt.Fprintf(mac, "%d.", timestamp)mac.Write(rawBody)v1 := hex.EncodeToString(mac.Sum(nil))expected := fmt.Sprintf("t=%d,v1=%s", timestamp, v1)if !hmac.Equal([]byte(expected), []byte(header)) {return errors.New("bad signature")}
Success is HTTP 2xx within 5 seconds. Redirects are not followed.
Eight attempts over about 24 hours: 0s, 30s, 2m, 10m, 30m, 2h, 6h, 24h (with jitter).
410 Gone stops retries. 429, 5xx, timeouts, and connect errors retry. Other 4xx are terminal. After the last failure, auto-retry stops; the webhook stays enabled so you can fix the URL and redeliver.
Use webhook.site for a public HTTPS URL. ngrok/Cloudflare Tunnel also work. Localhost and RFC1918 addresses are rejected (SSRF).
Submit worked but Slack did not. Check the delivery log on the form. Webhooks are async; a 500 from Slack still saved the lead. Redeliver after you fix the URL.
Old secret after rotate. In-flight and new deliveries sign with the secret stored on the webhook at send time. Update your receiver.