# Subscriptions

A subscription is one recurring billing relationship on a contract. All date
fields are calendar dates on the seller's calendar — billing runs on dates, not
instants — and a new day begins at midnight in the timezone set on the
seller's organization profile (UTC when none is set). A term starting on the
1st is renewed and invoiced from midnight on the 1st there.

## 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` — collection failed on its **last permitted attempt** (where
  off-session collection is on, the loop makes at most 3 attempts in total, at
  least 20 minutes apart, before giving up; where it is off nothing retries
  and the first miss lands here), or a trial ended with no payment method on
  file.
  Nothing further is retried from here: recovery is a paid invoice on the
  contract — your hosted link, a matched transfer, a new card — after which
  the subscription returns to `active` on its own. 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`.

## Payment method on file

Renewals and other recurring-origin invoices are collected off-session only
where the deployment has `billing.invoice.auto_charge` switched on (a
platform flag operated by Fynex, not a per-account setting) **and** the
customer has a saved card. A card is saved on Fynex when the buyer ticks
*save card* on the hosted checkout page, provides an e-mail, and the payment
succeeds with a billing address; it is matched to the `sellerCustomerId` by
that e-mail. The `subscription` block on the Payments API's
`POST /initialize-payment` does **not** store a card here — it only flags the
processor-side stored-credential relationship. Without a saved card an
invoice is still issued and sent; it is paid by someone visiting its link or
making a transfer, and a renewal that cannot be collected goes `past_due`.
`trialRequiresPaymentMethod` on a trial says whether this must be in place
before the trial converts.

## 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` | Billing continues **on the original schedule** — the period is not restarted, and 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.
