Webhook Examples

Reference implementations for receiving Forge webhooks securely. These examples use Node.js/Express, but the patterns apply to any backend language (Python, Go, Ruby, etc.).


1. Basic Webhook Receiver

The simplest implementation accepts a POST request and returns a 200 OK status.

// server.js
const express = require('express');
const app = express();

// Ensure you can parse JSON bodies
app.use(express.json());

app.post('/webhooks/forge', (req, res) => {
  const payload = req.body;

  // 1. Log the event
  console.log(`Received submission: ${payload.submission_id}`);

  // 2. Process data (e.g., save to database)
  saveToDatabase(payload.data);

  // 3. Acknowledge receipt immediately
  res.status(200).send('OK');
});

app.listen(3000, () => console.log('Listening on port 3000'));

2. Verifying HMAC Signatures

To prevent spoofing, you must verify the X-Forge-Signature header. Forge signs the payload body using your webhook secret and HMAC-SHA256.

const crypto = require('crypto');

function verifySignature(req) {
  const signature = req.headers['x-forge-signature'];
  const secret = process.env.FORGE_WEBHOOK_SECRET;

  // Create HMAC using the raw body
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(JSON.stringify(req.body)).digest('hex');

  // Use timingSafeEqual to prevent timing attacks
  const signatureBuffer = Buffer.from(signature, 'utf8');
  const digestBuffer = Buffer.from(digest, 'utf8');

  return crypto.timingSafeEqual(signatureBuffer, digestBuffer);
}

app.post('/webhooks/forge', (req, res) => {
  if (!verifySignature(req)) {
    console.error('Invalid signature');
    return res.status(401).send('Unauthorized');
  }

  // Proceed with processing...
  res.status(200).send('Verified');
});

3. Handling Retries Safely

If your server returns anything other than a 2xx status (e.g., 500, 503, 404) or times out (10s limit), Forge will attempt to redeliver the webhook.

Best Practice: Perform heavy processing (e.g., sending emails, generating PDFs) asynchronously. Push the job to a queue and return 200 OK immediately to Forge.

app.post('/webhooks/forge', async (req, res) => {
  try {
    // 1. Validate signature (omitted for brevity)

    // 2. Push to internal queue (Redis/SQS/RabbitMQ)
    await myJobQueue.add('process-submission', req.body);

    // 3. Acknowledge immediately
    res.status(200).send('Queued');

  } catch (err) {
    // If your queue is down, return 500 to trigger Forge retry
    console.error(err);
    res.status(500).send('Internal Error');
  }
});

4. Idempotency Pattern

Because of retries, your endpoint may receive the same event twice. You should check thesubmission_id to prevent duplicate processing.

// Pseudocode example with a database

app.post('/webhooks/forge', async (req, res) => {
  const { submission_id } = req.body;

  // Check if we already processed this ID
  const exists = await db.processedEvents.find(submission_id);

  if (exists) {
    // Already processed? Return 200 so Forge stops retrying.
    return res.status(200).send('Already Processed');
  }

  // Process...
  await processSubmission(req.body);

  // Mark as processed
  await db.processedEvents.create(submission_id);

  res.status(200).send('OK');
});

5. Local Testing

You can test your endpoint locally using curlto simulate a Forge request.

curl -X POST http://localhost:3000/webhooks/forge \
  -H "Content-Type: application/json" \
  -H "X-Forge-Signature: test_signature_123" \
  -d '{
    "event": "submission.created",
    "submission_id": "sub_12345",
    "data": {
      "email": "dev@example.com",
      "message": "Hello world"
    }
  }'

6. Common Mistakes

Accidental 500s

Unhandled exceptions in your code will return a 500 error, causing Forge to retry the event unnecessarily. Always wrap webhook logic in try/catch blocks.

Long Processing

Webhooks time out after 10 seconds. Do not perform SMTP sending or heavy calculations directly in the request handler.

Next Steps