Set Up Stripe Webhooks: Payment Automation with n8n and Make.com
How to set up Stripe webhooks for automatic payment processing.
Stripe webhooks are the key to fully automated payment processes. Instead of manually checking whether a payment came through, your system reacts automatically to every event: payment successful, subscription cancelled, chargeback - all in real-time. In this guide, we show you the complete setup.
What Are Stripe Webhooks?
Webhooks are HTTP callbacks: Stripe sends a message to your URL as soon as something happens.
Without webhooks:Customer pays -> You check manually -> Hours later: Access granted
With webhooks:
Customer pays -> Stripe sends event -> Immediately: Access granted
Important Stripe Events
| Event | When Triggered | Typical Action |
|---|---|---|
checkout.session.completed | Checkout completed | Grant access |
payment_intent.succeeded | Payment successful | Send invoice |
payment_intent.failed | Payment failed | Notify customer |
invoice.paid | Invoice paid | Extend subscription |
invoice.payment_failed | Subscription payment failed | Send reminder |
customer.subscription.deleted | Subscription cancelled | Revoke access |
customer.subscription.updated | Subscription changed | Adjust plan |
charge.refunded | Refund issued | Revoke access |
charge.dispute.created | Chargeback | Alert team |
Step 1: Create Webhook Endpoint
Option A: n8n Webhook
- HTTP Method: POST
- Path: stripe-webhook
https://your-n8n-instance.com/webhook/stripe-webhookOption B: Make.com Webhook
Step 2: Register Webhook with Stripe
In Stripe Dashboard
checkout.session.completed)Copy Webhook Secret
After creation, Stripe shows a Signing Secret:
whsec_1234567890abcdefghijklmnop
Important: Store this key securely - for signature verification!
Via Stripe CLI (for Development)
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to localhost
stripe listen --forward-to localhost:5678/webhook/stripe-webhook
# Shows temporary signing secret
Step 3: Verify Signature (Security!)
Why important? Without verification, anyone could send fake events to your endpoint.n8n: Check Signature
// Node: Code (before all other nodes)
const crypto = require('crypto');
const payload = $json.body; // Raw body
const signature = $headers['stripe-signature'];
const webhookSecret = 'whsec_1234567890abcdefghijklmnop';
// Stripe signature format: t=timestamp,v1=signature
const elements = signature.split(',');
const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
const expectedSig = elements.find(e => e.startsWith('v1=')).split('=')[1];
// Calculate signature
const signedPayload = ${timestamp}.${JSON.stringify(payload)};
const computedSig = crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
if (computedSig !== expectedSig) {
throw new Error('Invalid signature');
}
// Check timestamp (max 5 minutes old)
const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp) > 300) {
throw new Error('Timestamp too old');
}
return $json;
Make.com: HTTP Module with Verification
In Make.com, you can add an HTTP module that verifies the signature.
Step 4: Process Events
Workflow: Checkout Completed
Stripe Webhook
(checkout.session.completed)
|
Verify signature
|
Extract customer data
|
Parallel:
+- Database: Create/activate user
+- Email: Send welcome email
+- CRM: Mark deal as won
+- Slack: Notify team
n8n Implementation
Node 1: Webhook Trigger// Webhook receives event
{
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_1234",
"customer": "cus_1234",
"customer_email": "customer@example.com",
"amount_total": 9900,
"currency": "eur",
"metadata": {
"plan": "pro",
"user_id": "123"
}
}
}
}
Node 2: Switch by Event Type
// Node: Switch
const eventType = $json.type;
switch(eventType) {
case 'checkout.session.completed':
return { route: 'checkout_completed' };
case 'invoice.paid':
return { route: 'invoice_paid' };
case 'customer.subscription.deleted':
return { route: 'subscription_cancelled' };
default:
return { route: 'unknown' };
}
Node 3: Extract Data
// Node: Set
{
"customerId": "{{ $json.data.object.customer }}",
"email": "{{ $json.data.object.customer_email }}",
"amount": "{{ $json.data.object.amount_total / 100 }}",
"plan": "{{ $json.data.object.metadata.plan }}",
"userId": "{{ $json.data.object.metadata.user_id }}"
}
Node 4: Activate User (Your Database)
// Node: HTTP Request (to your API)
{
"method": "POST",
"url": "https://api.your-app.com/users/activate",
"body": {
"userId": "{{ $json.userId }}",
"plan": "{{ $json.plan }}",
"stripeCustomerId": "{{ $json.customerId }}"
}
}
Workflow: Subscription Payment Failed
Stripe Webhook
(invoice.payment_failed)
|
Check attempt number
|
Switch:
+- Attempt 1: Friendly email
+- Attempt 2: Urgent email
+- Attempt 3: Final warning
+- Attempt 4: Suspend access
Implementation
// Node: Code
const invoice = $json.data.object;
const attemptCount = invoice.attempt_count;
let action, emailTemplate;
switch(attemptCount) {
case 1:
action = 'soft_reminder';
emailTemplate = 'payment_failed_soft';
break;
case 2:
action = 'urgent_reminder';
emailTemplate = 'payment_failed_urgent';
break;
case 3:
action = 'final_warning';
emailTemplate = 'payment_failed_final';
break;
default:
action = 'suspend_account';
emailTemplate = 'account_suspended';
}
return { action, emailTemplate, attemptCount };
Workflow: Refund
Stripe Webhook
(charge.refunded)
|
Full or partial?
|
Full:
+- Revoke access
+- Create internal note
+- Notify team
|
Partial:
+- Just log
Workflow: Chargeback (Dispute)
Stripe Webhook
(charge.dispute.created)
|
IMMEDIATE alert to team!
|
Slack: @channel Alert
|
Zendesk: High-priority ticket
|
Gather all customer data
(for Stripe dispute response)
Important: Chargebacks cost $15 in fees and can lead to account suspension!
Make.com Scenario: Stripe Checkout
Module Setup
Filter Example
// Only for certain products
Condition: {{data.object.metadata.product_type}} = "subscription"
Advanced Patterns
Ensure Idempotency
Stripe can send events multiple times. Prevent duplicate processing:
// Node: Code
const eventId = $json.id; // e.g., "evt_1234"
// Check if already processed
const existing = await getFromDatabase('stripe_events', eventId);
if (existing) {
return { skip: true, reason: 'already_processed' };
}
// Mark as processed
await saveToDatabase('stripe_events', {
id: eventId,
type: $json.type,
processedAt: new Date().toISOString()
});
return { skip: false };
Retry Logic
If your endpoint fails, Stripe retries:
| Attempt | Wait Time |
|---|---|
| 1 | Immediately |
| 2 | 1 hour |
| 3 | 6 hours |
| 4 | 24 hours |
| 5 | 48 hours |
Webhook Queue for Reliability
Stripe Webhook
|
Save to queue (Redis/SQS)
|
Immediately respond 200 OK
|
[Asynchronously]
Worker processes queue
Testing
Stripe Test Events
In Stripe Dashboard:
Stripe CLI
# Send test event
stripe trigger checkout.session.completed
# With custom data
stripe trigger payment_intent.succeeded \
--add payment_intent:metadata.user_id=123
n8n Test Mode
Debugging
Event Logs in Stripe
Dashboard -> Developers -> Webhooks -> Events
Shows:
- All sent events
- HTTP response from your server
- Retry attempts
Common Errors
| Error | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Wrong API key | Check key |
| 400 Bad Request | Invalid data | Check payload |
| 500 Internal Error | Your code has bugs | Check logs |
| Timeout | Processing too slow | Process async |
| Signature mismatch | Wrong signing secret | Check secret |
Production Checklist
Costs
| Component | Cost |
|---|---|
| Stripe Webhooks | Free |
| n8n (1000 executions) | ~20 EUR/month |
| Make.com (10,000 ops) | ~30 EUR/month |
Conclusion
Stripe webhooks are essential for any SaaS/e-commerce business:
- Real-time response to payments
- Fully automatic subscription management
- Proactive churn management
- Compliance (invoices, etc.)
Next Steps
We support you with Stripe integration - from setup to complex subscription logic.