# Fynex Billing API

The Billing API is the public, versioned REST surface for billing data:
invoices, subscriptions, contracts, metered usage, and credit balances. Your
systems read the documents the billing engine issues, raise collection on
them, and write the metering that drives them — see **What v1 does, and what
it does not** below for exactly where that line falls.

All endpoints live under the path prefix:

```
/billing-api/v1
```

The host depends on the environment you were onboarded to — see **Base URL**
in the Quickstart. Throughout these guides examples use `$FYNEX_API_BASE`,
which you set once to your host plus that prefix.

Authenticate every request with your seller secret key (`sk_test_…` or
`sk_live_…`) in the `Authorization` header.

**New here? Read Concepts first** — five objects and the relationships
between them, in two minutes. **Then the Quickstart**, which goes from issuing
a key to listing invoices and downloading a PDF. Then:

| Guide | What it covers |
| --- | --- |
| Concepts | The five objects, how they relate, and the three questions their names do not answer. |
| Quickstart | Your first calls, and what each failure status means. |
| Authentication | Key format, environments, rotation, failure contract. |
| Errors | Status contract, rate limits, what is safe to retry. |
| Sandbox & testing | Hosts, keys, test cards, how to exercise date-driven flows without waiting, and the flags a sandbox needs on. |
| Pagination & Amounts | The keyset paging loop; minor units and decimal strings. |
| Endpoint reference | Every operation in detail: parameters, response fields, failure modes, worked examples. |
| Workflows | Collecting on an invoice, polling for settlement, reconciling a period, watching usage and credit. |
| Usage ingestion | The write half of metered billing: metrics, pricing schemes, events, inbound webhooks, and the price preview. |
| Webhooks | The billing event catalog: invoice lifecycle and credit events, signing, retries. |
| Code examples | Paging and retry loops in Python, Node, Go and shell; generating a typed client. |

## What v1 does, and what it does not

The line runs between **documents** and **metering**, not between reads and
writes.

**Every object this API lists can also be created through it.** The line
that remains is between *creating* a document and *editing* one: contracts
are amended by appending a version, invoices are corrected by issuing a credit
note, and neither is ever changed in place.

**Six surfaces are writable.**

- **Contracts.** `POST /contracts` creates one in `draft` for a customer you
  own (with a required `Idempotency-Key`), and
  `POST /contracts/{contractId}/amendments` appends a version — a status move,
  new dates, a replaced component set — guarded by the `expectedBaseVersion`
  you last read. Closing a contract does not cancel its subscriptions: cancel
  those first — see **Contracts**.
- **Invoices.** `POST /invoices` composes, numbers and (by default) sends a
  document in one call, with a required `Idempotency-Key`; it is immutable
  from the moment it is issued — see **Invoices**. `POST /invoices/{invoiceId}/send`
  raises or re-raises the collection link.
- **Subscriptions.** `POST /contracts/{contractId}/subscriptions` creates one
  on a contract you own (with a required `Idempotency-Key`), and
  `/subscriptions/{subscriptionId}/cancel`, `/pause`, `/resume`, `/end-trial`
  and `/change-plan` drive its lifecycle — see **Subscriptions**. Marking a
  subscription past-due or recovered is not yours to do: that is the
  collection loop's verdict.
- **Credit top-ups.** `POST /contracts/{contractId}/credits/top-up` grants a
  credit lot on a contract you own, with a required `Idempotency-Key` whose
  body *is* compared — see **Credits**. It is the only way credit enters the
  ledger; the deductions, expiries and reversals that take it out are the
  engine's rows.
- **Customers.** `POST /customers` resolves one of your customers by e-mail,
  creating the record when the address is new — the `sellerCustomerId` every
  contract, credit grant and invoice refers to. No `Idempotency-Key`: the
  address is the key, so the call always answers `200`.
- **Metering**, described in full two paragraphs down.

Read-only does not mean static. On a seller who has them enabled, background
passes issue invoices for due subscription terms and closed usage, and charge
sent invoices against the customer's saved card. So a list can grow and a
document can reach `paid` with no call of yours — see **Sandbox & testing**
for the switches that decide it.

