# Errors

> Error envelope and codes.

## Envelope

All error responses share the same JSON shape:

```json
{
	"ok": false,
	"error": {
		"code": "rate_limited",
		"message": "Rate limit exceeded. Maximum 60 requests per minute."
	}
}
```

## Codes

| HTTP | Code | When |
| ---- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400 | `invalid_json` | Request body was not valid JSON. |
| 400 | `invalid_request` | JSON parsed but failed schema validation. Carries `issues`. |
| 400 | (none) | No upstream status to report: the URL did not resolve to a supported provider, or the call never reached the bank. Read `error`. |
| 401 | `missing_key` | No `x-api-key` header. |
| 401 | `invalid_key` | Key not recognised. |
| 401 | `revoked_key` | Key was revoked. |
| 429 | `rate_limited` | Hit the per-minute request rate. Includes `Retry-After`. |
| 429 | `quota_exceeded` | Hit the plan's uncached verification cap. Carries `used` and `cap`, plus `Retry-After` to the window rollover. |
| 429 | `image_cap_reached` | Hit the plan's monthly screenshot read allowance. `POST /api/verify-image` only. |
| 429 | `ocr_daily_cap_reached` | The service-wide daily screenshot budget is spent. Rare, and not about your account. |
| 503 | `provider_down` | The bank is failing every health check we run, so we did not queue a lookup that had no answer in it. Includes `Retry-After`. Never counts against your cap. |
| 502 | (none) | The bank answered but the receipt failed validation. See `error` and the partial `receipt`. |

There is **no 504**. A call that times out or gives up waiting for a busy bank
has no upstream status code to report, so it comes back as a 400 with the
reason in `error`. See
[what a busy bank does](/docs/integration-flows#what-a-busy-bank-does-to-a-sync-call).

`POST /api/verify-image` can also return `ai_not_configured` (503, the
extractor is not set up on this deployment) and `detection_failed` (502, the
extractor ran but could not read a receipt reference out of the image).

## When a bank is down

We probe every upstream about every five minutes, from each of our egress
addresses. When the most recent round has a bank failing on **all** of them,
a verification for that bank returns `503 provider_down` straight away
instead of queueing a call that would spend two 20 s attempts to reach the
same conclusion.

```json
{
	"ok": false,
	"error": {
		"code": "provider_down",
		"message": "Telebirr is not answering us right now (every check has failed for 25 minutes). We did not spend a lookup on it. Try again in a few minutes."
	}
}
```

`Retry-After` holds 300 s, the probe interval — there is no point asking again
before we have looked again. A single address failing is not enough to trigger
this, because that looks like our IP being blocked rather than the bank being
down. [`GET /api/status`](/docs/health) shows the same picture per component.

## Retry guidance

- **`rate_limited`** wait the `Retry-After` interval, then try again.
- **`provider_down`** wait the `Retry-After` interval. Retrying sooner cannot
 work: nothing is asked of the bank until our next probe round says it is
 answering again. It costs you nothing, so a queued job can simply try later.
- **`quota_exceeded`** and **`image_cap_reached`** do not retry on a timer.
 `Retry-After` can be days away, because the window is your plan's, not a
 minute. Upgrade, enable overage, or wait for the rollover.
- **502** and the busy-bank **400** are usually the bank rather than you.
 Retry after 1 to 2 s with jitter and treat three consecutive failures as a
 real outage. Do not retry a Siinqee reference at all: those links allow about
 five views in total, so a retry loop destroys the receipt. See
 [providers](/docs/providers).
- **401** never retry. Mint a new key from the dashboard.
- **400 `invalid_request`** never retry. The body is wrong.
