If you sell anything online, the most valuable signal you can capture isn’t a click — it’s a Stripe webhook. Every checkout.session.completed, every invoice.payment_failed, every customer.subscription.deleted is a moment where the next action determines whether you keep a customer or lose one. The problem is that Stripe doesn’t know about your CRM, your email tool, your Slack channel, or your finance spreadsheet. n8n is the cheapest, most flexible way to close that gap.
This guide walks through a production-ready n8n + Stripe webhook setup that you can deploy today: signing-secret verification, event routing, customer enrichment, idempotency, and three concrete downstream workflows (CRM sync, dunning, churn alert). By the end, you’ll have a single workflow that handles every Stripe event your business cares about.
Why route Stripe events through n8n instead of Zapier or custom code
Three reasons. First, cost: a busy SaaS can fire 5,000 Stripe events a month, which on Zapier means thousands of tasks consumed. n8n self-hosted costs you the same whether you process 500 or 500,000 events. Second, data control: webhook payloads contain PII (email, last-4 card, billing address). Routing them through your own n8n instance keeps that data inside infrastructure you control. Third, composability: a single n8n workflow can branch into ten downstream apps in parallel without paying ten “multi-step” upgrades.
The trade-off is setup time. You need to host n8n, configure a public URL, and write the verification logic once. After that, every new Stripe event is a five-minute branch.
The architecture in one paragraph
Stripe sends a POST request to your n8n webhook URL whenever an event fires. The first node verifies the Stripe-Signature header against your endpoint secret to prove the request really came from Stripe. The second node parses the event type and routes to the right branch via a Switch node. Each branch enriches the event (fetches the Customer object, maps to your internal user ID), runs the business logic (update CRM, send Slack alert, trigger email), and returns a 200 response so Stripe doesn’t retry. The whole thing fits in one workflow.
Step 1: Create the n8n webhook endpoint
Open your n8n canvas and drop a Webhook node. Set:
- HTTP Method: POST
- Path: something unguessable, e.g.
stripe-7f2a9c— Stripe will retry on 4xx/5xx, so you want path obscurity in addition to signature verification - Respond: “When last node finishes” so the workflow has time to process before responding
- Response Code: 200
Copy the production webhook URL n8n generates. It will look like https://your-n8n.com/webhook/stripe-7f2a9c.
Step 2: Register the endpoint in Stripe
In the Stripe Dashboard, go to Developers → Webhooks → Add endpoint. Paste the n8n URL. For “Events to send,” start with this minimum viable set:
checkout.session.completed— new customer or one-time purchasecustomer.subscription.created— new subscriptioncustomer.subscription.updated— plan change, status flip (active → past_due)customer.subscription.deleted— churninvoice.payment_succeeded— recurring payment clearedinvoice.payment_failed— dunning triggercharge.refunded— refund workflows
After saving, Stripe shows you the signing secret — a string starting with whsec_. Copy it. You’ll use it in the next step. Treat it like a password: anyone with this secret can forge events your workflow will treat as legitimate.
Step 3: Verify the Stripe signature
This is the step most tutorials skip and that costs people money. Stripe signs every webhook with HMAC-SHA256. If you don’t verify, anyone who finds your obscure path can POST fake events.
Add a Code node immediately after the Webhook node:
const crypto = require('crypto');
const sig = $input.first().headers['stripe-signature'];
const rawBody = $input.first().body;
const secret = $env.STRIPE_WEBHOOK_SECRET;
const [timestamp, v1] = sig.split(',').map(p => p.split('=')[1]);
const payload = `${timestamp}.${JSON.stringify(rawBody)}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (expected !== v1) {
throw new Error('Invalid Stripe signature');
}
// Reject events older than 5 minutes (replay protection)
const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (ageSeconds > 300) {
throw new Error('Event too old (replay attempt?)');
}
return { json: rawBody };
Store STRIPE_WEBHOOK_SECRET as an n8n environment variable, not in the node body. If verification fails, the workflow throws and Stripe sees the error response — it will retry, then eventually mark the endpoint as failing in your dashboard, which is a signal.
Important caveat: the n8n Webhook node normally parses the body as JSON before your Code node sees it. Signature verification needs the exact byte string Stripe sent. In production, set the Webhook node’s “Response” tab so the raw body is preserved, or use the “Binary Data” option. The simplest workaround: build the verification in a small Express middleware running in front of n8n that verifies first and forwards on success.
Step 4: Route by event type with a Switch node
After verification, add a Switch node and route on {{ $json.type }}. Create one output branch per Stripe event type you registered. Each branch is independent, which makes the workflow easy to extend — adding a new event type means adding one more case, not rewriting logic.
A typical n8nfuel setup looks like this:
checkout.session.completed→ enrich customer → create CRM contact → send welcome email → Slack #new-signupsinvoice.payment_failed→ enrich customer → send dunning email → tag CRM “at risk” → Slack #payments if > $500customer.subscription.deleted→ enrich customer → send exit survey → Slack #churn → archive in CRMcharge.refunded→ log to finance sheet → notify CSM
Step 5: Enrich every event with the Customer object
Webhook payloads contain a customer ID but not the email, name, or metadata. Add an HTTP Request node after the Switch that calls GET https://api.stripe.com/v1/customers/{{ $json.data.object.customer }} with your secret API key. The response gives you everything downstream nodes need.
Cache aggressively: if you process 50 events for the same customer in a day, you don’t want 50 round-trips to Stripe. n8n has a built-in Cache node or you can use Redis. Cache the Customer object for 5 minutes — Stripe webhook traffic for one customer is usually bursty (signup → trial → first invoice), so the cache hit rate is high.
Step 6: Idempotency — the silent killer
Stripe retries webhooks. If your endpoint returns 200 but a downstream system was slow and didn’t actually finish, you might process the same event twice. For most events this is fine. For “send welcome email” or “create CRM contact,” it’s not.
Solve it by storing the Stripe event.id in a small persistence layer (Redis, Postgres, or even a Google Sheet) before running side effects. At the start of each branch, look up the ID. If it’s already there, exit early. After the side effect succeeds, write the ID with a 7-day TTL (Stripe stops retrying after 3 days).
Step 7: Three concrete downstream workflows
CRM sync (HubSpot example)
On checkout.session.completed, n8n calls HubSpot’s POST /crm/v3/objects/contacts with email, name, plan, and MRR. Use HubSpot’s idempotencyKey to dedupe on their side too. Tag the contact with stripe_customer_id in a custom property so future syncs can do upserts instead of duplicates.
Dunning email sequence
On invoice.payment_failed, branch on {{ $json.data.object.attempt_count }}. Attempt 1 → soft email (“we couldn’t charge your card”). Attempt 2 → firmer email with update-payment-method link. Attempt 3 → CSM Slack alert. Attempt 4 → cancellation workflow. Stripe’s Smart Retries handle the actual re-charging; your job is the human-side communication.
Churn alert with context
On customer.subscription.deleted, calculate days-as-customer from created timestamp, compute LTV from total_paid, and post a Slack message to #churn with all three numbers plus a link to the customer in Stripe and your CRM. Make it one-click to open the exit-interview workflow.
Monitoring: how to know when the pipeline breaks
Add a final node at the end of every branch that writes a row to a Google Sheet or Airtable: timestamp, event ID, event type, customer ID, branch taken, success/failure. This is your audit log. Set a daily n8n cron that checks the sheet — if zero events were processed in the last 24 hours and you know you had paying customers, the webhook is silently failing and you need to look at the Stripe Dashboard’s webhook attempts view.
Stripe also keeps its own delivery log per endpoint. If something looks wrong, the dashboard tells you which events succeeded, which failed, and lets you replay any of them with one click. Combine that with your audit sheet and you have full visibility.
What this saves you in practice
A typical n8nfuel reader running this setup reports:
- 4–6 hours/week of manual CRM updates eliminated
- 15–25% recovery on previously-failed payments thanks to a tighter dunning loop
- Same-day churn response instead of week-late discovery
The whole workflow takes about three hours to build the first time and 15 minutes to clone for the next project.
Frequently asked questions
Do I need n8n Cloud or can I self-host?
Self-hosting is fine and recommended for Stripe webhooks specifically because of the PII concern. A $5/month VPS handles tens of thousands of events per month with headroom. n8n Cloud works too if you’d rather not manage infrastructure.
Will Stripe retry if my workflow takes 30 seconds?
Stripe’s webhook timeout is 30 seconds. If your workflow takes longer, return 200 immediately from the Webhook node and process the rest asynchronously via a queue. n8n’s Wait node plus a second workflow is the simplest pattern.
Can I use this for Stripe Connect platforms?
Yes. Connect events use the same signature scheme but include an additional account field. Add a branch in your Switch node on {{ $json.account }} to route per connected account.
What about test mode events?
Stripe sends test events to a separate webhook URL. Create a second n8n workflow for test mode and point your Stripe test webhook there. Don’t try to handle both in one workflow — you’ll send real CRM updates from test payments and that’s a bad afternoon.
How do I migrate an existing Zapier setup?
Run both in parallel for two weeks. Have n8n log events to a sheet without firing side effects. Compare the sheet to Zapier’s task history. When they match, flip n8n to active and delete the Zap. This avoids the “we missed a churn alert during the migration” disaster.
Next steps
If you’ve already got n8n running, the next two reads on n8nfuel are n8n Approval Workflows: Build Human-in-the-Loop Automations for the dunning-escalation pattern, and 15 Common n8n Mistakes Beginners Make to avoid the bugs that bite production webhook handlers. For the Spanish-language version of the broader n8n stack, see Crear Flujos de Aprobacion con n8n.
The Stripe webhook is the single highest-leverage automation in any SaaS business. Build it once, refine it over a month, and stop touching CRM, email, and Slack by hand for every payment event.