Errors
The error body, status codes, request ids and rate limiting.
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.