# Sandbox & testing

Everything here works without touching live money. The goal: your first
successful call within minutes, and a full trial-to-renewal subscription
exercised without waiting a month of wall-clock time.

## Hosts

| Environment | Host |
| --- | --- |
| Staging | `https://staging-api.fynex.ai` |
| Production | Assigned per deployment — confirm yours with Fynex before go-live. |

The environment that matters is your **account's**, not the host's: a Fynex
account operates in demo mode or live mode, and test behaviour follows the
account. Demo accounts exist on both hosts.

Both hosts are also declared in the OpenAPI document itself, in its `servers`
block — production first, the sandbox second, each labelled. A generated client
or an agent that reads only the spec can therefore pick the sandbox rather than
defaulting to the one host it happens to see. Each entry is the bare origin;
the `/billing-api/v1` prefix is already part of every path in the document, so
join a server with a path exactly as written and do not add the prefix twice.

## Keys

**No account yet? Mint a sandbox in one call.** `POST /sandbox/accounts` with
a JSON body creates an anonymous demo seller — wallets, a demo terminal, a
pre-set split rule, three test customers — and returns its `sk_test_` key:

```bash
curl -X POST https://staging-api.fynex.ai/sandbox/accounts \
  -H "Content-Type: application/json" -d '{}'
```

The door is open on the sandbox host (`staging-api.fynex.ai`); a host that has
it switched off answers `503` to this call and shows no `/sandbox` page. The
secret is shown once; the keys are time-limited — `expiresAt` in the
response says exactly when they stop working. An anonymous sandbox has no
dashboard login — it is a key, not a user — and it is a test environment: never
enter real personal or bank details. The `/sandbox` page on the API host has
the full contract.

For your own account, issue a **secret** key in the Fynex dashboard: **Integration** in the
left-hand menu, then the API keys card. While your account is in demo mode the
key is `sk_test_…`; after go-live only `sk_live_…` keys authenticate and the
test key answers `401` from then on. A key is shown once, at creation — store
it server-side and never in a browser.

The same key authenticates every surface in these docs: the Billing API and
usage ingestion under `/billing-api/v1`, and the Payments API.

## Test cards

On a demo account, hosted payment pages (including the links
`POST /invoices/{invoiceId}/send` creates) accept these Visa sandbox numbers:

| Card number | Notes |
| --- | --- |
| `4111 1111 1111 1111` | Universal Visa test card |
| `4530 9100 0001 2345` | Visa |
| `4037 1122 3300 0001` | Visa |

For all of them: any future expiry, any 3-digit CVV, Latin cardholder name.
Paying an invoice's link with one of these drives the document through
`sent → paid` exactly as a live card would, so it is the way to test the
settlement-polling loop end to end.

## Time travel

Subscription flows — trial ending, first charge, renewal — run on billing
dates, and a sandbox that makes you wait 30 real days for a renewal is not a
sandbox. **Each contract can be given a test clock of its own and moved
forward**, with a `sk_test_` key and no help from us:

| Call | What it does |
| --- | --- |
| `GET /contracts/{contractId}/test-clock` | Reads what the billing engines treat as *now* for that one contract. |
| `POST /contracts/{contractId}/test-clock/advance` | Moves it forward to `to`, then runs that contract's subscription lifecycle and recurring invoicing as of it. |

**Test-mode (demo) accounts only.** A live account answers `403` on both: a
live subscription's renewal is real revenue and a real document to a real
customer.

**One contract at a time.** The clock belongs to the contract in the path.
Advancing it never moves a sibling contract — yours or another account's — and
no scheduled pass sees a changed clock. That is why this is safe to publish.

**Forward only, capped at 366 days.** `to` must be after the contract's
current clock. Re-sending the instant the clock already sits at is a **no-op**:
`200` with `advanced: false`, nothing renewed and nothing issued twice, so the
call is safe to retry. A target in the past is `400` — the invoices an advance
mints are numbered and immutable, so a clock cannot be wound back.

### Worked example: a monthly subscription renewing in one call

Create the subscription today (`42` is a contract that already carries at
least one invoice — see the note below):

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

Ask where the contract's clock is:

```bash
curl -s "$FYNEX_API_BASE/contracts/42/test-clock" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
# {"contractId":42,"now":"2026-01-01T09:15:00Z","simulated":false}
```

Advance it past the end of the first term — 31 days:

```bash
curl -s -X POST "$FYNEX_API_BASE/contracts/42/test-clock/advance" \
  -H "Authorization: Bearer $FYNEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"2026-02-01T00:00:00Z"}'
```

```json
{
  "clock": { "contractId": 42, "now": "2026-02-01T00:00:00Z", "simulated": true },
  "advanced": true,
  "lifecycle": { "renewed": 1, "trialsActivated": 0, "cronDisabled": false },
  "invoices": { "issued": 1, "sent": 1, "skipped": 0, "failed": 0, "passDisabled": false }
}
```

The renewal is now visible on the two reads that matter — the subscription's
new term, and the document it earned:

