# The Dodo subscription lifecycle, which event means what and who keeps access

> active, past_due, on_hold, paused, cancelled, failed, expired. Map each to access on purpose, re-read the subscription on every event, and let a grace period decide how failed renewals feel.

A subscription is a small state machine, and every provider names the states
a little differently. This repo stores Dodo's word as `provider_status` and a
shared word as `status`, which is what access reads:

| Dodo status | What happened | Stored as | Access |
|---|---|---|---|
| `pending` | checkout started, first payment not settled | `incomplete` | no |
| `active` | paying, all normal | `active` | yes |
| `active`, inside `trial_period_days` | free trial | `trialing` | yes |
| `past_due` | a renewal failed, grace period open | `past_due` | yes |
| `on_hold` | a renewal failed, no grace (or it ended) | `unpaid` | no |
| `paused` | suspended on purpose | `paused` | no |
| `cancelled` | ended | `canceled` | no |
| `failed` | the first payment never went through | `incomplete` | no |
| `expired` | the term ran out | `expired` | no |

An unknown word maps to `incomplete`, which grants nothing until someone maps
it.

## Failed renewals: grace period or not

When a renewal charge fails, Dodo moves the subscription to `on_hold`, or to
`past_due` if you set a **grace period** (Settings -> Subscriptions ->
Subscription Grace Period). Dodo's own description of the two is the rule this
repo follows: past due keeps access until the window ends, on hold is loss of
access.

That puts the policy in one place, Dodo's dashboard:

- **No grace period**: the first declined renewal switches paid features off.
  Strict, and it costs you customers whose bank refused one charge.
- **A grace period of a week or two**: they keep working while they fix the
  card, and most do.

Turn on **Payment Retries** too (Settings -> Recovery). They are off by
default, so without them Dodo never charges a failed renewal again on its own:
the customer has to update their card in the portal, which charges what is due
and makes the subscription `active`.

The customer needs to hear about it. On `payment.failed` for a renewal whose
subscription is now `past_due` or `unpaid`, the adapter emits a
`payment.failed` billing event and the shared core mails a warning with a link
to `/billing`. A first payment declined at checkout gets no warning: the buyer
saw it on Dodo's page.

## Trials

Dodo has no trial status. A trialing subscription is `active` with
`trial_period_days` above zero, counted from `created_at`. The adapter works
out the trial end and stores `trialing` until then. The trial length comes from
the catalogue (`trialDays` in `src/lib/pricing.ts`), sent with each checkout,
so a trial on the Dodo product itself is overridden.

## Cancelling at the period end

A cancellation in Dodo's portal sets `cancel_at_next_billing_date` and leaves
the subscription `active` until then. That arrives as `subscription.updated`,
so subscribe to it. The row stores `cancel_at_period_end = true`, and the
billing page says "Ends on" the next billing date instead of "Renews on". Always
read that through `scheduledEnd(subscription)`, never the flag alone.

## Plan changes

`subscription.plan_changed` fires when the customer switches plans in the
portal. The subscription now has a different `product_id`, while the checkout
metadata still names the plan they first bought. The adapter names the plan
from the product only (`BILLING_PRICE_*` maps it back to the catalogue), so a
downgrade is never read as the old plan. A product no env var names is stored
with no plan: shown as paid, unlocking no plan, and logged.

## Re-read on every event

Every `subscription.*` handler throws the payload away and calls
`dodo.subscriptions.retrieve(id)`. Dodo retries a failed delivery for about a
day, so events arrive out of order: a `subscription.active` from before a
cancellation can land after it. Storing whatever Dodo says now makes the order
irrelevant, and makes a resend from the dashboard (or
`bun run billing:reconcile`) a safe way to repair anything.

## Which events to subscribe

All of these, on both the test and the live endpoint:

```
subscription.active   subscription.updated    subscription.renewed
subscription.plan_changed   subscription.past_due   subscription.on_hold
subscription.paused   subscription.unpaused   subscription.cancelled
subscription.failed   subscription.expired
```

plus `payment.succeeded` and `payment.failed` for receipts and warnings. The
list in code is `HANDLED_EVENTS` in `src/lib/billing/dodo-events.ts`. The
easy ones to miss are `subscription.updated` (scheduled cancellations) and
`subscription.plan_changed` (portal switches): without them the app keeps the
old truth.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
