Skip to content

Failed payments: recover the card

Failed payments: retry the card, send one card-update note, and keep every other ask away meanwhile.

A failed card is retried and the Customer gets one note to update it, before the Subscription lapses.

Recover the cardMode try
You, to your agent

Alder & Co’s card failed. Recover it before the Subscription lapses. Don’t send.

curl "https://api.sb/leaseline.app/invoices?where=status%20%3D%20%22open%22%20and%20attempts%20%3E%200&sort=due" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

curl -X POST "https://api.sb/leaseline.app/invoices/invoice_88/collect?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

curl -X POST "https://api.sb/leaseline.app/approvals/approval_81/approve?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

curl -X POST "https://api.sb/leaseline.app/messages/message_91/schedule?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"at":"2026-10-20T14:00:00Z"}'

curl -X POST "https://api.sb/leaseline.app/messages/message_92/schedule?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"at":"2026-10-20T15:00:00Z"}'

# Not live yet (gaps #4)
curl -X POST https://api.sb/leaseline.app/metrics/query \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metric":"churned-arr","shape":"trend","window":"28d"}'
import { $ } from 'startup-builder'

const { invoices } = await $.Invoice.list({ where: 'status = "open" and attempts > 0', sort: 'due' })   // 2 Customers past due; Alder & Co since Oct 17
await $.Invoice.collect({ invoice: 'invoice_88' }, { mode: 'try' })   // the retry waits for you: Billing is capped at You approve each one
await $.Approval.approve({ approval: 'approval_81' }, { mode: 'try' })
await $.Message.schedule({ message: 'message_91', at: '2026-10-20T14:00:00Z' }, { mode: 'try' })   // the card-update note, with the link to their billing page
await $.Message.schedule({ message: 'message_92', at: '2026-10-20T15:00:00Z' }, { mode: 'try' })   // an upgrade note from usage-upsell, the same day
// 422 · The upgrade note to Alder & Co did not send: their card failed on Oct 17.
// keep-first: Let their CSM reach them first, then ask again once they are healthy. · then Task.next
return await $.Metric.query({ metric: 'churned-arr', shape: 'trend', window: '28d' })   // Not live yet, gaps #4
npx startup-builder do Invoice.list --json '{"where":"status = \"open\" and attempts > 0","sort":"due"}'
npx startup-builder do Invoice.collect --json '{"invoice":"invoice_88"}' --try
npx startup-builder do Approval.approve --json '{"approval":"approval_81"}' --try
npx startup-builder do Message.schedule --json '{"message":"message_91","at":"2026-10-20T14:00:00Z"}' --try
npx startup-builder do Message.schedule --json '{"message":"message_92","at":"2026-10-20T15:00:00Z"}' --try
npx startup-builder do Metric.query --json '{"metric":"churned-arr","shape":"trend","window":"28d"}'   # Not live yet (gaps #4)
{
  "name": "do",
  "arguments": {
    "code": "const { invoices } = await $.Invoice.list({ where: 'status = \"open\" and attempts > 0', sort: 'due' })   // 2 Customers past due; Alder & Co since Oct 17\nawait $.Invoice.collect({ invoice: 'invoice_88' }, { mode: 'try' })   // the retry waits for you: Billing is capped at You approve each one\nawait $.Approval.approve({ approval: 'approval_81' }, { mode: 'try' })\nawait $.Message.schedule({ message: 'message_91', at: '2026-10-20T14:00:00Z' }, { mode: 'try' })   // the card-update note, with the link to their billing page\nawait $.Message.schedule({ message: 'message_92', at: '2026-10-20T15:00:00Z' }, { mode: 'try' })   // an upgrade note from usage-upsell, the same day\n// 422 · The upgrade note to Alder & Co did not send: their card failed on Oct 17.\n// keep-first: Let their CSM reach them first, then ask again once they are healthy. · then Task.next\nreturn await $.Metric.query({ metric: 'churned-arr', shape: 'trend', window: '28d' })   // Not live yet, gaps #4",
    "mode": "try"
  }
}
Response200application/json
{
  "metric": {
    "$id": "https://api.sb/leaseline.app/metrics/churned-arr",
    "$type": "Metric",
    "name": "Churned ARR"
  },
  "rows": [
    {
      "window": "28d",
      "value": -1388,
      "customers": 4,
      "failedCard": 0
    }
  ]
}
sample · 1 Not live yetPreview

