Webhooks Overview
Forge delivers form submissions to your application in real-time. This guide details how to consume our form.submission events securely.
Quick Summary
SecurityVerify
X-Forge-Signature (HMAC-SHA256).Replay ProtectionCheck
X-Forge-Timestamp (±5 min window).RetriesAutomatic exponential backoff on failure.
IdempotencyDeduplicate using
submission_id.Delivery Lifecycle
Events flow from Forge to your secure endpoint. We require a 2xx response to acknowledge receipt.
Envelope & Payload
All v1 events follow this envelope structure. The actual form data is nested within payload.data.
payload.json
{
"spec_version": "forge.forwarder.v1",
"event_id": "evt_123456789",
"event_type": "form.submission",
"occurred_at": "2025-12-25T12:34:56.789Z",
"workspace_id": "ws_abc123",
"form_id": "frm_xyz789",
"submission_id": "sub_987654", // Use this for idempotency
"payload": {
"data": {
"email": "alice@example.com",
"message": "Hello world"
},
"meta": {
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"submitted_at": "2025-12-25T12:34:56.000Z"
}
}
}Implementation (Node.js / Next.js)
Drop this handler into your API. It handles signature verification, replay protection, and basic validation.
route.tsProduction Ready
import crypto from 'crypto';
function verifySignature(rawBody, secret, timestamp, signatureHex) {
const now = Math.floor(Date.now() / 1000);
// Replay protection: 5 minute window
if (!timestamp || Math.abs(now - Number(timestamp)) > 300) return false;
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signatureHex, 'hex')
);
} catch (e) {
return false; // Handle length mismatch or encoding errors
}
}
export async function POST(req) {
const rawBody = await req.text();
// 1. Verify Headers
const sig = req.headers.get('x-forge-signature');
const ts = req.headers.get('x-forge-timestamp');
if (!sig || !ts) {
return new Response('Missing headers', { status: 400 });
}
// 2. Verify Signature
const secret = process.env.FORGE_WEBHOOK_SECRET;
if (!verifySignature(rawBody, secret, ts, sig)) {
return new Response('Invalid signature', { status: 401 });
}
// 3. Parse & Idempotency Check
const body = JSON.parse(rawBody);
const submissionId = body.submission_id;
if (await isDuplicate(submissionId)) {
return new Response('Already processed', { status: 200 });
}
// 4. Process Event
await saveSubmission(body);
return new Response('OK', { status: 200 });
}