Creating Webhooks: The Complete Guide for Beginners (with n8n and Make.com)
What are webhooks and how do you create them? Simple guide with examples.
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.
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
|
Event occurs (e.g., new order)
|
Service sends POST request to your URL
|
Your server receives the data
|
You process the data
|
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 URLn8n 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 Webhookhttps://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):{
"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
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:GitHub Push Webhook
|
Branch = "main"?
| Yes
SSH: Run deploy script
|
Slack: Notify about deployment
Example 4: Typeform Response Webhook
Enable webhook:// 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.siten8n 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:
<h2 class="text-2xl font-bold mt-10 mb-6 text-gray-900">Order Created Webhook</h2>
<h3 class="text-xl font-bold mt-8 mb-4 text-gray-900">Trigger</h3>
Fired when a new order is created.
<h3 class="text-xl font-bold mt-8 mb-4 text-gray-900">HTTP Request</h3>
- Method: POST
- Content-Type: application/json
<h3 class="text-xl font-bold mt-8 mb-4 text-gray-900">Headers</h3>
Header Description X-Webhook-Signature HMAC-SHA256 signature X-Webhook-Timestamp Unix timestamp
<h3 class="text-xl font-bold mt-8 mb-4 text-gray-900">Payload</h3>
json
{
"event": "order.created",
"data": {
"orderId": "string",
"customerEmail": "string",
"total": "number"
}
}
<h3 class="text-xl font-bold mt-8 mb-4 text-gray-900">Response</h3>
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.