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.
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"
}
}Response200
application/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 yetPreview2 of 3 failed cards recovered in 28 days · 0 Customers lost to a failed card · ■□□□ Too early to tell · n 3
Metric.query({"metric": · sample · leaseline.app at 2026-10-19
What it does
| What it does | Price per unit | Moves | Who does it | Today |
|---|---|---|---|---|
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 here | Save rate, Lost Customers | Billing | 5 of 6 calls live |
Set it up
| Check (dry run on a 0-Customer Startup, gaps #8) | Fix | Then | Your time | Wait |
|---|---|---|---|---|
billing-connected (Not live yet, gaps #8) · fail | Connect Stripe so every charge and Meter reading is live. | POST /{namespace}/integrations/connect | 5 min | the first sync, about 10 min |
role-unstaffed (Not live yet, gaps #8) · warn: Billing has no person: every charge comes to you | Staff Billing, or leave it: every retry comes to you to approve. | POST /{namespace}/roles/{role}/staff | 0 min | none |
The records
{
"$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
| Call | Here | Emits | Status |
|---|---|---|---|
GET /{namespace}/invoices | 2 Customers past due; Alder & Co since Oct 17 | — | live |
POST /{namespace}/invoices/{invoice}/collect | the retry waits for you: Billing is capped at You approve each one | Invoice.collected | live |
POST /{namespace}/approvals/{approval}/approve | Approval.approved | live | |
POST /{namespace}/messages/{message}/schedule | the card-update note, with the link to their billing page | Message.scheduled | live |
POST /{namespace}/messages/{message}/schedule | an upgrade note from usage-upsell, the same day | Message.scheduled | live |
POST /{namespace}/metrics/query | Metric.queried | Not live yet, gaps #4 |
Events
| Event | When it fires | What hears it |
|---|---|---|
Invoice.collected | the retry clears | ends dunning; the Customer leaves past due |
Approval.approved | you approve the retry | runs POST /{namespace}/invoices/{invoice}/collect |
Task.offered | a Subscription turns past due | the CSM’s queue within 1 business day |
Refuses when
| Check | Fix | Then |
|---|---|---|
keep-first fail · Not live yet, gaps #8 | Let 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.
| Check | Refuses | Fix | Then |
|---|---|---|---|
keep-first (Not live yet, gaps #8) | an upgrade, referral, review or reference ask to a Customer who is at risk, past due or leaving | Let 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 records | Connect Stripe so every charge and Meter reading is live. | POST /{namespace}/integrations/connect |
Plays that use it
| Play | What it does | Use 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
Example
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.