**Metering is fully writable.** Registering a metric, pricing it, sending
events and previewing what a configuration charges are all part of this API —
see the two paragraphs below and the **Usage ingestion** guide. Nothing about
metered billing requires the dashboard.

Issued documents stay immutable either way: corrections are new credit notes,
never edits.

**Billing emits webhooks** — `InvoiceIssued`/`Sent`/`Paid`/`Overdue`/`Voided`/
`WrittenOff` and `CreditApplied`/`CreditDepleted` — through the same signed
delivery pipe as payment events: one receiver handles both. See the
**Webhooks** guide. Polling `GET /invoices` remains the right tool for
reconciliation.

**Metered usage is reported under `/billing-api/v1/usage`**: register a
metric, price it, then send events singly, in batches or by CSV. That is what
`GET /billing-api/v1/contracts/{contractId}/usage` reads back. The **Usage
ingestion** guide documents it end to end.

Two related surfaces are documented separately and are **not** duplicated
here:

- **Payments API** (`/payments-api/v1`) — payments, payouts, wallets,
  webhooks, and **top-up invoices**.
- **Payment Links API** (`/api/v1/payment-links`) — standalone payment-link
  management. Billing invoices create their own collection links via
  `POST /billing-api/v1/invoices/{invoiceId}/send`; the resulting hosted
  payment page URL is returned on the invoice itself.


# Overview

## Resources

| Resource | What it is |
| --- | --- |
| Contract | The commercial agreement between you and one of your customers. Invoices, subscriptions, usage and credits all hang off a contract. |
| Invoice | An immutable, numbered billing document. Once issued it is never edited — corrections are separate credit-note documents. |
| Subscription | A recurring billing relationship on a contract: cadence, price, trial, pause and cancellation state. |
| Usage | Metered consumption aggregated per contract and billing period, with plan allowances and caps. |
| Credits | Stored-value balances (prepayments, promotional credit) and their append-only ledger. |

## Versioning

The path prefix (`/billing-api/v1`) is the API version. Backwards-compatible
additions (new fields, new endpoints) happen within a version; breaking
changes get a new prefix. Unknown response fields must be ignored by clients.

## Changelog

