Stripe payment and churn alerts in Slack with n8n

Post failed payments, cancellations and disputes from Stripe to a Slack channel with n8n's Stripe Trigger, with signature checks and duplicate filtering.

· ranx.cloud

Illustration of coins rolling down a wooden chute and ringing a brass desk bell

This is for a founder running a small subscription business who wants to hear about money problems the minute they happen, not when the monthly report comes in. At the end you have one n8n workflow that posts to a Slack channel when an invoice payment fails, a subscription is cancelled, or a customer opens a dispute, and a short line when an invoice is paid. No LLM is involved, so it costs nothing beyond the instance.

The workflow is five nodes:

Stripe Trigger → Remove Duplicates → Switch (by event type) → Slack (one per branch)

What you need

  • A Stripe account. Do the whole thing in a sandbox (test mode) first.
  • A Slack workspace where you can install an app.
  • An n8n instance reachable over HTTPS. Stripe only delivers webhooks to public HTTPS URLs.

1. Create the Stripe credential

In the Stripe Dashboard, go to API keys. For a sandbox, reveal and copy the test secret key (sk_test_...). For live mode, create a new secret key and name it something like n8n alerts so you can revoke it later without breaking anything else.

In n8n, open Credentials, create a Stripe API credential and paste the key into Secret Key. Leave Signature Secret empty for now; you fill it in after the first publish (step 6).

Don't paste the publishable key (pk_...). n8n's docs also note they haven't tested restricted keys (rk_...), so use a standard secret key unless you're prepared to debug permissions. The n8n Stripe credential page covers this.

Keep test and live keys in two separate credentials. Stripe objects in one mode are invisible in the other, and mixing them up is the most common reason the trigger "does nothing".

2. Add the Stripe Trigger

Create a new workflow and add Stripe Trigger. Pick the credential and select these Events:

invoice.payment_failed
invoice.paid
customer.subscription.deleted
charge.dispute.created

Don't pick *. Stripe's own guidance is to subscribe only to the events you handle, and on a busy account * sends you charge.updated, customer.updated and similar noise all day.

