The Webhook node is the easiest way to get n8n listening for incoming events, and that ease is exactly why it's misused: it's tempting to wire it straight to business logic and call it done. The workflows that hold up under real traffic add three things the default setup doesn't give you — signature verification so you know the request is genuinely from Stripe or GitHub, idempotency so a retried delivery doesn't double-process, and an explicit choice about whether to respond before or after the work finishes.
Verifying incoming webhook signatures
Every webhook endpoint is a public URL, and n8n's Webhook node will happily execute the workflow for anyone who finds it — there's no built-in requirement that the payload actually came from the provider it claims to. Stripe signs webhook payloads with an HMAC in the Stripe-Signature header; GitHub does the same with X-Hub-Signature-256. Verify it in a Code node immediately after the trigger, before any node touches the database or calls another API, and stop the workflow if verification fails.
const crypto = require('crypto');
const signatureHeader = $input.first().json.headers['stripe-signature'];
const rawBody = $input.first().json.body_raw; // configure Webhook node to pass raw body
const secret = $env.STRIPE_WEBHOOK_SECRET;
const [tPart, v1Part] = signatureHeader.split(',');
const timestamp = tPart.split('=')[1];
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
if (expected !== v1Part.split('=')[1]) {
throw new Error('Invalid Stripe signature — rejecting webhook');
}
return $input.all();
HMAC verification needs the exact bytes that were signed. If n8n has already parsed the payload as JSON and re-serialized it, whitespace differences will break the signature check. Set the Webhook node's response mode to keep raw body access, or verify against the raw text before any JSON.parse happens.
Idempotency for retried deliveries
Stripe and GitHub both retry webhook deliveries on timeout or non-2xx response, which means your workflow will occasionally see the same event twice — sometimes minutes apart, sometimes after your workflow already fully processed it once. Every event from Stripe carries a unique id (e.g. evt_1N...); GitHub sends a delivery ID in the X-GitHub-Delivery header. Store processed event IDs in a table or key-value store and check it before doing any side-effecting work — the check-then-insert needs to happen inside the workflow, not as an afterthought, or you'll double-charge or double-notify on the retry.
Respond to Webhook immediately, or after processing
n8n gives you two shapes for responding to the caller: the Webhook node's own "Respond Immediately" setting, or an explicit Respond to Webhook node placed later in the workflow. If the provider expects a fast 2xx (Stripe times out around 20 seconds and will retry on timeout), don't make it wait for a slow downstream call — respond immediately with a 200, then continue the rest of the workflow asynchronously. If you need to return a computed result to the caller synchronously (an internal API-style webhook, not a third-party notification), place Respond to Webhook after the processing nodes so the response carries real data.
{
"name": "Stripe Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": {
"path": "stripe/invoice-paid",
"responseMode": "responseNode",
"options": { "rawBody": true }
}
}
// downstream: Respond to Webhook node fires a 200 immediately,
// a parallel branch continues to verify signature, check idempotency,
// then update the invoice record
Error workflows for what slips through
Every workflow that handles external input needs a designated error workflow attached in its settings — n8n will route unhandled node failures there instead of just failing silently or returning a generic 500 to the provider. A minimal error workflow logs the failed execution ID, the triggering payload, and posts a notification (Slack, email, whatever your team actually looks at) so a bad deploy or a malformed payload from a provider gets noticed in minutes, not discovered when a customer complains that their payment never triggered fulfillment.
Stripe's CLI (stripe trigger) and GitHub's webhook redelivery UI both let you resend a real event on demand. Use them to confirm your idempotency check actually blocks a duplicate — a workflow that only gets tested with fresh, unique payloads will pass every manual test and still double-process in production.
Wrapping up
A webhook trigger that only handles the well-formed, first-delivery, friendly case is a demo, not a production integration. Verify the signature before trusting the payload, de-duplicate on the provider's event ID before doing side-effecting work, decide deliberately whether the response should fire before or after processing, and give every webhook workflow an error workflow to catch what you didn't anticipate.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.