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 });
}