KEEP LEARNING
Build the bigger picture.
The Workflow Engineer connects individual n8n concepts to testing, deployment and running a complete workflow.
Tips > Building Workflows
A webhook relay receives an incoming webhook, transforms the payload, and fans it out to multiple destinations.
A webhook relay is a workflow that receives one incoming webhook, transforms the payload, and fans it out to multiple destinations. It decouples the source system from its downstream consumers: the source sends a single webhook, and the relay handles distribution, per-destination transformation, and formatting so each system receives exactly the data shape it needs.
Real-world example: Stripe sends payment events to a single webhook endpoint. Your relay routes the event to your application backend, your analytics pipeline, and your accounting system, each receiving the data in its own format.
Webhook Trigger (/webhook/stripe-relay)
|
Code Node: Validate Stripe Signature
|
Switch Node: Route by Event Type
|
+-> "charge.succeeded":
| +-> HTTP Request: App Backend (full payload)
| +-> HTTP Request: Analytics (minimal payload)
| +-> HTTP Request: Accounting System (financial fields only)
|
+-> "customer.subscription.updated":
| +-> HTTP Request: App Backend
| +-> HTTP Request: CRM Update
|
+-> Default:
+-> Log to Google Sheets (unhandled event types)
Code Node: Validate Stripe Signature
const crypto = require('crypto');
// Use rawBody to get the exact bytes sent by Stripe.
// Never use JSON.stringify() -- it may reorder keys or alter whitespace.
const payload = $json.rawBody;
const signature = $json.headers['stripe-signature'];
const webhookSecret = $env.STRIPE_WEBHOOK_SECRET;
// Parse the Stripe signature header
const elements = signature.split(',');
const timestamp = elements
.find(e => e.startsWith('t='))?.split('=')[1];
const expectedSig = elements
.find(e => e.startsWith('v1='))?.split('=')[1];
// Compute the expected signature
const signedPayload = `${timestamp}.${payload}`;
const computedSig = crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
// Use constant-time comparison to prevent timing attacks
const computedBuffer = Buffer.from(computedSig, 'utf8');
const expectedBuffer = Buffer.from(expectedSig || '', 'utf8');
if (computedBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(computedBuffer, expectedBuffer)) {
throw new Error('Invalid Stripe signature');
}
// Signature valid -- pass through the event
return [{ json: $json.body }];
Warning: Enable Raw Body on the Webhook node
This code requires the Webhook node's Raw Body option to be enabled. Without it, rawBody will be undefined and signature verification will fail silently.
HTTP Request: Analytics (minimal payload)
{
"method": "POST",
"url": "https://analytics.example.com/api/events",
"sendBody": true,
"bodyContentType": "json",
"jsonBody": {
"event": "={{ $json.type }}",
"amount": "={{ $json.data.object.amount }}",
"currency": "={{ $json.data.object.currency }}",
"timestamp": "={{ $json.created }}"
}
}
Tip: Respond Immediately
Configure the Webhook node with Response Mode: Immediately. Stripe (and most webhook senders) require a 200 response within a few seconds. Your relay should acknowledge receipt immediately, then process asynchronously. If per-destination delivery fails, that is handled by retry logic on each downstream branch, not by making Stripe wait.
This pattern centralizes webhook management, eliminates duplicate webhook configurations in the source system, and lets each destination receive exactly the data format it needs.
Related: Use Path Parameters in Webhook URLs for Dynamic Routing · Use Edit Fields in "Map Each" Mode for Simple Renames
KEEP LEARNING
The Workflow Engineer connects individual n8n concepts to testing, deployment and running a complete workflow.
APPLY IT TO YOUR SYSTEM
Bring the process, the tools involved and an example of where the current workflow gets stuck.