Fynex Billing API
What v1 covers, its resources, identifiers and versioning, with the errors and paging conventions, on one page.
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 hosts are listed in the OpenAPI document's servers block — this
deployment's own origin first, then the sandbox — and spelled out under 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, knowing when it is paid (webhook or poll), 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 /contractscreates one indraftfor a customer you own (with a requiredIdempotency-Key), andPOST /contracts/{contractId}/amendmentsappends a version — a status move, new dates, a replaced component set — guarded by theexpectedBaseVersionyou last read. Closing a contract does not cancel its subscriptions: cancel those first — see Contracts. - Invoices.
POST /invoicescomposes, numbers and (by default) sends a document in one call, with a requiredIdempotency-Key; it is immutable from the moment it is issued — see Invoices.POST /invoices/{invoiceId}/sendraises or re-raises the collection link. - Subscriptions.
POST /contracts/{contractId}/subscriptionscreates one on a contract you own (with a requiredIdempotency-Key), and/subscriptions/{subscriptionId}/cancel,/pause,/resume,/end-trialand/change-plandrive 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-upgrants a credit lot on a contract you own, with a requiredIdempotency-Keywhose 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 /customersresolves one of your customers by e-mail, creating the record when the address is new — thesellerCustomerIdevery contract, credit grant and invoice refers to. NoIdempotency-Key: the address is the key, so the call always answers200. - Metering, described in full two paragraphs down.
Writable by you does not mean written only by you. Where the deployment has
them switched on, 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, CreditApplied/CreditDepleted, the ten BillingSubscription*
lifecycle events and UsageAlertFired — 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 viaPOST /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 records public changes, deprecations, replacements and removal dates.
Identifiers
Billing resources use numeric ids (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.
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
originstays 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:
| 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:
{"error": "invoice not found"}
Authentication failures add a code: missing_api_key or invalid_api_key
on a 401, account_not_active on a 403.
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.
The metering routes under /billing-api/v1/usage are not counted against
this per-seller budget and return no X-RateLimit-* headers: bound your own
concurrency and send events in batches rather than one call per event — see
Usage ingestion.
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
Paged list endpoints use keyset pagination, with the same two parameters:
limit— page size, 1–100 (default 20).cursor— where to continue from. Omit it (or pass0) for the first page.
Each page reports how to continue:
{
"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 following bounded registry/operational listings are deliberately unpaged; their endpoint descriptions state their ordering explicitly:
| List | Order | Why it is unpaged |
|---|---|---|
/usage/metrics |
Metric name ascending | One row per current metric name; bounded by the seller's registry |
/usage/dead-letters |
Oldest first (receivedAt, then id) |
Bounded operational queue response |
/usage/webhook-endpoints |
Endpoint id descending (newest first) | Bounded by the seller's registered endpoints |
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 midnight in the seller's
timezone (the one on the organization profile, UTC when none is set), the
same calendar the invoices are dated on, and the range is half-open —
issuedFrom inclusive, issuedTo exclusive — so adjacent months never
double-count a document. Send the timestamp form to filter on any other
boundary.
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.