Errors
Every non-2xx response on /v1 uses one envelope. Branch on errors[0].code, never on the message text: messages are for people and can change, codes are stable.
The error envelope
{
"status": "error",
"data": null,
"errors": [
{
"code": "IDEMPOTENCY_KEY_CONFLICT",
"message": "Idempotency-Key was already used with a different request body",
"parameter": "idempotency-key"
}
]
}
statusis always"error"anddatais alwaysnull.errorshas at least one entry.codeis one of the values in the table below;parameternames the offending field or header when the error can be pinned to one, otherwise it isnull.
Request ids
Every response carries an x-request-id header. If you send a well-formed x-request-id with the request, it is echoed back; otherwise one is generated. Log it with each call and quote it when you contact support at hello@leeri.ai.
How to react
| Situation | What to do |
|---|---|
429 with RATE_LIMIT_EXCEEDED or PROVIDER_CAPACITY_EXCEEDED | Wait for the number of seconds in the Retry-After header, then retry the same request. See Rate limits and quotas. |
503 with REPORT_RENDERING | Retry after the Retry-After interval; the report link stays valid. |
503 with RULESET_UNAVAILABLE, RATE_LIMIT_STORE_UNAVAILABLE, SERVICE_DRAINING or UPSTREAM_UNAVAILABLE | No Retry-After is sent. Retry with exponential backoff and jitter. Nothing is charged for a request that was refused. |
500 with INTERNAL_ERROR | Retry with backoff and the same Idempotency-Key; if it keeps failing, contact support with the x-request-id. |
409 with IDEMPOTENCY_REQUEST_IN_FLIGHT | The first request with this key is still running. Retry the same request after a short pause. See Idempotency. |
409 with IDEMPOTENCY_LEASE_LOST | Another retry took over the key while this request was running. Send the same request again to read the recorded result. |
422 with PROVIDER_CAPACITY_REQUEST_TOO_LARGE | Never retry unchanged: the request is larger than the capacity can ever hold. Split it into smaller requests. |
402 with INSUFFICIENT_BALANCE | Quota and prepaid balance are both used up. Retrying does not help until the balance is topped up. |
Any other 4xx | The request itself is wrong (VALIDATION_ERROR, ADDRESS_REQUIRED, UNSUPPORTED_ASSET, NOT_FOUND, SCOPE_ERROR and so on). Fix it; retrying unchanged returns the same error. |
A request that fails with a 4xx or 5xx status is not recorded under its Idempotency-Key, so a retry runs again.
Error codes
The table is generated from the API contract this documentation is pinned to, so it always lists exactly the codes the API can return. Each code is a link target: for example, ./errors.mdx#ADDRESS_REQUIRED points at its row.
| Code | HTTP status | Condition |
|---|---|---|
VALIDATION_ERROR | 400 | The request body, params or a header failed Zod validation — an unknown field, a value outside its declared bounds, or a shape the endpoint cannot parse. |
AUTH_ERROR | 401 | The Authorization header is missing, malformed, or does not resolve to an active, unexpired API key. |
SCOPE_ERROR | 403 | The API key authenticated but does not carry a scope this route requires. |
TENANT_BOUNDARY | 403 | A data-layer read or write detected a cross-tenant reference — the platform-level RLS guard's own rejection, not a route-level check. |
NOT_FOUND | 404 | No resource with this id exists for the authenticated tenant and key environment (live/sandbox) — including a cross-tenant or cross-environment id, which deliberately answers identically to an unrelated, nonexistent one. |
IDEMPOTENCY_KEY_CONFLICT | 422 | The same Idempotency-Key was already used against this endpoint with a different request body. |
INSUFFICIENT_BALANCE | 402 | The tenant has exhausted both its remaining quota and its prepaid balance for the current billing period. |
PROVIDER_CAPACITY_EXCEEDED | 429 | The global or tenant provider-work capacity is full. No check or billing hold is accepted; retry after the interval named in Retry-After. With the durable provider flow each live check holds up to 10 provider slots until its lookups are frozen; the default tenant capacity is 1000 slots, i.e. 100 live checks awaiting provider work at once, which always admits one full 100-item batch. |
PROVIDER_CAPACITY_REQUEST_TOO_LARGE | 422 | This single request needs more provider-work slots than the tenant or global capacity holds in total, so it can never be admitted: retrying does not help and no Retry-After is sent. No check or billing hold is accepted; split the request into smaller ones. |
RULESET_UNAVAILABLE | 503 | No published scoring ruleset was reachable when the check ran — the check is never charged when this fires. |
RATE_LIMIT_STORE_UNAVAILABLE | 503 | The rate-limit store is unreachable. The request is refused rather than served with no limit at all (fail-closed). |
RATE_LIMIT_EXCEEDED | 429 | A rate limit — IP, caller, failed-authentication, the per-tenant rederive or address-report limiter, or the dedicated documentation limiter — was exceeded. Retry after the window named in Retry-After. |
SERVICE_DRAINING | 503 | The instance is mid-shutdown and refusing new work; retry against another instance. |
UPSTREAM_UNAVAILABLE | 503 | A dependency this request calls synchronously (DNS resolution for a webhook URL, a provider lookup) failed or timed out. |
REPORT_RENDERING | 503 | The evidence report PDF is being generated and was not ready within the wait budget. Retry after the interval named in Retry-After; the link stays valid. |
REPORT_RENDER_FAILED | 503 | The evidence report PDF could not be generated. The message carries the short report ID to quote to support; opening the link again after a short cooldown retries the render. |
UNSUPPORTED_ASSET | 422 | The asset symbol is not in the screening engine's supported registry. |
ADDRESS_REQUIRED | 422 | A transaction check left address out for an asset that cannot derive it (anything but TRX, ETH, BNB and BASE), or a sandbox key left it out of any transaction check. Send the customer's address in the transaction. |
TRANSACTION_UNRESOLVED | 422 | A transaction check left address out and the transaction could not be read from its chain (no such transaction on that network, an undecodable recipient for a deposit, or the chain data source did not answer in time). Nothing is accepted or charged; check the hash and network, retry, or send the address. |
PAYLOAD_TOO_LARGE | 413 | The request body exceeds the configured body-size limit. |
IDEMPOTENCY_REQUEST_IN_FLIGHT | 409 | A request carrying this Idempotency-Key is already being processed by another request; retry shortly. |
IDEMPOTENCY_LEASE_LOST | 409 | This request's idempotency lease was taken over by a concurrent retry before it finished; retry to read the recorded, published result. |
CHECK_FAILED | 422 | The screening engine ran to completion but the check itself terminated in failure (e.g. an unrecoverable downstream provider error). |
INTERNAL_ERROR | 500 | An unclassified server fault. Every 5xx this taxonomy does not otherwise name (and every internal-only CustomError code) collapses to this one on the wire — the raw message never leaves the server log. |
The AMLBot-compatible endpoint
The AMLBot-compatible endpoint keeps the convention of the API it reproduces: it answers HTTP 200 in every case and reports failures in its own result: false envelope; see Responses in the migration guide. The envelope and codes above apply to /v1.