2 of 3 failed cards recovered in 28 days · 0 Customers lost to a failed card · ■□□□ Too early to tell · n 3

Metric.query({"metric":"churned-arr","shape":"trend","window":"28d"}) · sample · leaseline.app at 2026-10-19

What it does

What it doesPrice per unitMovesWho does itToday
The dunning Workflow retries an open Invoice with POST /{namespace}/invoices/{invoice}/collect and sends one card-update note; Billing is capped at You approve each one.$0.04 per note (Worker cost); a retry costs nothing hereSave rate, Lost CustomersBilling5 of 6 calls live

Set it up

Check (dry run on a 0-Customer Startup, gaps #8)FixThenYour timeWait
billing-connected (Not live yet, gaps #8) · failConnect Stripe so every charge and Meter reading is live.POST /{namespace}/integrations/connect5 minthe first sync, about 10 min
role-unstaffed (Not live yet, gaps #8) · warn: Billing has no person: every charge comes to youStaff Billing, or leave it: every retry comes to you to approve.POST /{namespace}/roles/{role}/staff0 minnone

The records

Alder & Co’s open Invoice
{
  "$id": "https://api.sb/leaseline.app/invoices/invoice_88",
  "$type": "Invoice",
  "number": "LL-0088",
  "total": 29,
  "due": "2026-10-17",
  "status": "open",
  "collection": "charge"
}

Calls

CallHereEmitsStatus
GET /{namespace}/invoices2 Customers past due; Alder & Co since Oct 17—live
POST /{namespace}/invoices/{invoice}/collectthe retry waits for you: Billing is capped at You approve each oneInvoice.collectedlive
POST /{namespace}/approvals/{approval}/approveApproval.approvedlive
POST /{namespace}/messages/{message}/schedulethe card-update note, with the link to their billing pageMessage.scheduledlive
POST /{namespace}/messages/{message}/schedulean upgrade note from usage-upsell, the same dayMessage.scheduledlive
POST /{namespace}/metrics/queryMetric.queriedNot live yet, gaps #4

Events

EventWhen it firesWhat hears it
Invoice.collectedthe retry clearsends dunning; the Customer leaves past due
Approval.approvedyou approve the retryruns POST /{namespace}/invoices/{invoice}/collect
Task.offereda Subscription turns past duethe CSM’s queue within 1 business day

Refuses when

422RefusedThe upgrade note to Alder & Co did not send: their card failed on Oct 17.
CheckFixThen
keep-first fail · Not live yet, gaps #8Let their CSM reach them first, then ask again once they are healthy.POST /{namespace}/tasks/next
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The upgrade note to Alder & Co did not send: their card failed on Oct 17.",
  "checks": [
    {
      "check": "keep-first",
      "status": "fail",
      "doc": "/code/refusals#check-keep-first",
      "fix": "Let their CSM reach them first, then ask again once they are healthy.",
      "then": "Task.next"
    }
  ]
}

It says no, says why, and names the fix.

CheckRefusesFixThen
keep-first (Not live yet, gaps #8)an upgrade, referral, review or reference ask to a Customer who is at risk, past due or leavingLet their CSM reach them first, then ask again once they are healthy.POST /{namespace}/tasks/next
billing-connected (Not live yet, gaps #8)a Customer-side Play with no live billing recordsConnect Stripe so every charge and Meter reading is live.POST /{namespace}/integrations/connect

Plays that use it

PlayWhat it doesUse it when
failed-payment-recovery (Not live yet, gaps #4)The Play that recovers a failed card before the Subscription lapses.today: The hub Workflow dunning: POST /{namespace}/invoices/{invoice}/collect retries, then one card-update note.

Hub guides it deepens

On startupbuilder.devId
Dunningworkflow:dunning
Billingprocess:billing

Example

Leaseline, as the fixture ran it
Alder & Co · card failed Oct 17 · retry asked Oct 18, waiting on your OK · card-update note Oct 20 · an upgrade note the same day refused (keep-first) · 2 of 3 failed cards recovered in 28 days · sample

Compiled, not written: edit its page, then regenerate. Every step runs from a sentence or a line of code. In your words: the CRM words you know, and where each lives here.

Contents