Skip to content
In this section

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 /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.

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 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 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.

many contractsbilled by a runbilled by a rundrawn at issueCustomerwho you billContractthe agreement: currency, termSubscriptionrecurring feeUsagemetered quantityCreditsstored valueInvoiceimmutable

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:

payspaysCustomersellerCustomerId · your buyerBilling APISeller (you)sellerAccountId · API key holderboth APIsPayeepayeeId · who you disburse toPayments 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:

{"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 pass 0) 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.