Skip to content

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.

Open the renewalMode try
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"
  }
}
Response200application/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 yet

1 renewal open · $348 a year at stake · 15 days out · sample

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

What it does

What it doesPrice per unitMovesWho does itToday
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 CustomersCSM4 of 5 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
use-when (Not live yet, gaps #8) · pass: 9 Customers renew in the next 60 daysRun the Play Not yet when names, or wait until the volume is there.POST /{namespace}/metrics/query1 minnone

The records

Juniper Homes’ renewal
{
  "$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

CallHereEmitsStatus
POST /{namespace}/renewals/open15 days out: the usage review reads 38 units, down from 64Renewal.openedlive
POST /{namespace}/quotes/{quote}/sendannual-prepay’s offer: the year at 25 % offQuote.sentlive
POST /{namespace}/approvals/{approval}/approveApproval.approvedlive
POST /{namespace}/subscriptions/{subscription}/renewwhat Nov 3 bills, at the Offer Version they acceptedSubscription.renewedlive
POST /{namespace}/metrics/queryMetric.queriedNot live yet, gaps #4

Events

EventWhen it firesWhat hears it
Renewal.openedthe renewal opens before the term endsthe CSM’s Task and the notice
Subscription.renewedthe next term startsbooks nothing new; a lapse books Lost Customers

Refuses when

422RefusedThe prepay Quote to Juniper Homes did not send: 25 % off is below the Offer’s floor.
CheckFixThen
price fail · Not live yet, gaps #1Get 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.

CheckRefusesFixThen
price (Not live yet, gaps #1)a Quote below the Offer’s price floorGet 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 leavingLet their CSM reach them first, then ask again once they are healthy.POST /{namespace}/tasks/next

Plays that use it

PlayWhat it doesUse it when
Annual prepayOffers 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.devId
Renewal noticeworkflow:renewal-notice
Billingprocess:billing

Example

Leaseline, as the fixture ran it
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.

Contents