data/skills/n8n-workflow-patterns/webhook_processing.md
Use Case: Receive HTTP requests from external systems and process them instantly.
Webhook → [Validate] → [Transform] → [Action] → [Response/Notify]
Key Characteristic: Instant event-driven processing
Purpose: Create HTTP endpoint to receive data
Configuration:
{
path: "form-submit", // URL path: https://n8n.example.com/webhook/form-submit
httpMethod: "POST", // GET, POST, PUT, DELETE
responseMode: "onReceived", // or "lastNode" for custom response
responseData: "allEntries" // or "firstEntryJson"
}
Critical Gotcha: Data is nested under $json.body
❌ {{$json.email}}
✅ {{$json.body.email}}
Purpose: Verify incoming data before processing
Options:
Example:
// IF node condition
{{$json.body.email}} is not empty AND
{{$json.body.name}} is not empty
Purpose: Map webhook data to desired format
Typical nodes:
Example (Set node):
{
"user_email": "={{$json.body.email}}",
"user_name": "={{$json.body.name}}",
"timestamp": "={{$now}}"
}
Purpose: Do something with the data
Common actions:
Purpose: Send custom HTTP response
Webhook Response Node:
{
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: {
"status": "success",
"message": "Form received"
}
}
Flow: Form → Webhook → Validate → Database → Email Confirmation
Example:
1. Webhook (path: "contact-form", POST)
2. IF (check email & message not empty)
3. Postgres (insert into contacts table)
4. Email (send confirmation to user)
5. Slack (notify team in #leads)
6. Webhook Response ({"status": "success"})
Real Data Access:
Name: {{$json.body.name}}
Email: {{$json.body.email}}
Message: {{$json.body.message}}
Flow: Payment Provider → Webhook → Verify → Update Database → Send Receipt
Security: Verify webhook signatures
// Code node - verify Stripe signature
const crypto = require('crypto');
const signature = $input.item.headers['stripe-signature'];
const secret = $credentials.stripeWebhookSecret;
// Verify signature matches
const expectedSig = crypto
.createHmac('sha256', secret)
.update($input.item.body)
.digest('hex');
if (signature !== expectedSig) {
throw new Error('Invalid webhook signature');
}
return $input.item.body; // Return validated body
Flow: Chat Command → Webhook → Process → Respond
Example (Slack slash command):
1. Webhook (path: "slack-command", POST)
2. Code (parse Slack payload: $json.body.text, $json.body.user_id)
3. HTTP Request (fetch data from API)
4. Set (format Slack message)
5. Webhook Response (immediate Slack response)
Slack Data Access:
Command: {{$json.body.command}}
Text: {{$json.body.text}}
User ID: {{$json.body.user_id}}
Channel ID: {{$json.body.channel_id}}
Flow: Git Event → Webhook → Parse → Notify/Deploy
Example (new PR notification):
1. Webhook (path: "github", POST)
2. IF (check $json.body.action equals "opened")
3. Set (extract PR details: title, author, url)
4. Slack (notify #dev-team)
5. Webhook Response (200 OK)
GitHub Data Access:
Event Type: {{$json.headers['x-github-event']}}
Action: {{$json.body.action}}
PR Title: {{$json.body.pull_request.title}}
Author: {{$json.body.pull_request.user.login}}
URL: {{$json.body.pull_request.html_url}}
Flow: Device → Webhook → Validate → Store → Alert (if threshold)
Example (temperature sensor):
1. Webhook (path: "sensor-data", POST)
2. Set (extract sensor readings)
3. Postgres (insert into sensor_readings)
4. IF (temperature > 80)
5. Email (alert admin)
{
"headers": {
"content-type": "application/json",
"user-agent": "...",
"x-custom-header": "..."
},
"params": {
"id": "123" // From URL: /webhook/form/:id
},
"query": {
"token": "abc" // From URL: /webhook/form?token=abc
},
"body": {
// ⚠️ YOUR DATA IS HERE!
"name": "John",
"email": "[email protected]"
}
}
// Headers
{{$json.headers['content-type']}}
{{$json.headers['x-api-key']}}
// URL Parameters
{{$json.params.id}}
// Query Parameters
{{$json.query.token}}
{{$json.query.page}}
// Body (MOST COMMON)
{{$json.body.email}}
{{$json.body.user.name}}
{{$json.body.items[0].price}}
Simple but less secure
// IF node - validate token
{{$json.query.token}} equals "your-secret-token"
Better security
// IF node - check header
{{$json.headers['x-api-key']}} equals "your-api-key"
Best security (for webhooks from services like Stripe, GitHub)
// Code node
const crypto = require('crypto');
const signature = $input.item.headers['x-signature'];
const secret = $credentials.webhookSecret;
const calculatedSig = crypto
.createHmac('sha256', secret)
.update(JSON.stringify($input.item.body))
.digest('hex');
if (signature !== `sha256=${calculatedSig}`) {
throw new Error('Invalid signature');
}
return $input.item.body;
Restrict access by IP (n8n workflow settings)
Behavior: Immediate 200 OK response, workflow continues in background
Use when:
Configuration:
{
responseMode: "onReceived",
responseCode: 200
}
Behavior: Wait for workflow completion, send custom response
Use when:
Configuration:
{
responseMode: "lastNode"
}
Then add Webhook Response node:
{
statusCode: 200,
headers: {
"Content-Type": "application/json"
},
body: {
"id": "={{$json.record_id}}",
"status": "success"
}
}
Main Flow:
Webhook → [nodes...] → Success Response
Error Flow:
Error Trigger → Log Error → Slack Alert → Error Response
Error Trigger Configuration:
{
workflowId: "current-workflow-id"
}
Error Response (if responseMode: "lastNode"):
{
statusCode: 500,
body: {
"status": "error",
"message": "Processing failed"
}
}
Webhook → IF (validate) → [True: Process]
└→ [False: Error Response]
False Branch Response:
{
statusCode: 400,
body: {
"status": "error",
"message": "Invalid data: missing email"
}
}
Per-node setting: Continue even if node fails
Use case: Non-critical notifications
Webhook → Database (critical) → Slack (continueOnFail: true)
Replace Webhook with Manual Trigger for testing:
Manual Trigger → [set test data] → rest of workflow
curl -X POST https://n8n.example.com/webhook/form-submit \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "name": "Test User"}'
Webhook → Queue (Redis/DB) → Response (immediate)
Separate Workflow:
Schedule → Check Queue → Process
{{$json.email}} // Empty or undefined
{{$json.body.email}} // Data is under .body
Using Webhook Response node with responseMode: "onReceived" (ignored)
Set responseMode: "lastNode" to use Webhook Response node
Assuming data is always present and valid
Validate data early with IF node or Code node
Using same path for dev/prod
Use environment variables: {{$env.WEBHOOK_PATH_PREFIX}}/form-submit
From n8n template library (1,085 webhook templates):
Simple Form to Slack:
Webhook → Set → Slack
Payment Processing:
Webhook → Verify Signature → Update Database → Send Receipt → Notify Admin
Chat Bot:
Webhook → Parse Command → AI Agent → Format Response → Webhook Response
Use search_templates({query: "webhook"}) to find more!
Key Points:
Pattern: Webhook → Validate → Transform → Action → Response
Related: