n8n Webhook Node: Response Modes, Custom Headers, and Auth (Free Workflow JSON)
The n8n Webhook node is where most automation workflows begin â but most tutorials only cover the basics: receive a request, do something, done. This guide goes deeper: response modes, custom response headers, header-based auth, managing multiple HTTP methods, and real-world patterns that handle edge cases cleanly.
What we cover:
- The 4 response modes and when to use each
- Custom response headers and status codes
- Webhook auth: header secret, HMAC signature, and IP allowlist
- Handling GET vs POST on the same webhook path
- Idempotency: safely processing the same event twice
- Free workflow JSON at the end
1. Response Mode Overview
Every n8n Webhook node has a Response Mode setting. Choosing the wrong one is the most common cause of integration failures.
| Mode | What it does | Use when |
|---|---|---|
| Immediately | Responds 200 OK as soon as the request arrives | Best for Stripe, GitHub, Slack â they just want acknowledgement |
| When Last Node Finishes | Waits for the full workflow to complete, returns the last node output | Use for synchronous APIs that expect a real response body |
| Using Respond to Webhook Node | You control exactly what gets returned | Best for GraphQL-style APIs, custom error formats, partial results |
| No Response | Returns nothing | Rare; use only when the caller manages the connection itself |
Trap: Using "When Last Node Finishes" with a Stripe webhook causes Stripe to retry. Stripe expects a 200 within 5 seconds. Use Immediately for any webhook that just needs acknowledgement.
2. The Respond to Webhook Node
When you need full control over the response body, status code, and headers, set Webhook Response Mode to Using Respond to Webhook Node, then add a Respond to Webhook node anywhere in the flow.
Example â return 201 Created with the new record ID by setting the Respond to Webhook node:
- Status Code: 201
- Body Type: JSON
- Body:
{"success": true, "id": "{{$('Save to DB').item.json.id}}"}
You can place multiple Respond to Webhook nodes on different branches (success, error). Only the first one that executes fires â the others are skipped.
3. Custom Response Headers
Use case: your webhook returns JSON and needs CORS headers so a browser can call it directly.
In the Respond to Webhook node, enable Custom Headers and add:
Access-Control-Allow-Origin: *Content-Type: application/jsonX-Request-Id: {{$execution.id}}
Or set headers on the Webhook node itself under Response Headers for headers that should always be present.
4. Securing Your Webhook
4a. Header Secret (Simplest)
Most SaaS platforms send a secret in a header (e.g., X-Webhook-Secret). Validate it with an IF node right after the Webhook node:
- Condition:
{{$headers['x-webhook-secret']}}equals{{$vars.WEBHOOK_SECRET}} - True branch: continue processing
- False branch: Respond to Webhook with status 401
Store the secret in n8n Variables ($vars.WEBHOOK_SECRET), not hardcoded in the expression.
4b. Stripe-Style HMAC Signature
Stripe signs the payload with HMAC-SHA256. Use the Code node to verify â and enable Raw Body in the Webhook node Options first (the signature is over the raw bytes, not the parsed JSON):
const crypto = require('crypto');
const sig = $input.first().headers['stripe-signature'];
const payload = $input.first().rawBody;
const secret = $vars.STRIPE_WEBHOOK_SECRET;
const [tPart, v1Part] = sig.split(',');
const timestamp = tPart.replace('t=', '');
const received = v1Part.replace('v1=', '');
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
if (expected !== received) throw new Error('Invalid Stripe signature');
return $input.all();
4c. IP Allowlist
const allowed = ['192.30.252.', '185.199.108.']; // GitHub webhook IP prefixes
const ip = ($input.first().headers['x-forwarded-for'] || '').split(',')[0].trim()
|| $input.first().headers['x-real-ip'] || '';
if (!allowed.some(prefix => ip.startsWith(prefix))) {
throw new Error('Blocked IP: ' + ip);
}
return $input.all();
5. Handling GET and POST on the Same Path
n8n creates separate Webhook nodes per HTTP method. To handle both GET and POST at the same logical path, use two Webhook nodes (same path, different method settings) merged downstream:
GET /endpoint -> [Webhook GET] \
[Merge: Multiplex] -> [Process]
POST /endpoint -> [Webhook POST] /
Set the Merge node to Multiplex mode so each incoming item is processed independently.
6. Idempotency: Safely Re-Processing Retried Events
Webhooks get retried. Prevent duplicate inserts with the Redis node (SETNX pattern):
-
Redis GET
processed:{{$json.id}} - IF key exists â Respond 200 "already processed" (stop)
-
IF key missing â Process the event â Redis SET
processed:{{$json.id}}value1TTL 604800 (7 days)
This guarantees each event ID is processed exactly once regardless of how many times the platform retries.
7. Free Workflow JSON
Production-ready webhook with header-secret auth, custom response, and error branch:
{
"name": "Secure Webhook Template",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "secure-hook",
"responseMode": "responseNode",
"options": { "rawBody": true }
},
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [240, 300]
},
{
"parameters": {
"conditions": {
"string": [{"value1": "={{$headers['x-webhook-secret']}}", "value2": "={{$vars.WEBHOOK_SECRET}}", "operation": "equal"}]
}
},
"name": "Auth Check",
"type": "n8n-nodes-base.if",
"position": [440, 300]
},
{
"parameters": { "statusCode": 401, "body": "{\"error\":\"Unauthorized\"}" },
"name": "Respond 401",
"type": "n8n-nodes-base.respondToWebhook",
"position": [640, 460]
},
{
"parameters": { "statusCode": 200, "body": "{\"ok\":true}" },
"name": "Respond 200",
"type": "n8n-nodes-base.respondToWebhook",
"position": [840, 300]
}
],
"connections": {
"Webhook": { "main": [[{"node": "Auth Check", "type": "main", "index": 0}]] },
"Auth Check": {
"main": [
[{"node": "Respond 200", "type": "main", "index": 0}],
[{"node": "Respond 401", "type": "main", "index": 0}]
]
}
}
}
Import into n8n, set WEBHOOK_SECRET in Variables, activate â done in under 2 minutes.
8. Common Gotchas
| Gotcha | Fix |
|---|---|
rawBody is undefined |
Enable Raw Body in Webhook node Options |
| Stripe keeps retrying | Switch to Immediately response mode |
| HMAC signature always fails | Enable Raw Body; verify you are reading the correct header name |
| Webhook not triggering in production | Activate the workflow â test URL bypasses activation; production URL requires it |
| CORS errors from browser | Add Access-Control-Allow-Origin header in Respond to Webhook |
| Test vs live behaves differently | Test URL runs synchronously; production URL runs async in the background |
9. Related Guides
- n8n HTTP Request Node: Advanced Patterns
- n8n Redis Node: Cache, Queue, and Rate Limiting
- n8n Error Handling: Catch Errors and Build Resilient Workflows
Want 10 production-ready n8n workflows (including the Webhook Auth + Idempotency template)? Grab the n8n Workflow Starter Pack ($29) â includes webhooks, lead capture, Stripe fulfillment, Slack notifications, and more.
Drop a comment with your toughest webhook edge case â I will share the specific pattern for it.
Top comments (1)
Which response mode do you use most often? Have you hit the Stripe retry problem from picking the wrong one? Drop your webhook setup in the comments.