The [Billing API Changelog](https://api.fynex.ai/billing-api/v1/docs/changelog.md)
records public changes, deprecations, replacements and removal dates.

## Identifiers

Billing resources use numeric `id`s (`int64`), unique per resource type.
Contract, invoice, and subscription ids are safe to store and to use in URLs.
Your tenant identity is derived from the API key — it is never passed as a
parameter, and objects belonging to other sellers are indistinguishable from
missing ones (`404`).

## Immutability

Issued invoices are append-only legal records. There is no update or delete:
a wrong invoice is voided or corrected by a credit note, and both documents
remain visible in listings. Credit ledger entries are likewise append-only.


# Concepts

Five objects, and the relationships between them are not obvious from their
names. Read this once and the rest of the reference follows; skip it and the
most likely mistake is inventing a call that cannot exist.

```
                        ┌──────────────┐
                        │   Customer   │  who you bill
                        └──────┬───────┘
                               │ one customer, many contracts
                        ┌──────▼───────┐
                        │   Contract   │  the agreement: currency, term
                        └──────┬───────┘
              ┌────────────────┼────────────────┐
              │                │                │
     ┌────────▼──────┐  ┌──────▼──────┐  ┌──────▼──────┐
     │ Subscription  │  │    Usage    │  │   Credits   │
     │ recurring fee │  │ metered qty │  │ stored value│
     └────────┬──────┘  └──────┬──────┘  └──────┬──────┘
              │                │                │
              └───────┬────────┘                │ drawn down at issue
                      │ billed by a run          │
               ┌──────▼───────┐                  │
               │   Invoice    │◄─────────────────┘
               │  immutable   │
               └──────────────┘
```

**Contract is the anchor.** Everything else hangs off one. A subscription, a
usage meter, a credit balance and an invoice all name a contract, and the
contract carries the currency they must all agree on.

**Invoice is a document, not a record you edit.** Once issued it has a number
and is immutable. A correction is a new document — a credit note — and both
stay visible in every listing. There is no `PATCH` and no `DELETE`, which
means an agent cannot be told to "fix" an invoice: the only correct move is
to issue a correcting one.

## Three questions the object names do not answer

### Can an invoice exist without a contract?

**No.** Every invoice names a contract, and issuing one without it is refused
before anything is written.

**But you do not have to create the contract first.** An ad-hoc invoice — a
one-off with `origin: adhoc` — provisions what it needs from the bill-to you
type: the customer is found or created by email, and a live contract in the
same currency is reused if one exists, created if not. So a one-off stays one
call. Send the same recipient a second one-off and it lands on the same
customer and the same contract rather than minting duplicates.

This provisioning is deliberately **only** for ad-hoc documents. A usage or
recurring invoice with no contract is a caller mistake, and inventing a
contract for it would detach that revenue from the agreement it belongs to.

### Does a subscription generate invoices automatically?

**No — not by itself.** Two different mechanisms are easy to confuse:

- The **lifecycle pass** advances a subscription's *state* on its dates: a
  trial activates, a cancellation takes effect after its notice period, a
  fixed end expires it, and a term that has rolled over gets the term now in
  force. It issues no documents at all.
- The **billing run** raises the money. It bills each revenue model on a
  contract — recurring, then usage, then project work, then any one-time line
  — each through its own engine, each producing its own document so the
  invoice's `origin` stays truthful.

So a subscription's period turning over does not, on its own, produce an
invoice. Something has to run the billing — and on a seller who has the
issuance passes switched on (`billing.recurring.invoice_pass`,
`billing.usage.invoice_pass`), that something is a schedule, not a person.
Both default to off. Write your integration so a document appearing without
you is normal, not an anomaly.

**Double billing is prevented by claims, not by memory.** A subscription
period lands on a unique `(subscription, period_start)` row, usage lines and
project work flip guarded status columns, and every claim's invoice carries
its id. Re-running a billing run finishes only what is missing; an ambiguous
commit is repaired by looking the claim up, never by issuing again. A model
that fails does not roll back a sibling's document — a numbered invoice
cannot be un-issued — and the run reports per model what happened.

### Do credits apply before or after tax?

**After.** Credit is a payment method, not a discount. It draws down the
invoice's grand total — the amount *including* tax — and never reduces the
taxable base. The tax the document reports is the tax on the full price,
whatever the customer's balance was.

Two consequences worth knowing before you model this:

- A credit-funded invoice is neither *expected* nor *received* cash. It is its
  own thing, and reporting treats it that way.
- **An invoice collected by bank transfer draws no credit at all.** The buyer
  was told to wire the grand total; shrinking the collectible underneath that
  instruction would make every full-face wire arrive as an overpayment. The
  balance stays on the ledger for the next link-collected document.

## Three words that are one letter of confusion apart

The platform has three roles, and two of their field names differ by a single
word while meaning entirely different things:

The direction of the money is what separates them:

```
   Customer  ──── pays ────►   Seller   ──── pays ────►   Payee
 sellerCustomerId            sellerAccountId             payeeId
 your buyer                  YOU, the key holder         who you disburse to
 Billing API                 both APIs                   Payments API
```

| Word | Who | Lives on |
| --- | --- | --- |
| **Seller** (`sellerAccountId`) | **You** — the account holder the API key belongs to. Every object in both APIs is scoped to one. | Both APIs |
| **Customer** (`sellerCustomerId`) | Who **pays you** — the party your invoices are addressed to. | This API |
| **Payee** (`payeeId`) | Who **you pay** — a counterparty receiving money through split payments and payouts. | Payments API |

`sellerAccountId` and `sellerCustomerId` look like siblings and are not: the
first is your own identity, the second is your customer's. A marketplace
operator is all three sentences at once — they **are** a Fynex seller, they
**have** customers who pay invoices, and they **have** payees who receive
splits and payouts.

If you think in the words *vendor*, *merchant* or *supplier*: the merchant
running the account is the **seller**; a vendor or supplier you disburse money
to is a **payee**; the buyer you bill is a **customer**. The settlement-trail
endpoint (`GET /invoices/{invoiceId}/settlement`) is where the two APIs meet:
a customer's payment on this side becomes a payee's payout on the other.

## Where money and quantities live

| Thing | On the wire |
| --- | --- |
| An amount of money | Integer **minor units**, field ends `Minor` — `4999` is €49.99 |
| A rate or percentage | Integer **basis points**, field ends `Bps` — `275` is 2.75% |
| A metered quantity | Decimal **string** — `"1250.5"` |
| A per-unit rate | Decimal **string** in MAJOR units — `"0.004"`, the one deliberate exception, because a rate is routinely finer than a minor unit |

Everything on a contract shares that contract's currency. There is no
conversion anywhere in this API: a mismatch is refused rather than converted.


# Errors

Errors are JSON with a single field:

```json
{"error": "invoice not found"}
```

(Authentication failures from the gateway layer may answer with a plain-text
body; treat any non-2xx as failed regardless of body shape.)

Every response, including `401`, `429`, and server errors, carries
`X-Request-Id: req_<uuid>`. Include this value in a support request; it is the
safe correlation handle for a request, not your API key or customer data.

| Status | Meaning |
| --- | --- |
| `400` | The request is malformed: an unparseable id, an unknown filter value, an invalid parameter. |
| `401` | Missing or invalid API key. |
| `403` | The key is valid but the seller account is not active. |
| `404` | The object does not exist — or belongs to another seller. |
| `422` | The object exists but the action is not applicable to it (for example, sending an invoice that is collected by bank transfer). |
| `429` | Rate limit exceeded. Honour `Retry-After` and the `X-RateLimit-*` headers. |
| `501` | The capability is not enabled for this deployment (for example, credits before stored value is switched on). |
| `500` | Server fault. Safe to retry idempotent (GET) requests with backoff. |

## Rate limiting

Requests are rate-limited per seller, on a budget dedicated to this API —
billing traffic and payments-api traffic do not throttle each other. The
default budget is **1000 requests per hour per seller**, with a burst of 100 so
a single client cannot spend the whole hour in one instant. The number is set
per environment, so read `X-RateLimit-Limit` rather than hard-coding it.

Responses normally carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
`X-RateLimit-Reset` (whole seconds until the budget refills), plus the current
standards-track structured fields `RateLimit-Policy` (`"seller";q=1000;qu="requests";w=3600`)
and `RateLimit` (`"seller";r=940;t=2100`). Both spellings carry the same numbers.
Note these are not the `RateLimit-Limit`/`RateLimit-Remaining`/`RateLimit-Reset`
triple from an earlier revision of that draft, which is not sent. Read them rather than
assuming a number: the budget is set per environment, and `X-RateLimit-Limit`
is the authoritative value for yours. Treat them as advisory — during a limiter outage requests are allowed through without the
headers, so a client that requires them will break exactly when the platform
is already degraded.

Three write operations carry an **extra** per-seller quota on top of that
budget, each in its own bucket, because each accepted call spends something
that cannot be handed back:

| Operation | Default quota | Why |
| --- | --- | --- |
| `POST /contracts` | 20 requests per rolling 24 hours (refusals and replays count) | Bounds how fast one key opens contracts; a create allocates a number from a series shared across sellers. |
| `POST /contracts/{contractId}/amendments` | 200 requests per rolling 24 hours (a stale `expectedBaseVersion` counts) | Every accepted amendment appends a contract version — up to 100 line items — to a history that is append-only and has no delete. |
| `POST /invoices` | 500 requests per rolling 24 hours (refusals and replays count) | Issuing allocates a number from your own gapless series, so a runaway loop burns your month of numbers. |

All three refuse with the same `429`, `Retry-After` and `X-RateLimit-*`
headers as the surface-wide limit.

A rejected request answers `429` with `Retry-After` in seconds. Wait that
long — retrying sooner only deepens the overage.

Bulk work is what actually hits this. Reconciling a month walks pages of up
to 100 invoices and may pull a PDF per document, so a few hundred requests in
one burst is normal. Pace bulk exports (a short sleep between pages costs far
less than being throttled mid-walk), and if a legitimate workload cannot fit
the budget, ask Fynex to raise it rather than working around it with parallel
keys.

### When the write quotas fail closed

Unlike the surface-wide budget, those three operations — `POST /contracts`,
`POST /contracts/{contractId}/amendments` and `POST /invoices` — **fail
closed**. While the limiter itself is unreachable they answer `503` with
`Retry-After` instead of letting an uncounted burst of writes through. Nothing
was created, amended or issued, so the retry is safe: wait the header out and
repeat the call with the **same `Idempotency-Key`**, which is what stops the
retry from opening a second contract or issuing a second document. Amendments
carry no `Idempotency-Key` — resend the same `expectedBaseVersion`, and a
version that did land answers `422` rather than appending a duplicate.

Two things this does *not* mean. It is the limiter FAILING, not the quota
being absent: a deployment that switches a quota off (its request budget set
to `0`) is simply unmetered on that operation, exactly as before, and never
answers `503` for this reason. And the quotas count **requests**, not
successes — a refusal or a replay spends one too, so a client retrying a `400`
in a loop can exhaust its day without ever writing anything.


# Pagination

Every list endpoint pages by keyset, with the same two parameters:

- `limit` — page size, 1–100 (default 20).
- `cursor` — where to continue from. Omit it (or pass `0`) for the first page.

Each page reports how to continue:

```json
{
  "invoices": [ ... ],
  "hasMore": true,
  "nextCursor": 4177
}
```

Pass `nextCursor` back as `cursor` until `hasMore` is `false`. That loop is
the same code on every list in this API.

Keyset paging is stable under concurrent inserts: new rows appear on the first
page of a fresh iteration and never shift the pages of an iteration already in
flight.

## Direction is a property of the list, not of the parameter

The lists do not all walk the same way, and that part is deliberate:

| List | Order | A cursor means |
| --- | --- | --- |
| `/invoices`, `/subscriptions` | Newest first | ids **below** the cursor |
| The credit ledger on `/contracts/{contractId}/credits` | Newest first | ids **below** the cursor |
| `/contracts` | Ascending contract id | ids **above** the cursor |

Contracts walk forward because the id is a stable identity to iterate, not a
recency ranking. What used to differ as well was the parameter NAME —
`beforeId` on some lists, `afterId` on others — so anyone who wrote a working
loop for invoices wrote a broken one for contracts. `cursor` and `nextCursor`
are the same on all of them; only the documented order changes.

The credit ledger pages the `entries` array only: the balances in the same
response are always complete. Seller-wide credit balances are one row per
currency and credit type, so that list is genuinely bounded and does not page.

## The original parameter names

`beforeId` / `nextBeforeId` and `afterId` / `nextAfterId` still work,
unchanged, on the lists that had them, and every response still carries the
original field beside `nextCursor` with the same value. Existing integrations
need no change. Sending both `cursor` and the original name with **different**
values is a `400` rather than a silent choice between them.

## Dates

`issuedFrom` and `issuedTo` accept a calendar date (`2026-08-01`) or a full
RFC 3339 timestamp. A bare date is read as **UTC midnight**, and the range is
half-open — `issuedFrom` inclusive, `issuedTo` exclusive — so adjacent months
never double-count a document. If your books close in a non-UTC zone, send the
timestamp form with your offset rather than the bare date.

## Amounts

All monetary amounts are **integers in the currency's minor units**
(`grandTotalMinor: 12050` is €120.50 for a EUR document). Quantities and tax
rates are decimal strings. Never parse amounts as floating point.


## Retention and numbering

**Stored events are retained indefinitely.** Nothing prunes them — there is no
retention window on ingested usage and no job that deletes it, so an
idempotency key you used a year ago is still recognised and resending that
event is still a no-op. Documents are the exception: invoices and credit notes
carry a statutory retention period per jurisdiction, which is a legal minimum
on how long they are kept.

An invoice number is `<agreement>-<YYMM><order>`: your agreement number, a
dash, the issue month as `YYMM`, and a three-letter order within that month
(`AAA`, `AAB`, …) — for example `UK2607AA-2608AAB` is the second document
issued in August 2026 under agreement `UK2607AA`. The series is allocated
inside the issuing transaction, so it is gapless per month, and it is shared
with top-up invoices so the two document families can never collide.
