Webhooks are the backbone of modern automation. Instead of constantly checking for updates, you get notified automatically when something happens. In this guide, we explain webhooks from scratch: what they are, how they work, and how to use them in practice.
What is a Webhook?
Simple Explanation:
A webhook is like a doorbell. Instead of checking the door every 5 minutes (polling), the visitor rings the bell and you get notified immediately. Webhooks are a core concept in API integration and essential for real-time automation.
Technical:
A webhook is an HTTP POST request that a service sends to your URL when an event occurs.
Polling vs. Webhook
| Aspect | Polling | Webhook |
|---|---|---|
| Initiator | You ask | Service notifies you |
| Latency | Minutes (depending on interval) | Real-time (seconds) |
| Resources | Many unnecessary requests | Only on events |
| Complexity | Easier to set up | Requires public URL |
How Do Webhooks Work?
The Flow
1. You register your URL with the service
|
2. Event occurs (e.g., new order)
|
3. Service sends POST request to your URL
|
4. Your server receives the data
|
5. You process the data
|
6. You respond with 200 OK
A Typical Webhook Request
POST /webhook/orders HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
{
"event": "order.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"orderId": "12345",
"customerEmail": "customer@example.com",
"total": 99.00
}
}
Creating a Webhook Endpoint
Option 1: n8n Webhook
Step 1: Add Webhook Node
- HTTP Method: POST
- Path: orders (becomes /webhook/orders)
- Response Mode: When Last Node Finishes
Step 2: Copy URL
n8n shows two URLs:
- Test URL: For development (only when workflow is open)
- Production URL: For live operation (after activation)
Test: https://n8n.your-domain.com/webhook-test/orders
Prod: https://n8n.your-domain.com/webhook/orders
Step 3: Process Data
// Node: Code
// Process webhook data
const event = $json.body.event;
const data = $json.body.data;
console.log(`Event: ${event}`);
console.log(`Order ID: ${data.orderId}`);
return {
orderId: data.orderId,
email: data.customerEmail,
total: data.total,
processedAt: new Date().toISOString()
};
Option 2: Make.com Webhook
Step 1: Create Custom Webhook
https://hook.eu1.make.com/abc123xyz456
Step 2: Define Structure
Either:
- Automatically on first request
- Or manually in "Data structure" tab
Option 3: Your Own Server (Node.js)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/orders', (req, res) => {
const { event, data } = req.body;
console.log('Webhook received:', event);
// Here: Process data
processOrder(data);
// Always respond with 200 OK (quickly!)
res.status(200).json({ received: true });
});
app.listen(3000);
Securing Webhooks
1. Signature Verification
Most services sign their webhooks:
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return computed === signature;
}
// In your webhook handler:
const isValid = verifySignature(
JSON.stringify(req.body),
req.headers['x-webhook-signature'],
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
2. IP Whitelisting
Only accept requests from known IPs:
const allowedIPs = ['192.168.1.1', '10.0.0.1'];
if (!allowedIPs.includes(req.ip)) {
return res.status(403).json({ error: 'IP not allowed' });
}
3. Use HTTPS
Webhooks should ALWAYS run over HTTPS, never HTTP.
4. Timestamp Validation
Reject old requests (prevent replay attacks):
const timestamp = parseInt(req.headers['x-webhook-timestamp']);
const now = Math.floor(Date.now() / 1000);
if (now - timestamp > 300) { // Older than 5 minutes
return res.status(400).json({ error: 'Request too old' });
}
Practical Examples
Example 1: Shopify Order Webhook
Register webhook (Shopify Admin):
Payload example:
{
"id": 820982911946154500,
"email": "customer@example.com",
"created_at": "2024-01-15T10:30:00+01:00",
"total_price": "199.00",
"line_items": [
{
"title": "Product A",
"quantity": 2,
"price": "99.50"
}
]
}
n8n Workflow:
Shopify Webhook
|
Extract data
|
Parallel:
|-- Google Sheets: Log order
|-- Email: Send confirmation
+-- Slack: Notify team
For a complete Stripe integration walkthrough, see our dedicated Stripe webhook setup guide.
Example 2: Stripe Payment Webhook
Register webhook:
# Stripe Dashboard or CLI
stripe webhooks create \
--url https://n8n.your-domain.com/webhook/stripe \
--events payment_intent.succeeded,payment_intent.failed
Process payload:
// Node: Switch - By event type
const eventType = $json.type;
switch(eventType) {
case 'payment_intent.succeeded':
return { route: 'success' };
case 'payment_intent.failed':
return { route: 'failed' };
default:
return { route: 'ignore' };
}
Example 3: GitHub Push Webhook
Set up webhook:
Usage:
GitHub Push Webhook
|
Branch = "main"?
| Yes
SSH: Run deploy script
|
Slack: Notify about deployment
Example 4: Typeform Response Webhook
Enable webhook:
Use data:
// Typeform sends answers in structured format
const answers = $json.form_response.answers;
const name = answers.find(a => a.field.ref === 'name')?.text;
const email = answers.find(a => a.field.ref === 'email')?.email;
const message = answers.find(a => a.field.ref === 'message')?.text;
return { name, email, message };
Webhook Debugging
Local Development
Problem: Webhooks need a public URL.
Solution 1: ngrok
# Install ngrok
brew install ngrok
# Start tunnel
ngrok http 5678
# Shows public URL
# https://abc123.ngrok.io -> localhost:5678
Solution 2: Stripe CLI
stripe listen --forward-to localhost:5678/webhook/stripe
Request Inspection
Tool: RequestBin / Webhook.site
n8n Debug
Error Handling
Retries
If your endpoint fails, many services retry:
| Service | Retry Behavior |
|---|---|
| Stripe | 3 days, exponential backoff |
| Shopify | 19 attempts, 48 hours |
| GitHub | 3 attempts |
Idempotency
Webhooks can be delivered multiple times. Process each event only once:
// Save webhook ID
const webhookId = $json.id;
// Check if already processed
const existing = await db.get('processed_webhooks', webhookId);
if (existing) {
return { status: 'already_processed' };
}
// Process
await processWebhook($json);
// Mark as processed
await db.set('processed_webhooks', webhookId, {
processedAt: new Date().toISOString()
});
Async Processing
Respond quickly, process later:
app.post('/webhook', async (req, res) => {
// Respond immediately
res.status(200).json({ received: true });
// Process async (after response)
setImmediate(async () => {
try {
await processWebhook(req.body);
} catch (error) {
console.error('Webhook processing failed:', error);
await alertTeam(error);
}
});
});
Creating Webhook Documentation
If you offer your own webhooks, document them:
## Order Created Webhook
### Trigger
Fired when a new order is created.
### HTTP Request
- Method: POST
- Content-Type: application/json
### Headers
| Header | Description |
|--------|-------------|
| X-Webhook-Signature | HMAC-SHA256 signature |
| X-Webhook-Timestamp | Unix timestamp |
### Payloadjson
{
"event": "order.created",
"data": {
"orderId": "string",
"customerEmail": "string",
"total": "number"
}
}
### Response
Respond with HTTP 200 within 30 seconds.
Costs
| Solution | Cost |
|---|---|
| n8n Cloud | From $20/month |
| Make.com | From $9/month |
| Own Server | From $5/month (VPS) |
| Serverless (AWS Lambda) | Pay-per-use |
Conclusion
Webhooks are fundamental to modern automation:
- Real-time processing instead of polling
- Resource-efficient
- Easy to implement with n8n/Make.com
Next Steps
We can help you with webhook integration, from setup to production.