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.
Next.js
Add your webhook endpoint in the dashboard
Go to Settings → Webhooks → Add 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
FORMTORCH_WEBHOOK_SECRET=your_signing_secret_hereCreate the route handler
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
- Webhooks feature overview — setup, events, retry behavior, delivery log
- Webhook Events — full event type and payload reference
- Signature Verification — security implementation details
- Webhooks as an integration layer — connecting to Slack, Make, n8n, and other tools