```bash
curl -s "$FYNEX_API_BASE/subscriptions?contractId=42" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
# currentPeriodStart moves to 2026-02-01, currentPeriodEnd to 2026-03-01

curl -s "$FYNEX_API_BASE/invoices?contractId=42" \
  -H "Authorization: Bearer $FYNEX_API_KEY"
# one more invoice, origin "recurring", for the new term
```

`GET /contracts/{contractId}/invoices/upcoming` before the advance, and the
issued document after it, should agree — which is the cheapest end-to-end
check that your contract is configured the way you think it is.

### What it does not advance

The clock moves the **subscription state machine** and the **recurring
invoice**. It does *not*:

- close metered usage periods or mint usage invoices,
- send dunning e-mail for a subscription that went `past_due`,
- run off-session auto-charge on an issued invoice,
- move the issue date: an invoice raised by an advance is dated at REAL time
  (so its number stays in the real month's gapless sequence) while its period
  is the simulated term — read the period, not the date, to check the renewal.

Those stay on real time in their own scheduled passes, and every advance
repeats the list in its `notAdvanced` field. Do not write a test that waits on
one of them after an advance.

### When an advance renews but issues nothing

Two operator switches decide whether the halves above run at all, and the
response says which one stopped:

| Response field | Switch | Symptom |
| --- | --- | --- |
| `lifecycle.cronDisabled` | `billing.subscription.lifecycle_cron` | Nothing renews, no matter how far the clock moves. |
| `invoices.passDisabled` | `billing.recurring.invoice_pass` | The term renews, and no document appears on `GET /invoices`. |

Both default to **off** — see the table below. There is also one data
precondition the clock cannot supply: the recurring lane continues a
contract's existing invoicing pattern, so a contract with **no prior invoice**
is reported in `invoices.skipped` rather than issued. Raise the first document
once with `POST /invoices` (or let a contract you already invoiced carry the
subscription), and every later advance issues by itself.

The staff-operated clock is a different thing and is not this. It shifts the
lifecycle dates of test-mode *payment-link* subscriptions (the recurring
hosted-checkout product), estate-wide, and never touches the billing
subscriptions this reference describes.

## What a fresh sandbox has switched off

Several billing capabilities sit behind operator feature flags that default to
**off**, because each one moves money or issues documents on a schedule. A
sandbox where they are off looks broken — subscriptions never charge, usage
periods never close into invoices, no invoice is ever raised, credits report
`501` — when it is merely unconfigured. If a flow below does nothing, ask Fynex
to confirm the flag before debugging your integration:

| Flag | What it gates |
| --- | --- |
| `billing.subscription.lifecycle_cron` | The pass that ends trials, charges renewals and applies scheduled plan changes. |
| `billing.usage.close_pass` | The pass that closes usage periods and mints usage invoice lines. |
| `billing.recurring.invoice_pass` | The pass that turns due subscription terms into issued invoices. |
| `billing.usage.invoice_pass` | The pass that composes closed usage lines into issued invoices. Separate from the recurring one on purpose — a seller may be ready to auto-bill terms and not metered usage, or the reverse. |
| `billing.proration.invoice_binding` | Whether a subscription's unbilled mid-term proration is put on the invoice its term is billed on — a charge as an extra line, a credit as a discount on the recurring line, any remainder as a non-expiring `proration` credit lot. While off, a mid-term change is still recorded against the contract but never reaches a document. |
| `billing.invoice.auto_charge` | Off-session collection: the loop that charges a customer's saved card for a sent or overdue invoice. Without it a document is only ever paid by someone visiting its link or making a transfer. |
| `payment_links.recurring_billing` | Recurring charging through hosted payment links. |
| `billing.credits` | Stored-value credits; while off, the credit endpoints answer `501` and invoices draw no credit down. |

**Closing a period is not the same as billing it.** The close pass and the
invoice passes are different switches: with only `billing.usage.close_pass` on,
`GET /contracts/{contractId}/usage` moves and periods close, and no invoice
ever appears on `GET /invoices`. That pairing is the single most common reason
a sandbox looks like it has stopped halfway.

## A sandbox session that proves the loop

The target this page is written against is the one in our own onboarding
PRD: **a first transaction under 30 minutes, self-serve** — from a fresh
sandbox key to an invoice on `GET /invoices` without talking to anyone at
Fynex. The steps below are that path; if one of them cannot be done inside
that budget, the gap is ours, not yours.

1. `GET /contracts` — key, host and network path all work.
2. Register a metric and price it (see **Usage ingestion**), send a few
   events, and watch `GET /contracts/{contractId}/usage` move.
3. Create a subscription in the dashboard with a trial that has already
   ended, and let the lifecycle pass run — the renewal invoice appears on
   `GET /invoices`.
4. `POST /invoices/{invoiceId}/send`, pay the link with a test card, and poll
   the invoice to `paid`.

That is the whole billing loop — metering, rating, issuance, collection,
settlement — without a real card or a real month.