Set API Version to the version your account uses (it's shown in Workbench). If you leave it empty, Stripe uses your account's default at the time the endpoint is created, and a later account upgrade can change the payload shape under you.

You don't create the webhook endpoint in Stripe yourself. When the workflow is published, n8n calls Stripe's API and creates an endpoint pointing at its production webhook URL, with the description Created by n8n for workflow ID: <id>. When you unpublish, it deletes it.

3. Drop duplicate deliveries

Stripe says plainly that an endpoint can receive the same event more than once, and that you should track event IDs. Add a Remove Duplicates node after the trigger:

Operation:           Remove Items Processed in Previous Executions
Keep Items Where:    Value Is New
Value to Dedupe On:  {{ $json.id }}

$json.id is the Stripe event ID (evt_...). The node remembers the last 10,000 values by default (History Size), which is plenty for this volume.

4. Branch on the event type

Add a Switch node in Rules mode with four rules, each comparing {{ $json.type }} (string, is equal to) against one of the four event names. That gives you four outputs.

5. Write the Slack messages

Create the Slack app

  1. Go to api.slack.com/apps → Create New App → From scratch, and pick your workspace.
  2. Under OAuth & Permissions → Bot Token Scopes, add chat:write.
  3. Click Install to Workspace and copy the Bot User OAuth Token (xoxb-...).
  4. In Slack, invite the bot to the channel: /invite @your-app-name in #billing.

In n8n, create a Slack API credential and paste the token into Access Token.

One Slack node per branch

Add a Slack node to each Switch output: Resource Message, Operation Send, Send Message To Channel, pick #billing, Message Type Simple Text Message. Under Options, turn off Include Link to Workflow unless you want the n8n link appended to every message.

Stripe amounts are integers in the smallest currency unit, so divide by 100 for USD/EUR (not for zero-decimal currencies like JPY).

Failed payment:

:warning: Payment failed: {{ $json.data.object.customer_email }}
{{ ($json.data.object.amount_due / 100).toFixed(2) }} {{ $json.data.object.currency.toUpperCase() }}, attempt {{ $json.data.object.attempt_count }}
Next retry: {{ $json.data.object.next_payment_attempt ? new Date($json.data.object.next_payment_attempt * 1000).toISOString().slice(0, 16) + ' UTC' : 'none, Stripe has stopped retrying' }}
Invoice: {{ $json.data.object.hosted_invoice_url }}

Cancellation:

:wave: Subscription cancelled: customer {{ $json.data.object.customer }}
Reason: {{ $json.data.object.cancellation_details?.reason ?? 'n/a' }}
Feedback: {{ $json.data.object.cancellation_details?.feedback ?? 'none given' }}

Dispute:

:rotating_light: Dispute opened: {{ ($json.data.object.amount / 100).toFixed(2) }} {{ $json.data.object.currency.toUpperCase() }}
Reason: {{ $json.data.object.reason }}
Respond by: {{ new Date($json.data.object.evidence_details.due_by * 1000).toISOString().slice(0, 10) }}

Paid invoice (keep it short, or skip this branch entirely if the channel gets too chatty):

:moneybag: {{ ($json.data.object.amount_paid / 100).toFixed(2) }} {{ $json.data.object.currency.toUpperCase() }} from {{ $json.data.object.customer_email }}

The cancellation payload carries a customer ID, not an email. If you want the email, put a Stripe node (Resource Customer, Operation Get, Customer ID {{ $json.data.object.customer }}) in front of that Slack node and reference its output.

6. Test, publish, then add the signature secret

  1. Open the Stripe Trigger and click Listen for test event. n8n registers a temporary endpoint on its /webhook-test/ URL.
  2. In your Stripe sandbox, cause an event: for example, create a subscription for a test customer with the card 4000 0000 0000 0341, which attaches fine but declines on charge, so you get invoice.payment_failed.
  3. Check the output, adjust the Slack text, and run the rest of the workflow with Execute workflow.
  4. Click Publish. n8n now creates the production endpoint in Stripe at https://<your-host>/webhook/<id>/webhook.
  5. In Stripe, open Workbench → Webhooks, find the endpoint with the Created by n8n description, reveal the Signing secret (whsec_...) and paste it into the Signature Secret field of your n8n Stripe credential.

With a signature secret set, the trigger checks the Stripe-Signature header and returns 401 for anything unsigned, forged, or older than five minutes, without running the workflow. Without it, anyone who learns the URL can post fake events into your Slack channel. This check exists from n8n 2.25.7 onward, per the Stripe Trigger docs.

Repeat steps 1 to 5 with the live credential when the sandbox version looks right. Live and sandbox endpoints have different signing secrets.

On ranx.cloud the instance already has HTTPS and a stable hostname, so the endpoint n8n registers is https://<name>.ranx.cloud/webhook/... and you don't need a tunnel or a reverse proxy.

Costs and limits

  • n8n and this workflow: no per-event cost.
  • Slack: chat.postMessage is rate limited to roughly one message per second per channel, with short bursts allowed (Slack rate limits). A subscription business would need a very bad day to hit that. If you ever do, Slack returns 429 and the Slack node fails; turn on Retry On Fail in the node's settings.
  • Stripe: an account can have at most 16 webhook endpoints (event destinations). Every published workflow with a Stripe Trigger uses one, and so does each test listen while it's open.

What goes wrong

Nothing arrives after publishing. Check, in this order: the credential mode matches where you're creating events (sandbox key, sandbox events); the endpoint exists in Workbench and is enabled; its Event deliveries tab. A 401 there means the Signature Secret in n8n doesn't match that endpoint, typically because you pasted the sandbox secret into the live credential or the endpoint was re-created after an unpublish and publish.

The endpoint keeps getting re-created. n8n stores the endpoint ID in the workflow and deletes the endpoint when you unpublish. If you delete the endpoint by hand in Stripe, n8n notices on the next publish and creates a new one, with a new signing secret. Update the credential each time.

Every event posts twice. Usually two published workflows subscribed to the same event, often a copy you duplicated for testing. Each has its own endpoint. Search Workbench for Created by n8n endpoints and compare workflow IDs. Genuine Stripe redeliveries are handled by the Remove Duplicates node.

Expressions return undefined. The payload follows the endpoint's API version. If you left API Version empty and later upgraded the account, some fields may have moved. Pin the version in the node and re-publish.

Your instance was down for a while. Stripe retries failed deliveries in live mode for up to three days with exponential backoff (sandbox: three tries over a few hours), per Stripe's retry policy. A restart of a few minutes means the events arrive late, not that they're lost. That is also why the dedupe step matters: a delivery that reached n8n just before a restart may be retried. If a run was cut off mid-way by the restart, n8n marks that execution as crashed when it starts again; check Executions after any outage.

Variations

  • Revenue log. Add a Google Sheets node (Operation Append Row) on the invoice.paid branch with columns for date, customer email, amount and invoice ID. You need a Google OAuth credential for this, which takes ten minutes in Google Cloud Console; the support-email triage post walks through it.
  • Escalate disputes. Send the dispute branch to a direct message instead of a channel (Send Message To: User), since disputes have a hard deadline.
  • Error alerts. Set an error workflow on this one so you hear about it when the alerting itself breaks. The backups and error alerts post shows how.