Payments

Stripe subscription lifecycle router in n8n

Verifies the Stripe signature, then routes customer.subscription events, invoice.paid and invoice.payment_failed to one branch per subscription status.

Trigger
Stripe webhook, POST on the path /stripe-subscription-lifecycle
Nodes
15
Requires
No external credentials
Category
Payments
From the course
Stripe and Payment Automations
Published
2026-09-10

Node types used

  • Webhook
  • Code
  • Switch
  • No Operation
Workflow diagram: Stripe subscription lifecycle router in n8nsubscriptioninvoice.paidinvoice.payment_failedotherincompleteincomplete_expiredtrialingactivepast_dueunpaidcanceledpausedWebhookVerify Stripe signatu…Verify Stripe signatureRoute on event typeRoute on subscription…Route on subscription statusNo access yetNever paid, close it …Never paid, close it outGrant trial accessGrant / extend accessWarn, keep accessRevoke accessRevoke and offboardPaused, hold accessExtend access on rene…Extend access on renewalAt risk, email and al…At risk, email and alertLog only
TriggerActionLogic

What it does

Stripe sends an event for every subscription state change and most of them are not the one you care about. An active status can arrive from a new signup, a trial converting, a renewal or a recovered payment. past_due and unpaid look similar and mean opposite things about whether the customer should still be able to log in. Teams usually find this out after cutting off someone who paid, or leaving a cancelled account with full access.

The Webhook node takes the POST with Raw Body on and the same Code node used in the signature verification template checks the Stripe-Signature HMAC before anything else runs. A first Switch reads $json.type: anything starting with customer.subscription. goes to a second Switch, invoice.paid and invoice.payment_failed each get their own named output, and everything else falls through to a named "other" output. The second Switch reads $json.data.object.status and has one output for each of the eight Stripe subscription statuses: incomplete, incomplete_expired, trialing, active, past_due, unpaid, canceled and paused. Every output ends on a NoOp node named for the decision that belongs there, from granting trial access, through warning a past_due customer while keeping their access, to revoking and offboarding a cancelled one.

The NoOp nodes are deliberate placeholders. This export is the routing map, not the implementation: nothing grants access, sends an email or writes a row. Replace each NoOp with whatever your product does, and the branch names tell you which behaviour belongs where.

Node by node

  1. 1

    Webhook Accepts POST requests on the path stripe-subscription-lifecycle with Raw Body on, so the signed bytes survive as a binary property.

  2. 2

    Verify Stripe signature Recomputes the HMAC-SHA256 of `${timestamp}.${rawBody}` with $env.STRIPE_WEBHOOK_SECRET, compares it to every v1 signature in the stripe-signature header using crypto.timingSafeEqual, rejects a timestamp more than 300 seconds old, and returns the parsed event.

  3. 3

    Route on event type A Switch with three rules on $json.type plus a fallback: "subscription" matches anything starting with customer.subscription., then exact matches on invoice.paid and invoice.payment_failed, and the fallback output is renamed "other".

  4. 4

    Route on subscription status A second Switch with eight exact matches on $json.data.object.status, in the order incomplete, incomplete_expired, trialing, active, past_due, unpaid, canceled, paused. There is no fallback output, so an unknown status ends the run here.

  5. 5

    No access yet Placeholder for status incomplete: the subscription exists but the first payment has not gone through, so nothing should be unlocked.

  6. 6

    Never paid — close it out Placeholder for status incomplete_expired: the first payment never completed and Stripe has given up on it.

  7. 7

    Grant trial access Placeholder for status trialing: unlock the product, but the customer has not been charged yet.

  8. 8

    Grant / extend access Placeholder for status active: the normal paid state, reached from a new signup, a trial converting or a recovered payment.

  9. 9

    Warn — keep access Placeholder for status past_due: a renewal failed and Stripe is still retrying, so the usual answer is to email the customer and leave access on.

  10. 10

    Revoke access Placeholder for status unpaid: Stripe has exhausted its retries. This is the point where access normally stops.

  11. 11

    Revoke and offboard Placeholder for status canceled: end access and run whatever offboarding you owe the customer.

  12. 12

    Paused — hold access, ask for a card Placeholder for status paused: collection is paused rather than cancelled, so the decision is usually to hold access and ask for a payment method.

  13. 13

    Extend access on renewal Placeholder on the invoice.paid branch: a renewal succeeded, so push the paid-through date forward.

  14. 14

    At risk — email, alert, keep access Placeholder on the invoice.payment_failed branch: the charge failed but the subscription has not changed status yet.

  15. 15

    Log only — no lifecycle action Placeholder on the "other" fallback branch, for every subscribed event that is not a subscription or invoice event.

Setup after import

  1. 1

    Set STRIPE_WEBHOOK_SECRET in the n8n environment. The Code node reads it through $env, exactly as in the signature verification template.

  2. 2

    Copy the production webhook URL n8n shows for the path stripe-subscription-lifecycle and add it as an endpoint in the Stripe dashboard.

  3. 3

    Subscribe that endpoint to customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.paid and invoice.payment_failed. Any other event you add lands on the "other" output.

  4. 4

    Replace each NoOp node with the action your product needs. The node names are the specification: they say what belongs on each branch and nothing more.

  5. 5

    Decide what an unknown subscription status should do. The status Switch has no fallback output, so today it silently ends the run.

Limits

  • Every branch ends on a NoOp. Nothing is implemented.

  • No Postgres logging in this export. Pair it with the reconciliation template if you want the events on disk.

  • The status Switch has no fallback output, so a status Stripe adds later is dropped without a trace.

  • Routing is on the event alone. It does not fetch the subscription from Stripe to confirm the state is still current when a retried delivery arrives late.

Download the workflow

The 15 node export as n8n reads it. Credential ids, personal values and real endpoints are replaced with placeholders, so nothing here can reach an account that is not yours. No email, no account.

Download stripe-subscription-lifecycle-router.json

Get help with this in the community

If the import fails, if a node errors on the first run, or if you want to point this at a service it does not cover yet, post it in the free House of Loops community. Say which template it is and paste the error. Shannon Atkinson answers, and the answer stays there for the next person who hits the same thing.

Ask in the community