Skip to content

Webhooks

Receive signed HTTP callbacks when things happen in your workspace.

Events

Eventdata
form.publishedformId, versionId, version, slug
form.updated
form.archivedformId
submission.createdsubmissionId, formId, version, status (no answers)
submission.completedsubmissionId, formId, version, answers, labels, computed, attribution, completedAt, paymentStatus
submission.deletedsubmissionId, 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.