URL: /revenue/renewals · Id: `page:/revenue/renewals`

# 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.

```text title="Ask"
Juniper Homes renews Nov 3. Open the renewal and offer the year up front. Don’t send.
```

```ts title="SDK"
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
```

```json title="Response · sample"
{
  "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
    }
  ]
}
```

`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 does | Price per unit | Moves | Who does it | Today |
| --- | --- | --- | --- | --- |
| <Op verb="Renewal.open" /> starts the renewal with a usage review and the `renewal-notice` Workflow; <Op verb="Subscription.renew" /> bills the next term at the accepted Offer Version. | $0.04 per notice (Worker cost) | [One-time sales ending](/revenue/one-time-rolloff-arr), [Lost Customers](/revenue/churned-arr) | [CSM](/capabilities/how-your-agents-act#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`](/code/refusals#check-billing-connected) (Not live yet, gaps #8) · fail | Connect Stripe so every charge and Meter reading is live. | <Op verb="Integration.connect" /> | 5 min | the first sync, about 10 min |
| [`use-when`](/code/refusals#check-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. | <Op verb="Metric.query" /> | 1 min | none |

## The records
```json title="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
| Call | Here | Emits | Status |
| --- | --- | --- | --- |
| <Op verb="Renewal.open" /> | 15 days out: the usage review reads 38 units, down from 64 | `Renewal.opened` | live |
| <Op verb="Quote.send" /> | annual-prepay’s offer: the year at 25 % off | `Quote.sent` | live |
| <Op verb="Approval.approve" /> |  | `Approval.approved` | live |
| <Op verb="Subscription.renew" /> | what Nov 3 bills, at the Offer Version they accepted | `Subscription.renewed` | live |
| <Op verb="Metric.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

```json title="The 422 · sample"
{
  "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"
    }
  ]
}
```

| Check | Refuses | Fix | Then |
| --- | --- | --- | --- |
| [`price`](/code/refusals#check-price) (Not live yet, gaps #1) | a Quote below the Offer’s price floor | Get the discount approved, or quote at the floor. | <Op verb="Approval.approve" /> |
| [`keep-first`](/code/refusals#check-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. | <Op verb="Task.next" /> |

## Plays that use it
| Play | What it does | Use it when |
| --- | --- | --- |
| [Annual prepay](/plays/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](https://startupbuilder.dev/workflows/processes/billing/renewal-notice) | `workflow:renewal-notice` |
| [Billing](https://startupbuilder.dev/workflows/processes/billing) | `process:billing` |

## Example
```text title="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
```