Renewals: the notice, the prepay and the date
Renewals: the renewal opens with a usage review, the prepay offer stays above the floor, and the date bills.
Each renewal opens 30 days out with a usage review, then one prepay offer inside your price floor.
You, to your agent
Juniper Homes renews Nov 3. Open the renewal and offer the year up front. Don’t send.
curl -X POST "https://api.sb/leaseline.app/renewals/open?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subscription":"subscription_7"}'
curl -X POST "https://api.sb/leaseline.app/quotes/quote_7/send?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
curl -X POST "https://api.sb/leaseline.app/approvals/approval_131/approve?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"note":"Annual at 15 % off, inside the cap"}'
curl -X POST "https://api.sb/leaseline.app/subscriptions/subscription_7/renew?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
# 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":"one-time-rolloff-arr","shape":"trend","window":"28d"}'import { $ } from 'startup-builder'
await $.Renewal.open({ subscription: 'subscription_7' }, { mode: 'try' }) // 15 days out: the usage review reads 38 units, down from 64
await $.Quote.send({ quote: 'quote_7' }, { mode: 'try' }) // annual-prepay’s offer: the year at 25 % off
// 422 · The prepay Quote to Juniper Homes did not send: 25 % off is below the Offer’s floor.
// price: Get the discount approved, or quote at the floor. · then Approval.approve
await $.Approval.approve({ approval: 'approval_131', note: 'Annual at 15 % off, inside the cap' }, { mode: 'try' })
await $.Subscription.renew({ subscription: 'subscription_7' }, { mode: 'try' }) // what Nov 3 bills, at the Offer Version they accepted
return await $.Metric.query({ metric: 'one-time-rolloff-arr', shape: 'trend', window: '28d' }) // Not live yet, gaps #4
npx startup-builder do Renewal.open --json '{"subscription":"subscription_7"}' --try
npx startup-builder do Quote.send --json '{"quote":"quote_7"}' --try
npx startup-builder do Approval.approve --json '{"approval":"approval_131","note":"Annual at 15 % off, inside the cap"}' --try
npx startup-builder do Subscription.renew --json '{"subscription":"subscription_7"}' --try
npx startup-builder do Metric.query --json '{"metric":"one-time-rolloff-arr","shape":"trend","window":"28d"}' # Not live yet (gaps #4){
"name": "do",
"arguments": {
"code": "await $.Renewal.open({ subscription: 'subscription_7' }, { mode: 'try' }) // 15 days out: the usage review reads 38 units, down from 64\nawait $.Quote.send({ quote: 'quote_7' }, { mode: 'try' }) // annual-prepay’s offer: the year at 25 % off\n// 422 · The prepay Quote to Juniper Homes did not send: 25 % off is below the Offer’s floor.\n// price: Get the discount approved, or quote at the floor. · then Approval.approve\nawait $.Approval.approve({ approval: 'approval_131', note: 'Annual at 15 % off, inside the cap' }, { mode: 'try' })\nawait $.Subscription.renew({ subscription: 'subscription_7' }, { mode: 'try' }) // what Nov 3 bills, at the Offer Version they accepted\nreturn await $.Metric.query({ metric: 'one-time-rolloff-arr', shape: 'trend', window: '28d' }) // Not live yet, gaps #4",
"mode": "try"
}
}Response200
application/json{
"metric": {
"$id": "https: //api.sb/ leaseline.app/ metrics/ one-time-rolloff-arr",
"$type": "Metric",
"name": "One-time roll-off ARR"
},
"rows": [
{
"window": "28d",
"value": 0,
"orders": 0
}
]
}
sample · 1 Not live yet1 renewal open · $348 a year at stake · 15 days out · sample
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}/renewals/open starts the renewal with a usage review and the renewal-notice Workflow; POST /{namespace}/subscriptions/{subscription}/renew bills the next term at the accepted Offer Version. | $0.04 per notice (Worker cost) | One-time sales ending, Lost Customers | CSM | 4 of 5 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 |
use-when (Not live yet, gaps #8) · pass: 9 Customers renew in the next 60 days | Run the Play Not yet when names, or wait until the volume is there. | POST /{namespace}/metrics/query | 1 min | none |
The records
{
"$id": "https://api.sb/leaseline.app/renewals/renewal_5" ,
"$type": "Renewal",
"termEndsAt": "2026-11-03",
"openedAt": "2026-10-19",
"usageReview": {
"units": 38,
"was": 64
},
"noticeSentAt": null,
"saveOffer": "offer_6",
"outcome": null
}Calls
| Call | Here | Emits | Status |
|---|---|---|---|
POST /{namespace}/renewals/open | 15 days out: the usage review reads 38 units, down from 64 | Renewal.opened | live |
POST /{namespace}/quotes/{quote}/send | annual-prepay’s offer: the year at 25 % off | Quote.sent | live |
POST /{namespace}/approvals/{approval}/approve | Approval.approved | live | |
POST /{namespace}/subscriptions/{subscription}/renew | what Nov 3 bills, at the Offer Version they accepted | Subscription.renewed | live |
POST /{namespace}/metrics/query | Metric.queried | Not live yet, gaps #4 |
Events
| Event | When it fires | What hears it |
|---|---|---|
Renewal.opened | the renewal opens before the term ends | the CSM’s Task and the notice |
Subscription.renewed | the next term starts | books nothing new; a lapse books Lost Customers |
Refuses when
| Check | Fix | Then |
|---|---|---|
price fail · Not live yet, gaps #1 | Get the discount approved, or quote at the floor. | POST /{namespace}/approvals/{approval}/approve |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The prepay Quote to Juniper Homes did not send: 25 % off is below the Offer’s floor.",
"checks": [
{
"check": "price",
"status": "fail",
"doc": "/code/refusals#check-price",
"fix": "Get the discount approved, or quote at the floor.",
"then": "Approval.approve"
}
]
}It says no, says why, and names the fix.
| Check | Refuses | Fix | Then |
|---|---|---|---|
price (Not live yet, gaps #1) | a Quote below the Offer’s price floor | Get the discount approved, or quote at the floor. | POST /{namespace}/approvals/{approval}/approve |
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 |
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 |
Hub guides it deepens
| On startupbuilder.dev | Id |
|---|---|
| Renewal notice | workflow: |
| Billing | process: |
Example
Juniper Homes · renews Nov 3 · opened Oct 19, 38 units against 64 · the prepay Quote at 25 % off refused (price) · approved at 15 % off, inside the cap · 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.