When someone cancels: the cancel, then one save offer
When someone cancels: the cancel runs at once, one save offer follows, and no agent can hold it.
A cancel runs the moment the Customer asks. One save offer follows while access lasts, and no agent can hold the cancel.
You, to your agent
Tidewater Homes clicked cancel. Let it stand, then make one save offer. Don’t send.
curl -X POST "https://api.sb/leaseline.app/subscriptions/subscription_15/unsubscribe?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason":"too-expensive","comment":"We only manage 12 units now."}'
curl -X POST "https://api.sb/leaseline.app/subscriptions/subscription_15/keep?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"offer":"offer_5"}'
curl -X POST "https://api.sb/leaseline.app/actions/action_702/redirect?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"changes":{"effectiveAt":"2026-12-14"}}'
curl -X POST https://api.sb/leaseline.app/metrics/query \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"metric":"save-rate","shape":"trend","window":"28d"}'import { $ } from 'startup-builder'
await $.Subscription.unsubscribe({ subscription: 'subscription_15', reason: 'too-expensive', comment: 'We only manage 12 units now.' }, { mode: 'try' }) // 200: it ends Nov 14, at the close of the period, and access lasts until then
await $.Subscription.keep({ subscription: 'subscription_15', offer: 'offer_5' }, { mode: 'try' }) // the one save offer: the $19 Starter plan for 12 units, theirs to press
await $.Action.redirect({ action: 'action_702', changes: { effectiveAt: '2026-12-14' } }, { mode: 'try' }) // an agent tries to hold the cancel a month
// 422 · The cancel for Tidewater Homes was not held: a cancel runs on its date.
// exit-never-waits: Leave the cancel standing; the Customer takes it back with one press while access lasts. · then Subscription.keep
return await $.Metric.query({ metric: 'save-rate', shape: 'trend', window: '28d' })
npx startup-builder do Subscription.unsubscribe --json '{"subscription":"subscription_15","reason":"too-expensive","comment":"We only manage 12 units now."}' --try
npx startup-builder do Subscription.keep --json '{"subscription":"subscription_15","offer":"offer_5"}' --try
npx startup-builder do Action.redirect --json '{"action":"action_702","changes":{"effectiveAt":"2026-12-14"}}' --try
npx startup-builder do Metric.query --json '{"metric":"save-rate","shape":"trend","window":"28d"}'{
"name": "do",
"arguments": {
"code": "await $.Subscription.unsubscribe({ subscription: 'subscription_15', reason: 'too-expensive', comment: 'We only manage 12 units now.' }, { mode: 'try' }) // 200: it ends Nov 14, at the close of the period, and access lasts until then\nawait $.Subscription.keep({ subscription: 'subscription_15', offer: 'offer_5' }, { mode: 'try' }) // the one save offer: the $19 Starter plan for 12 units, theirs to press\nawait $.Action.redirect({ action: 'action_702', changes: { effectiveAt: '2026-12-14' } }, { mode: 'try' }) // an agent tries to hold the cancel a month\n// 422 · The cancel for Tidewater Homes was not held: a cancel runs on its date.\n// exit-never-waits: Leave the cancel standing; the Customer takes it back with one press while access lasts. · then Subscription.keep\nreturn await $.Metric.query({ metric: 'save-rate', shape: 'trend', window: '28d' })",
"mode": "try"
}
}Response200
application/json{
"metric": {
"$id": "https: //api.sb/ leaseline.app/ metrics/ save-rate",
"$type": "Metric",
"name": "Save rate"
},
"rows": [
{
"week": "2026-10-19",
"value": 0.33,
"saved": 1,
"cancels": 3
}
]
}
sample1 of 3 cancels saved in 28 days · ■□□□ 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 |
|---|---|---|---|---|
POST /{namespace}/subscriptions/{subscription}/unsubscribe ends the Subscription at the close of its period and returns 200; one POST /{namespace}/subscriptions/{subscription}/keep offer follows, the Customer’s own press. | $0 per cancel; $0.04 per save note (Worker cost) | Save rate, Lost Customers | CSM | every call 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: the CSM has no person: save offers come to you to approve | Staff the CSM, or leave it: each save offer comes to you. | POST /{namespace}/roles/{role}/staff | 0 min | none |
The records
{
"$id": "https://api.sb/leaseline.app/subscriptions/subscription_15" ,
"$type": "Subscription",
"status": "active",
"renewsAt": null,
"termEndsAt": "2026-11-14",
"quantity": 1
}Calls
| Call | Here | Emits | Status |
|---|---|---|---|
POST /{namespace}/subscriptions/{subscription}/unsubscribe | 200: it ends Nov 14, at the close of the period, and access lasts until then | Subscription.unsubscribed | live |
POST /{namespace}/subscriptions/{subscription}/keep | the one save offer: the $19 Starter plan for 12 units, theirs to press | Subscription.kept | live |
POST /{namespace}/actions/{action}/redirect | an agent tries to hold the cancel a month | Action.redirected | live |
POST /{namespace}/metrics/query | Metric.queried | live |
Events
| Event | When it fires | What hears it |
|---|---|---|
Subscription.unsubscribed | the Customer cancels, at once | the save step of save-and-win-back, and the reason to customer-research |
Subscription.kept | the Customer presses keep before Nov 14 | the CSM’s readout; nothing is booked as lost |
Refuses when
| Check | Fix | Then |
|---|---|---|
exit-never-waits fail · Not live yet, gaps #3 | Leave the cancel standing; the Customer takes it back with one press while access lasts. | POST /{namespace}/subscriptions/{subscription}/keep |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The cancel for Tidewater Homes was not held: a cancel runs on its date.",
"checks": [
{
"check": "exit-never-waits",
"status": "fail",
"doc": "/code/refusals#check-exit-never-waits",
"fix": "Leave the cancel standing; the Customer takes it back with one press while access lasts.",
"then": "Subscription.keep"
}
]
}It says no, says why, and names the fix.
| Check | Refuses | Fix | Then |
|---|---|---|---|
exit-never-waits (Not live yet, gaps #3) | any agent Action that holds, delays or conditions a Customer’s cancel or downgrade | Leave the cancel standing; the Customer takes it back with one press while access lasts. | POST /{namespace}/subscriptions/{subscription}/keep |
customer-ask-cap (Not live yet, gaps #3) | a second open ask to one Customer in a window, from any Play | Wait for the open ask to close; GET /{namespace}/decisions names the Play that holds them. | GET /{namespace}/decisions |
Plays that use it
| Play | What it does | Use it when |
|---|---|---|
| Annual prepay | Offers a steady monthly Customer a year up front, at a discount their own churn pays for, and answers an annual cancel with one save. | 10 or more monthly Customers past their third renewal, losing 2 % or more a month |
save-at-cancel (Not live yet, gaps #4) | The Play that offers one save after a Customer cancels, with the cancel already done. | today: The hub Workflow cancel-subscription, then one POST /{namespace}/subscriptions/{subscription}/keep offer. |
Hub guides it deepens
| On startupbuilder.dev | Id |
|---|---|
| Cancel a subscription | workflow: |
| Save and win back | workflow: |
Example
Tidewater Homes · cancel on Oct 18, reason too-expensive, “We only manage 12 units now.” · 200, ends Nov 14 · one save offer: the $19 Starter plan · an agent’s hold on the cancel refused (exit-never-waits) · 1 of 3 cancels saved 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.