
I've been automating workflows for years, and I can tell you that webhooks are the difference between clunky, scheduled integrations and truly responsive automation. Let me explain what's actually happening under the hood.
A webhook is an HTTP callback—essentially a way for one application to send real-time data to another application the moment something happens. Instead of your workflow constantly asking "did anything change yet?" (polling), a webhook is the source application saying "something just happened, here's the data."
Think of it like this: polling is checking your mailbox every hour. A webhook is the postman knocking on your door the moment mail arrives.
Why does this matter for automation? Speed. Reliability. Reduced API costs. If you're using n8n to replace expensive SaaS tools, webhooks cut your infrastructure costs dramatically because you're not hammering APIs with constant requests.
The webhook flow is straightforward but powerful:
The magic is that this all happens in milliseconds. No scheduled checks. No delays. Just instant reaction.
Here's what a typical webhook payload looks like:
{
"event": "payment.completed",
"timestamp": "2024-01-15T14:32:00Z",
"data": {
"payment_id": "pay_1234567890",
"amount": 9999,
"currency": "USD",
"customer_id": "cus_9876543210",
"status": "succeeded",
"metadata": {
"order_id": "ord_555",
"customer_email": "user@example.com"
}
}
}
The source app doesn't care if your webhook succeeds or fails immediately—it just sends the data. That's why you need to build error handling into your workflow.
I'm going to walk you through creating a production-ready webhook that captures data and processes it. I'll use n8n Cloud for this example, but the principles are identical for self-hosted.
In n8n, add a new workflow and place a Webhook trigger node. Here's the configuration:
POST
200
The URL will look like:
https://your-instance.n8n.cloud/webhook/unique-id-here
If you're self-hosting on Hetzner or Contabo, your URL might be:
https://automation.yourdomain.com/webhook/stripe-payments
Add a Set node to structure the webhook payload. Here's a real example that extracts Stripe payment data:
{
"paymentId": "{{$json.data.payment_id}}",
"amount": "{{$json.data.amount}}",
"currency": "{{$json.data.currency}}",
"customerId": "{{$json.data.customer_id}}",
"status": "{{$json.data.status}}",
"customerEmail": "{{$json.data.metadata.customer_email}}",
"orderReference": "{{$json.data.metadata.order_id}}",
"processedAt": "{{$now.toIso()}}"
}
This cleans up the raw webhook data into a standardized format your workflow can reliably work with.
Add a Switch node to handle different webhook events:
Switch Node Expression:
$json.event
Conditions:
- payment.completed → Send to spreadsheet
- payment.failed → Send alert email
- charge.refunded → Log to database
- Default → Log to error handler
💡 Fast-Track Your Project: Don't want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.
If you're storing payment data, you might send it to Google Sheets using n8n as a free database. Add a Google Sheets node:
Spreadsheet: "Payments"
Sheet: "All Transactions"
Options:
- Columns: paymentId, amount, currency, customerId, customerEmail, orderReference, status, processedAt
- Include Column Headers: true
Or if you need a proper database, use a Postgres node:
INSERT INTO payments (
payment_id,
amount,
currency,
customer_id,
customer_email,
order_reference,
status,
processed_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
Map your n8n variables to these placeholders.
Here's where most people mess up: they treat webhooks like a public API endpoint. Don't do that. Anyone can send data to your webhook URL if they know it exists.
Most services (Stripe, GitHub, Slack) sign their webhooks with an HMAC signature. Here's how Stripe does it:
Stripe-Signature headerIn n8n, add a Code node to verify this before processing:
const crypto = require('crypto');
const signature = $request.headers['stripe-signature'];
const secret = 'whsec_your_webhook_secret';
const timestamp = signature.split(',')[0].split('=')[1];
const signedContent = `${timestamp}.${$json.body}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
const receivedSignature = signature.split(',')[1].split('=')[1];
if (expectedSignature === receivedSignature) {
return [{ valid: true }];
} else {
return [{ valid: false, error: 'Invalid signature' }];
}
Add a Switch node after this:
valid === true → continue processingvalid === false → return 401 Unauthorized and stopIn your webhook configuration, always set a secret. When you expose your webhook URL publicly, append a token:
https://automation.yourdomain.com/webhook/stripe-payments?secret=sk_live_abc123xyz789
Then in n8n's Webhook node, under Authentication, select your authentication method and validate this token before any processing happens.
When someone submits a form on your website, immediately store data and send a confirmation email:
This is where webhooks shine. If you're automating social media posts with n8n, you could:
Webhooks can fail. Your server might be down. Add robustness:
const maxRetries = 3;
let attempts = 0;
let success = false;
while (attempts < maxRetries && !success) {
try {
attempts++;
const response = await fetch('https://your-api.com/endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify($json)
});
if (response.ok) {
success = true;
}
} catch (error) {
if (attempts === maxRetries) {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
}
}
return [{ success }];
If you receive thousands of webhooks per minute, batch them before processing:
This reduces database load dramatically.
Problem: Webhook not being called
curl -X POST https://your-webhook-url \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
Problem: Webhooks calling but n8n says "invalid"
Problem: Webhook times out
Your n8n workflow is taking too long. The source app will retry if you don't respond within a few seconds. Solution:
Problem: Duplicate webhook executions
Add a deduplication check using the webhook's unique ID:
const webhookId = $json.id;
const cached = $request.headers['x-cached'] === 'true';
if (cached) {
return [{ duplicate: true, skipped: true }];
}
// Continue processing
Ready to implement webhooks in your automation? Here's your next step:
Originally published on Automation Insider.