# Subscriptions

A subscription is one recurring billing relationship on a contract. All date
fields are UTC calendar dates — billing runs on dates, not instants.

## States

`trial → active ⇄ past_due / paused → canceled | expired`

- `trial` — running a free trial until `trialEnd`. `trialEndBehavior` says
  which way it ends: `convert` (becomes a paying subscription) or `cancel`
  (lapses). `trialRequiresPaymentMethod` tells you whether a payment method
  must be on file first.
- `active` — billing normally within `currentPeriodStart…currentPeriodEnd`.
- `past_due` — a renewal charge failed. **No automatic retry runs**: recovery
  is your action or a dashboard operation. Treat this status as "act now",
  not as "Fynex is handling it".
- `paused` — billing suspended (`pausedAt`, optional auto-resume at
  `pauseEndsAt`).
- `canceled` / `expired` — terminal. A cancellation with notice keeps serving
  until `cancelEffectiveAt`.

## Price and plan

There is no separate plan object: the plan reference is
`priceMinor + currency + billingFrequency` (with `customUnit`/`customEvery`
for custom cadences). A scheduled downgrade appears as
`pendingPriceMinor`/`pendingPriceChangeAt` until the renewal pass applies it.

## Endpoints

Read:

- `GET /billing-api/v1/subscriptions` — list, newest first. Filter:
  `contractId`. Pages with `cursor`/`limit`, newest first.
- `GET /billing-api/v1/subscriptions/{subscriptionId}` — one subscription.

Create:

- `POST /billing-api/v1/contracts/{contractId}/subscriptions` — a new
  subscription on a contract you own. **`Idempotency-Key` is required** (1–128
  characters, `A–Z a–z 0–9 _ . : -`): the same key always returns the
  subscription the first call created (`200` instead of `201`), whatever the
  body of the retry; use one key per subscription you intend to create. Keys
  are scoped to your seller account — the same key on a different contract
  answers `422`; a key under a billing-engine prefix (`proration:`, `redeem:`,
  `subscription:`, …) answers `400`. Body: `frequency` (`daily`, `weekly`,
  `bi_weekly`, `monthly`, `quarterly`, `semi_annual`, `annual`, `custom` with
  `customUnit`/`customEvery`), `priceMinor` + `currency` (the contract's),
  `startDate` (up to a year in the past), optional `anchorDate`, `trialEnd` +
  `trialEndBehavior` (`convert`/`cancel`), `autoRenew` (default `true`; an
  explicit `false` needs `endDate`, and `endDate` is refused otherwise),
  `noticePeriodDays` (0–365), `prorationPolicy`
  (`by_day`/`full_period`/`next_period` — selects what an immediate
  `change-plan` does once the proration engine is enabled: prorate the
  unserved remainder, bill the whole current period at the new price with the
  elapsed days included and no adjustment, or keep the old price until the
  next period. Inert while the engine is off, which is the default. Where
  invoice binding is enabled for the environment, the recorded proration is
  applied to the term's next invoice).

```bash
curl -X POST "$FYNEX_API_BASE/contracts/42/subscriptions" \
  -H "Authorization: Bearer $FYNEX_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"frequency":"monthly","priceMinor":9900,"currency":"EUR","startDate":"2026-10-01","noticePeriodDays":30}'
```

Lifecycle, all `POST /billing-api/v1/subscriptions/{subscriptionId}/…`:

| Action | Body | From | Effect |
| --- | --- | --- | --- |
| `cancel` | — | `trial`, `active`, `past_due`, `paused` | Sets `cancelRequestedAt`; serves until `cancelEffectiveAt` (notice period) or term end. Terminal. |
| `pause` | `{"pauseUntil": "YYYY-MM-DD"}` optional | `active` | Suspends billing; `pauseEndsAt` when a date was given. Longer than the seller's pause policy → `422`. |
| `resume` | — | `paused` | New term from today; the pause is not billed. |
| `end-trial` | — | `trial` | Converts now per `trialEndBehavior` / `trialRequiresPaymentMethod`. |
| `change-plan` | `{"priceMinor": 12900, "currency": "EUR", "atTermEnd": true}` | `active` | Scheduled (`pendingPriceMinor`) or immediate. `atTermEnd: true` always schedules; with `false` the subscription's `prorationPolicy` decides while the proration engine is on — `next_period` keeps the old price for the rest of this period and applies the new one from the next, `by_day` applies it now and posts an adjustment for the unserved remainder, `full_period` applies it now and bills the whole current period at the new price, elapsed days included, with no adjustment. Same price, or a change already pending → `422`. Currency cannot change. |

Every action returns the updated subscription. A request the current state
cannot take — pausing a trial, resuming an active subscription, cancelling
twice — answers `422` with the reason; so does a lost race against the
lifecycle pass (`subscription changed concurrently; re-read it and retry`).
`404` means the subscription is not yours. Marking past-due or recovered is
the collection loop's job and has no public route.
