Formfolk webhooks

Set up a webhook

  1. Open a form dashboard and find Integrations.
  2. Save a public HTTPS endpoint. New endpoints start paused.
  3. Copy the signing secret into your server’s secret storage.
  4. Send a test delivery and confirm your endpoint returns a 2xx response.
  5. Turn the webhook on.

Live response payload

Formfolk sends JSON with the event name, stable submission and form identifiers, the published form version, submission time, and answers keyed by question ID.

{
  "event": "form.response.created",
  "submission_id": "3a36d5a4-…",
  "form_id": "8ca31c85-…",
  "form_slug": "client-intake-a1b2c3",
  "form_version": 3,
  "submitted_at": "2026-09-22T16:30:00Z",
  "answers": {
    "name": "Alex Morgan",
    "services": ["Strategy", "Design"]
  }
}

Treat submission_id as the idempotency key in your system. A retry or manual resend keeps the same ID.

Headers

  • Content-Type: application/json
  • User-Agent: Formfolk-Webhook/1.0
  • X-Formfolk-Event: form.response.created
  • X-Formfolk-Signature: sha256=<hex digest>

Test deliveries use form.webhook.test and contain example answers rather than a real submission.

Verify the signature

Compute HMAC SHA-256 over the exact raw request body using the form’s signing secret. Compare the complete prefixed signature with a timing-safe comparison before parsing or acting on the payload.

import { createHmac, timingSafeEqual } from "node:crypto";

const signature = request.headers.get("x-formfolk-signature") ?? "";
const rawBody = await request.text();
const expected = "sha256=" + createHmac("sha256", process.env.FORMFOLK_WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");

const valid = signature.length === expected.length &&
  timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

if (!valid) return new Response("Invalid signature", { status: 401 });

Do not reserialize parsed JSON before verification; whitespace and key order would change the signed bytes.

Delivery and retries

Return any HTTP status from 200 through 299 only after your endpoint has safely accepted the event. Other statuses and timeouts are failures.

  • Formfolk retries new failures after approximately 1, 5, and 30 minutes.
  • Creators can manually send completed or failed deliveries again.
  • Delivery is at least once. Deduplicate downstream work using submission_id.
  • “Sent successfully” means your endpoint returned 2xx; it cannot confirm later downstream processing.

Secret rotation

Rotating the signing secret affects future attempts immediately, including retries and manual resends. Update your endpoint before rotating, or temporarily accept both secrets during the change.