Webhooks
Receive signed HTTP callbacks when things happen in your workspace.
Events
| Event | data |
|---|---|
form.published | formId, versionId, version, slug |
form.updated | |
form.archived | formId |
submission.created | submissionId, formId, version, status (no answers) |
submission.completed | submissionId, formId, version, answers, labels, computed, attribution, completedAt, paymentStatus |
submission.deleted | submissionId, formId |
payment.status_changed | |
workflow.completed | |
workflow.failed | |
experiment.started | |
experiment.paused | |
experiment.guardrail_breached | |
experiment.winner_declared | |
experiment.promoted |
Payload
json
{
"id": "evt_abc123",
"type": "submission.completed",
"created": 1793000000,
"workspace_id": "ws_...",
"data": {
"submissionId": "sub_...", "formId": "frm_...", "version": 3,
"answers": { "email": "ada@example.com" },
"labels": { "email": "Email address" },
"computed": { "scores": {}, "quotaIds": [] },
"attribution": { "utm_source": "newsletter", "channel": "email" },
"completedAt": 1793000000000, "paymentStatus": "none"
}
}Verifying signatures
Each request carries FormFlow-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret. Reject timestamps older than 5 minutes. Always verify against the raw body.
ts
import { verifyWebhookSignature } from "@formizeflow/sdk";
export async function POST(req: Request) {
const raw = await req.text();
const ok = await verifyWebhookSignature(raw, req.headers.get("formflow-signature"), process.env.FORMFLOW_WEBHOOK_SECRET!, 300);
if (!ok) return new Response("bad signature", { status: 400 });
const event = JSON.parse(raw);
// handle event.type ...
return new Response("ok");
}js
// Node without the SDK
const crypto = require("node:crypto");
function verify(raw, header, secret, tolerance = 300) {
const p = Object.fromEntries(header.split(",").map((s) => s.split("=")));
if (Math.abs(Date.now() / 1000 - Number(p.t)) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(p.t + "." + raw).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1 ?? ""));
}Delivery
Any 2xx acknowledges. Failures are retried with exponential backoff; endpoints that keep failing are disabled. Use id to deduplicate. Replay from the deliveries log or POST /webhooks/:id/deliveries/:deliveryId/redeliver.