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