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.