More seats and upgrades: when they are ready
More seats and upgrades: who is ready, the switch they press, and a downgrade that never waits.
More seats and a bigger plan are offered when use passes the Limit, and a downgrade runs at once.
You, to your agent
Which customers are ready for more seats, and what has to be set up first?
curl -X POST https://api.sb/leaseline.app/metrics/query \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"metric":"expansion-revenue","shape":"trend","breakdown":"customer","window":"28d"}'
curl -X POST "https://api.sb/leaseline.app/subscriptions/subscription_21/switch?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"offer":"offer_2"}'
curl -X POST "https://api.sb/leaseline.app/subscriptions/subscription_12/resize?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"quantity":6}'
curl -X POST "https://api.sb/leaseline.app/messages/message_79/schedule?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"at":"2026-10-22T15: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":"expansion-arr","shape":"trend","window":"28d"}'import { $ } from 'startup-builder'
const { rows } = await $.Metric.query({ metric: 'expansion-revenue', shape: 'trend', breakdown: 'customer', window: '28d' }) // 2 ready: Kestrel Rentals at 118 of 50 units, Elm Street at 180 of 150
await $.Subscription.switch({ subscription: 'subscription_21', offer: 'offer_2' }, { mode: 'try' }) // Kestrel to Team, prorated: the Customer’s own press
await $.Subscription.resize({ subscription: 'subscription_12', quantity: 6 }, { mode: 'try' }) // Birchway’s 10 to 6 runs at once; one save offer sits beside it
await $.Message.schedule({ message: 'message_79', at: '2026-10-22T15: 00: 00Z' }, { mode: 'try' }) // the seat note to Harbor Row, at 140 units
// 422 · The seat note to Harbor Row did not send: they are at risk.
// keep-first: Let their CSM reach them first, then ask again once they are healthy. · then Task.next
return await $.Metric.query({ metric: 'expansion-arr', shape: 'trend', window: '28d' }) // Not live yet, gaps #4
npx startup-builder do Metric.query --json '{"metric":"expansion-revenue","shape":"trend","breakdown":"customer","window":"28d"}'
npx startup-builder do Subscription.switch --json '{"subscription":"subscription_21","offer":"offer_2"}' --try
npx startup-builder do Subscription.resize --json '{"subscription":"subscription_12","quantity":6}' --try
npx startup-builder do Message.schedule --json '{"message":"message_79","at":"2026-10-22T15:00:00Z"}' --try
npx startup-builder do Metric.query --json '{"metric":"expansion-arr","shape":"trend","window":"28d"}' # Not live yet (gaps #4){
"name": "do",
"arguments": {
"code": "const { rows } = await $.Metric.query({ metric: 'expansion-revenue', shape: 'trend', breakdown: 'customer', window: '28d' }) // 2 ready: Kestrel Rentals at 118 of 50 units, Elm Street at 180 of 150\nawait $.Subscription.switch({ subscription: 'subscription_21', offer: 'offer_2' }, { mode: 'try' }) // Kestrel to Team, prorated: the Customer’s own press\nawait $.Subscription.resize({ subscription: 'subscription_12', quantity: 6 }, { mode: 'try' }) // Birchway’s 10 to 6 runs at once; one save offer sits beside it\nawait $.Message.schedule({ message: 'message_79', at: '2026-10-22T15:00:00Z' }, { mode: 'try' }) // the seat note to Harbor Row, at 140 units\n// 422 · The seat note to Harbor Row did not send: they are at risk.\n// keep-first: Let their CSM reach them first, then ask again once they are healthy. · then Task.next\nreturn await $.Metric.query({ metric: 'expansion-arr', shape: 'trend', window: '28d' }) // Not live yet, gaps #4",
"mode": "try"
}
}Response200
application/json{
"metric": {
"$id": "https: //api.sb/ leaseline.app/ metrics/ expansion-arr",
"$type": "Metric",
"name": "Expansion ARR"
},
"rows": [
{
"window": "28d",
"value": 2392,
"customers": 4,
"contraction": -347
}
]
}
sample · 1 Not live yet+$2,
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}/switch moves a Subscription to another Offer and POST /{namespace}/subscriptions/{subscription}/resize changes its seats; both prorate, and a downgrade runs at once. | $0.04 per note (Worker cost) | Upgrades, Downgrades | CSM | 4 of 5 calls live |
Set it up
| Check (dry run on a 0-Customer Startup, gaps #8) | Fix | Then | Your time | Wait |
|---|---|---|---|---|
limit-on-offer (Not live yet, gaps #8) · pass: Leaseline Pro: 50 units | Add a Limit to the Offer, then publish it. | POST /{namespace}/offers/{offer}/publish | 1 min | none |
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: 4 of your Customers have a person owner, Priya | Staff the CSM, or leave it: Priya’s 4 get drafts in her queue. | POST /{namespace}/roles/{role}/staff | 0 min | none |
The records
{
"$id": "https://api.sb/leaseline.app/subscriptions/subscription_21" ,
"$type": "Subscription",
"status": "active",
"quantity": 1,
"renewsAt": "2026-11-01"
}Calls
| Call | Here | Emits | Status |
|---|---|---|---|
POST /{namespace}/metrics/query | 2 ready: Kestrel Rentals at 118 of 50 units, Elm Street at 180 of 150 | Metric.queried | live |
POST /{namespace}/subscriptions/{subscription}/switch | Kestrel to Team, prorated: the Customer’s own press | Subscription.switched | live |
POST /{namespace}/subscriptions/{subscription}/resize | Birchway’s 10 to 6 runs at once; one save offer sits beside it | Subscription.resized | live |
POST /{namespace}/messages/{message}/schedule | the seat note to Harbor Row, at 140 units | Message.scheduled | live |
POST /{namespace}/metrics/query | Metric.queried | Not live yet, gaps #4 |
Events
| Event | When it fires | What hears it |
|---|---|---|
Subscription.switched | the Customer presses the upgrade | books Upgrades |
Subscription.resized | seats change, up or down | books Upgrades or Downgrades |
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 seat note to Harbor Row did not send: they are at risk.",
"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 |
limit-on-offer (Not live yet, gaps #8) | an upgrade Play on Offers that carry no Limit | Add a Limit to the Offer, then publish it. | POST /{namespace}/offers/{offer}/publish |
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 |
Plays that use it
| Play | What it does | Use it when |
|---|---|---|
| Usage upsell | Offers the next Offer when a Customer’s use paces past its Limit, with today’s prorated charge; the Customer upgrades in one press. | 10 or more Customers on an Offer with a Limit, 3 or more a month pacing past it |
| Signal expansion | Shows a growing Customer the seats they already use at the live Price, and names a successor within 5 business days when their champion leaves. | 20 or more paying Customers on a seat-priced Offer, 2 or more a month adding people past their seats |
| Single sign-on upgrade | Answers each single sign-on ask within one business hour with the plan that includes it, and the admin tests sign-in before paying. | 2 or more single sign-on asks a month, and an Offer that includes it |
| Next order due | Tells a Customer the day their last order makes the next one due, with the live price and a one-click order. | 10 or more Customers with a delivered Service Order in your outcome map, 3 or more next outcomes due a month |
downgrade-save (Not live yet, gaps #4) | The Play that offers one save before a seat or plan downgrade runs. | today: The hub Workflow change-plan: POST /{namespace}/subscriptions/{subscription}/resize runs at once; one POST /{namespace}/subscriptions/{subscription}/keep offer sits beside it. |
outcome-review (Not live yet, gaps #4) | The Play that reviews a delivered outcome with the Customer and offers the next one. | today: The hub Workflow fan-out-outcome. |
Hub guides it deepens
| On startupbuilder.dev | Id |
|---|---|
| Change a plan | workflow: |
| Trials and plan changes | process: |
Example
Kestrel Rentals · 118 of 50 units on Pro · Team offered, theirs to press · Birchway 10 to 6 seats runs at once · the seat note to Harbor Row refused (keep-first) · +$2,392 from upgrades, −$347 from downgrades 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.