# How we recover failed subscription payments An hourly agent tracks every failed Stripe invoice on a per-subscription ledger, escalating from a smart retry through a payment reminder, an update-your-card notice, and a final notice, alerting the revenue team on Slack and stopping the moment the invoice is paid. Canonical page: https://kortix.com/use-cases/payment-recovery Most churn isn't a customer deciding to leave. It's a card that expired, a bank that flagged a charge, a payment that failed for a reason that has nothing to do with whether the customer still wants the product. Left alone, that failed invoice quietly turns into a canceled subscription. This is involuntary churn, and it's recoverable if someone catches it fast and follows up consistently. We run a payment-recovery agent on Kortix that watches every failed Stripe invoice, works it through a fixed escalation ladder — smart retry, reminder, update-your-card notice, final notice — and stops the instant the invoice is paid. It never cancels a subscription, issues a credit, or refunds anything; those decisions stay with a human. - **Team:** Kortix - **Runs on:** Hourly cron, reusable session - **Connected systems:** Stripe · Slack · Email - **Mode:** Smart-retry + dunning only · escalation, never cancellation ## The problem A failed payment isn't one event, it's the start of a countdown. Stripe will retry automatically for a while, but a generic retry schedule doesn't know that this customer just had a fraud hold, or that another one hasn't opened an email in a week. Handled with a single blunt reminder, some customers churn who would have paid if asked the right way at the right time; handled with silence, the subscription lapses and support finds out only when the customer complains that their access disappeared. The common approaches don't hold state well. Stripe's built-in retry logic is useful but generic — it doesn't loop in a human, escalate the tone as time passes, or tell the revenue team which accounts are about to fall off a cliff. A spreadsheet tracking who got which email decays within a week. And a blanket policy of "cancel after N failures" trades a recoverable customer for a clean queue. ## What we built On Kortix, an hourly cron re-prompts one **persistent session** that keeps a per-subscription ledger of where every failed invoice sits on the recovery ladder: smart retry, payment reminder, update-your-card notice, or final notice. Each run it reads Stripe for failed invoices and subscription state, advances any subscription whose wait time has elapsed to the next rung, sends the matching email, and posts a summary of everything in flight to the revenue team's Slack channel. Any subscription that pays at any point is closed out on the ledger and never contacted again. ## How it works ### Run hourly on a reusable session A **cron trigger** fires every hour, but unlike a fresh-session use case, it re-prompts the **same session** each time. The ledger — which subscription is on which rung, and when it's next due to escalate — lives in that session's memory, so the agent always knows what it already sent and never repeats or skips a step. ### Read failed invoices from Stripe Through a scoped **connector**, brokered server-side so no raw key reaches the model, the agent reads failed invoices, subscription status, and payment-method state from Stripe. This is the only signal it needs to decide what happens next; the read covers both new failures and subscriptions already mid-ladder. ### Give the agent the escalation ladder The order and timing of the ladder live as a **skill**: attempt a smart retry first, then a friendly payment reminder, then a firmer "update your card" notice with a link to update the payment method, then a final notice — each rung gated on a minimum wait since the last one, so no customer gets two emails in the same day. ### Send the dunning email and alert the revenue team The agent sends the rung-appropriate **email** to the customer directly — this part doesn't wait for approval, because a smart retry or a dunning email is reversible and expected. It also posts to **Slack**: what advanced today, what's now on final notice, and what recovered since the last run. ### Stop at the guardrail, not at cancellation Reaching the final notice does not trigger a cancellation, a refund, or a credit. The ladder tops out there and the account sits, flagged in the Slack summary, waiting for a person on the revenue team to decide the next step. Credentials for Stripe and email are encrypted in the Secrets Manager and injected at runtime, never shown to the model or written to logs. ### Close the loop the moment it's paid Every run checks whether a ladder subscription has since paid. The moment it has, the agent marks it recovered on the ledger, stops emailing it, and reports it in the Slack summary as a win — no further action, no lingering reminder. > **The pattern** > An hourly cron re-prompts one **reusable session** holding a per-subscription > ledger. A read-mostly Stripe **connector** supplies the state; a **skill** > encodes the escalation ladder; **email** and **Slack** are the only outputs. > The ladder stops at a final notice — cancellation, credits, and refunds > always wait for a human. ## Guardrails The agent can retry a payment and send an email on its own; anything that moves money or ends a subscription is out of its hands: - **Smart-retry and dunning only.** The agent may retry a failed charge and send the four ladder emails. It never cancels a subscription, issues a credit, or processes a refund — those require a human, no exceptions. - **One rung at a time.** The skill enforces a minimum wait between rungs so no customer is double-messaged, and every subscription escalates on its own schedule rather than everyone moving in lockstep. - **Stops on payment.** The moment an invoice clears, the ledger closes it out and no further email goes out. - **Scoped secrets.** The Stripe credential is encrypted in the Secrets Manager and injected into the sandbox at runtime, never exposed to the model or the logs. - **Everything is code.** The ladder, the wait times, and the agent's grants are files in the repo, versioned and changed through a reviewed **change request** rather than a dashboard setting. ## The outcome - **Hourly:** Every failed invoice re-checked against the ladder - **4 rungs:** Smart retry → reminder → update-card → final notice - **0:** Cancellations, credits, or refunds issued by the agent Failed payments that used to sit unattended until a subscription quietly lapsed now work through a consistent, escalating recovery ladder the moment they happen, with the revenue team seeing exactly what's in flight and what needs their decision. The agent chases the payment; the people decide when to give up on it.