Subscriptions
Subscription states, trials, the payment method on file, and price and plan.
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 untiltrialEnd.trialEndBehaviorsays which way it ends:convert(becomes a paying subscription) orcancel(lapses).trialRequiresPaymentMethodtells you whether a payment method must be on file first.active— billing normally withincurrentPeriodStart…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 toactiveon its own. Treat this status as "act now", not as "Fynex is handling it".paused— billing suspended (pausedAt, optional auto-resume atpauseEndsAt).canceled/expired— terminal. A cancellation with notice keeps serving untilcancelEffectiveAt.
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 withcursor/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-Keyis required (1–128 characters,A–Z a–z 0–9 _ . : -): the same key always returns the subscription the first call created (200instead of201), 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 answers422; a key under a billing-engine prefix (proration:,redeem:,subscription:, …) answers400. Body:frequency(daily,weekly,bi_weekly,monthly,quarterly,semi_annual,annual,customwithcustomUnit/customEvery),priceMinor+currency(the contract's),startDate(up to a year in the past), optionalanchorDate,trialEnd+trialEndBehavior(convert/cancel),autoRenew(defaulttrue; an explicitfalseneedsendDate, andendDateis refused otherwise),noticePeriodDays(0–365),prorationPolicy(by_day/full_period/next_period— selects what an immediatechange-plandoes 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).
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.