Skip to content

Errors and rate limits

Every response outside the 2xx range uses the same envelope:

{
"error": {
"code": "invalid_request",
"message": "Invalid request",
"requestId": "9f0c1c2e-…",
"docs": "https://limitry.com/docs/errors/#invalid_request",
"details": [{ "path": "name", "message": "Required" }]
}
}
Field What it is
code One of a small, stable set of codes. Branch on this.
message A sentence for people. It may change, so don’t parse it.
requestId Matches the X-Request-Id response header. Include it when you contact support.
details Only on validation errors: one entry per field, with its path and message.
docs A link to the code’s entry on this page.

We may add new codes over time. Treat a code you don’t recognise as internal_error.

401. The credential is missing, malformed, invalid, revoked or expired. Send Authorization: Bearer <key or token> with a working credential, and create a new one if it was revoked or has expired.

403. The credential is valid but isn’t allowed to do this. Either it doesn’t have the endpoint’s scope, or your role in the workspace doesn’t allow it, or a personal access token can’t act in the workspace named by X-Workspace. Use a credential with the access it needs; the message names the missing scope.

Two more cases come from test and live. An API key always acts in its own environment, so a request whose X-Environment header names another one gets 403. A test key can read workspace-wide settings but not change them, so adding a webhook endpoint with a test key gets 403 with the message “A test key cannot change workspace-wide settings”.

404. The resource doesn’t exist, or isn’t yours to see. Check the path and the id; the API reports an id from another workspace as not found.

405. The path exists, but not with this method. Use one of the methods in the Allow response header.

400. The request didn’t pass validation. details lists each invalid field with its path and message. An X-Environment header that names an environment other than test or live gets this code too.

Two cases come from limits. A change to what a limit counts (its subject, action, unit, window, every, start or rolling) gets 400: create a new limit instead. A new limit, or a rename, whose name an active limit already has gets 409 with this code: send a name of its own.

429. The credential has used its quota (see API rate limits). Wait the number of seconds in Retry-After, then retry with backoff.

402. The workspace has used its plan’s monthly allowance for what this request needs. The message says which allowance and when it resets, and the workspace’s owners got an email and a notification at 80% and 100%. Upgrade the plan under Settings → Billing, or wait for the month to reset; retrying sooner gets the same answer.

Test checks have their own allowance and no grace. Past it, a check made with a test key gets 402 with a message saying the workspace has used its plan’s test checks for the month. Live checks are not affected.

409. This Idempotency-Key was already used for a different request. Use a new key for a new operation, and reuse a key only to retry the same request (see Idempotency).

409. A request with the same Idempotency-Key is still running. Wait the number of seconds in Retry-After, then retry with the same key.

503 with Retry-After: 1. Some counts are moving between Limitry’s servers: a very busy workspace’s are spread over more servers as its traffic grows, and a workspace’s move to a new region (Settings → Region) carries them over. While some of them move, which takes a few seconds, checks, commits and releases on those subjects answer moving. Nothing was charged: anything held for the request is given back, or finished by its retry. Retry with the same Idempotency-Key after Retry-After; the SDKs do this for you for up to 30 seconds before treating Limitry as unreachable.

503 with a Retry-After header (seconds). Limitry is deciding too many checks for these subjects at once, so it answered this one at once instead of making it wait in line. Nothing was charged: anything held for the request is given back, or finished by its retry. Wait the number of seconds in Retry-After, then retry with the same Idempotency-Key, backing off with random jitter if it happens again. The SDKs do this for you, within the same 30 seconds as moving, before treating Limitry as unreachable. This is about Limitry’s load, not your quota: it isn’t rate_limited, and it isn’t a refusal.

500, or 503 with a Retry-After header when the failure was momentary. Something failed on our side. A 503 saying the check “may have been charged” means Limitry couldn’t confirm it in time: retry with the same Idempotency-Key to get its answer, never without one. Retry with backoff (send an Idempotency-Key and a retried check never counts twice), and if it keeps happening, contact support with the requestId.

A refused check isn’t an error. It answers 200 with "allowed": false, and reason.code says why. The SDKs throw the first two as LimitReached.

A limit the check met is used up. reason.limitId names it, and retryAfterSeconds says when it could be allowed (null for a balance, which only credits raise). Your webhook endpoints also get limit.reached the first time a limit refuses a subject in a window.

The subject already has 1,000 open reservations. Commit or release some, or wait for them to expire.

Not a code from the API: the SDKs throw Unavailable when they get no decision from Limitry (no answer in time, a 429, a 5xx, or a 402 plan_limit_reached) and a limit’s fail mode is closed. With open, the work runs instead. See How Limitry works.

Every authenticated request counts against a quota for its credential: per API key on the workspace endpoints, per token on /v1/user/*. A personal access token has a separate quota in test and in live. When the quota runs out, the API answers:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

with code: "rate_limited". Wait for Retry-After, then retry with backoff. Because limits are per credential, one busy integration doesn’t slow down another key’s requests.