# Recurring billing

URL: /systems/finance/recurring-billing · Id: `capability:recurring-billing`

> Recurring billing charges Subscriptions on Stripe Billing, with trials, proration, metered usage, retries and dunning, while db.sb stays the record.

Recurring billing charges [Subscriptions](/business/nouns/subscription) each period on Stripe, with trials, pauses, quantity changes, proration and renewal notices. [db.sb](/code/platform/db-sb) keeps the record, and Stripe moves the money.

## Contract
A Subscription is always in one of six statuses, and only a [Verb](/business/nouns/verb) call or the calendar moves it.

| Status | What the [Customer](/business/nouns/customer) has | Entered by |
| --- | --- | --- |
| `trialing` | The [Offer](/business/nouns/offer) free until `Subscription.trialEndsAt` | [Offer.trial](/business/nouns/offer/trial) |
| `active` | The Offer, billed each period at the Offer [Version](/business/nouns/version) it accepted | [`Offer.subscribe`](/business/nouns/offer/subscribe), [`Subscription.resume`](/business/nouns/subscription/resume) or [`Subscription.keep`](/business/nouns/subscription/keep) |
| `paused` | Nothing billed and no [Entitlements](/business/nouns/entitlement) until the pause ends | [Subscription.pause](/business/nouns/subscription/pause) |
| `ending` | Full access until the period closes, then nothing | [Subscription.unsubscribe](/business/nouns/subscription/unsubscribe) |
| `pastDue` | Access while [Dunning](/workflows/processes/billing/dunning) retries a failed renewal | [Subscription.renew](/business/nouns/subscription/renew) |
| `canceled` | No access, and still a Customer forever | The period closing after `ending`, or the last failed retry of [Dunning](/workflows/processes/billing/dunning) |

| Move | Rule |
| --- | --- |
| Start a trial | `Offer.trial` takes `days` and `card`. With a card it converts when the trial ends; without one it ends and opens a conversion [Task](/business/nouns/task) for the [Success Manager](/agents/customer/success-manager). |
| Resize | [Subscription.resize](/business/nouns/subscription/resize) bills the prorated difference at once; a lower `quantity` takes effect at the next period. |
| Switch Offers | [Subscription.switch](/business/nouns/subscription/switch) prorates at once. A move to a cheaper Offer never waits; a higher [Price](/business/nouns/price) reaches existing Customers only at their first renewal after notice. |
| Pause | `Subscription.pause` stops billing and Entitlements until `until`, and moves `Subscription.renewsAt` and `Subscription.termEndsAt` by the paused days. `Subscription.resume` ends a pause early. |
| Cancel | `Subscription.unsubscribe` runs at once, with no [Limit](/business/nouns/limit), [Approval](/business/nouns/approval) or delay window, and records a reason code and the Customer’s own words as [Evidence](/business/nouns/evidence). |
| Take a cancel back | `Subscription.keep` restores renewal on the original date, on a cheaper Offer when the Customer took the one save offer. |
| Renew | `Subscription.renew` bills the next period at the accepted Version, never a newer Price, and adds each [Meter](/business/nouns/meter)’s usage exactly once. |
| Remind | [Renewal notice](/workflows/processes/billing/renewal-notice) writes 15 to 45 days before a term of a year or longer renews, and 3 to 21 days before a trial over 31 days converts. |

A cancel’s reason code is one of `tooExpensive`, `missingCapability`, `switched`, `unused`, `notNeeded`, `wrongFit`, `service` or `other`. Each maps one to one onto Stripe’s cancellation feedback, so both records agree.

## Verbs
| Verb | Object | Event |
| --- | --- | --- |
| [Offer.subscribe](/business/nouns/offer/subscribe) | [Offer](/business/nouns/offer) | [`Offer.subscribed`](/business/events/Offer.subscribed) |
| [Subscription.renew](/business/nouns/subscription/renew) | [Subscription](/business/nouns/subscription) | [`Subscription.renewed`](/business/events/Subscription.renewed) |
| [Subscription.switch](/business/nouns/subscription/switch) | [Subscription](/business/nouns/subscription) | [`Subscription.switched`](/business/events/Subscription.switched) |
| [Subscription.unsubscribe](/business/nouns/subscription/unsubscribe) | [Subscription](/business/nouns/subscription) | [`Subscription.unsubscribed`](/business/events/Subscription.unsubscribed) |
| [Offer.trial](/business/nouns/offer/trial) | [Offer](/business/nouns/offer) | [`Offer.trialed`](/business/events/Offer.trialed) |
| [Subscription.resize](/business/nouns/subscription/resize) | [Subscription](/business/nouns/subscription) | [`Subscription.resized`](/business/events/Subscription.resized) |
| [Subscription.pause](/business/nouns/subscription/pause) | [Subscription](/business/nouns/subscription) | [`Subscription.paused`](/business/events/Subscription.paused) |
| [Subscription.resume](/business/nouns/subscription/resume) | [Subscription](/business/nouns/subscription) | [`Subscription.resumed`](/business/events/Subscription.resumed) |
| [Subscription.keep](/business/nouns/subscription/keep) | [Subscription](/business/nouns/subscription) | [`Subscription.kept`](/business/events/Subscription.kept) |

## Interfaces
- [API](/code/api/reference)
- [SDK](/code/sdk/reference)
- [CLI](/code/cli/reference)
- [MCP](/code/mcp/reference)
- Webhooks

## Example
```ts
import { $ } from 'startup-builder'

// tutorloop.com sells Family at $40 a month per learner. A parent starts a 14-day trial with a card.
const { subscription } = await $.Offer.trial({ offer: 'offer_family', customer: 'customer_1', days: 14, card: true })

// The trial converted on May 1. On May 16 a second learner joins: $20 prorated is billed at once.
await $.Subscription.resize({ subscription: subscription.$id, quantity: 2 })

// The family travels in July. Billing stops until August 1, and each later renewal moves by the paused days.
await $.Subscription.pause({ subscription: subscription.$id, until: '2026-08-01' })

// In September the parent cancels in one step; the status is ending, and access lasts until the period closes.
await $.Subscription.unsubscribe({ subscription: 'subscription_1', reason: 'tooExpensive', comment: 'Two learners is a stretch this term.' })

// The one save offer is the $30 Family Lite Offer. The parent takes it before the period closes.
await $.Subscription.keep({ subscription: 'subscription_1', offer: 'offer_family_lite' })
```