Skip to Content
GuidesWebhook Processing

Webhook Processing

Webhooks let you run server-side logic whenever a form submission arrives: save to a database, send a Slack notification, trigger an email sequence, or anything else.

Formtorch sends a POST request to your endpoint with a JSON body and an HMAC-SHA256 signature in the X-FormTorch-Signature header.

Add your webhook endpoint in the dashboard

Go to Settings → WebhooksAdd Endpoint. Enter your URL, for example https://yourapp.com/api/webhooks/formtorch. Select the events you want (form.submitted, form.spam, form.test). Copy the signing secret.

Store the secret

.env.local
FORMTORCH_WEBHOOK_SECRET=your_signing_secret_here

Create the route handler

app/api/webhooks/formtorch/route.ts
import { createHmac, timingSafeEqual } from "crypto"; export async function POST(req: Request) { // Read the raw body BEFORE JSON.parse — the signature is over the raw string const body = await req.text(); const signature = req.headers.get("x-formtorch-signature") ?? ""; // Verify the signature const expected = `sha256=${createHmac( "sha256", process.env.FORMTORCH_WEBHOOK_SECRET! ) .update(body) .digest("hex")}`; const a = Buffer.from(signature); const b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) { return new Response("Unauthorized", { status: 401 }); } const event = JSON.parse(body) as FormtorchEvent; switch (event.event) { case "form.submitted": { const { id, data } = event.submission; console.log(`New submission ${id}:`, data); // e.g. save to your database, send to CRM, etc. break; } case "form.spam": { // optionally log or review spam submissions break; } case "form.test": { // test delivery — safe to ignore or log break; } } return new Response("OK"); } // TypeScript types matching the Formtorch payload shape type FormtorchEvent = { event: "form.submitted" | "form.spam" | "form.test"; timestamp: string; delivery_id: string; form: { id: string; name: string }; submission: { id: string; created_at: string; data: Record<string, unknown>; is_test: boolean; is_spam: boolean; }; };

Webhook payload shape

{ "event": "form.submitted", "timestamp": "2025-03-15T14:30:00.000Z", "delivery_id": "d3f456abc789", "form": { "id": "a1b2c3d4e5", "name": "Contact Form" }, "submission": { "id": "x9y8z7w6v5", "created_at": "2025-03-15T14:30:00.000Z", "data": { "name": "Alex", "email": "alex@example.com", "message": "Hello" }, "is_test": false, "is_spam": false } }

Reserved _* fields (like _redirect, _honeypot) are stripped from submission.data before delivery.

Always verify the signature

Skipping signature verification means anyone can send fake events to your endpoint.

Always verify x-formtorch-signature before processing. Read the raw request body as a string before parsing it as JSON — parsing first changes the string and breaks the signature check.

Retry behavior

If your endpoint returns a non-2xx response or times out (30 second limit), Formtorch retries with exponential backoff for up to 6 total attempts. Return 200 OK quickly and process asynchronously if your logic takes time.

Testing locally

Use a tunnel tool like ngrok  or Cloudflare Tunnel  to expose your local server during development. Add the tunnel URL as an endpoint in Settings → Webhooks, then click Send Test to trigger a form.test event without submitting a real form.

Learn more

Last updated on