Developer guide
Formfolk webhooks
Receive every new form response at your own HTTPS endpoint, verify that it came from Formfolk, and recover safely when downstream work fails.
Last updated September 22, 2026
Set up a webhook
- Open a form dashboard and find Integrations.
- Save a public HTTPS endpoint. New endpoints start paused.
- Copy the signing secret into your server’s secret storage.
- Send a test delivery and confirm your endpoint returns a 2xx response.
- 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/jsonUser-Agent: Formfolk-Webhook/1.0X-Formfolk-Event: form.response.createdX-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.