Errors and rate limits
Errors come in two shapes. Errors from your library use a structured envelope with a stable code. A small set of errors from the API front door (authentication, rate limiting, account state) use a plain message. Handle both.
The error envelope
Section titled “The error envelope”Every non-2xx response from your library looks like this:
{ "error": { "code": "LOCKED", "message": "Lineage 'guid:6F9619FF8B86D011B42D00C04FC964FF' is locked by another user.", "details": { "holder": { "id": 7, "name": "<user>", "display_name": "Sam Rigger" }, "lock": { "id": 31, "lineage_id": "guid:6F9619FF8B86D011B42D00C04FC964FF", "active": true } } }}Switch on code, not on message. Messages are written for people and can change. details is always an object (often empty) and carries machine-readable context where an endpoint documents it.
Front-door errors
Section titled “Front-door errors”Requests rejected before they reach your library have a simpler body:
{ "error": "Rate limit exceeded for this key. Back off and retry." }| Status | Cause |
|---|---|
401 |
Missing, unknown or revoked key, or the key’s creator left the workspace. |
402 |
The workspace is suspended for billing. Fix billing in the dashboard. |
403 |
A read-scoped key attempted a write, or the key belongs to a different workspace. |
404 |
The workspace has no library provisioned yet. |
413 |
An MCP request body is missing Content-Length or is larger than 1 MB. |
429 |
Rate limit or agent-call ceiling reached. See below. |
503 |
The API or your library is briefly unavailable, or still being prepared. Honor Retry-After when present. |
A safe client checks whether error is an object (envelope) or a string (front door).
Error codes
Section titled “Error codes”These are all the stable codes the envelope can carry. Statuses are the usual ones; details keys are listed where the API sets them.
| Code | Status | Meaning |
|---|---|---|
UNAUTHORIZED |
401 | Missing, invalid or expired credential. Also returned by device sign-in for an unknown or already-used device code. |
FORBIDDEN |
403 | Authenticated but not allowed: admin-only endpoint, not the owner of the thing you are changing, or the workspace is read-only (details.reason: read_only or ingest_blocked). |
RATE_LIMITED |
429 | Too many sign-in attempts. details.retry_after_seconds says when to retry. |
SETUP_ALREADY_DONE |
409 | Not used by hosted workspaces. |
DEVICE_PENDING |
400 | Device sign-in not approved yet; keep polling (details.interval). |
DEVICE_SLOW_DOWN |
429 | Device sign-in polled too fast (details.interval). |
DEVICE_EXPIRED |
400 | The device code expired; start a new sign-in. |
DEVICE_DENIED |
400 | The person denied the device sign-in. |
NOT_FOUND |
404 | Generic not found: unknown route, missing thumbnail or preview, unknown lock, project or preview artifact. |
ENTRY_NOT_FOUND |
404 | No entry with that id. Bulk calls list the missing ids in details.missing. |
OBJECT_NOT_FOUND |
404 | No object (immutable version) with that id. |
LINEAGE_NOT_FOUND |
404 | No lineage with that id in the catalog. |
PACK_NOT_FOUND |
404 | No pack with that id. |
TAG_NOT_FOUND |
404 | No tag with that id. |
COMMENT_NOT_FOUND |
404 | No comment with that id. |
ANNOTATION_NOT_FOUND |
404 | No annotation with that id. |
COLLECTION_NOT_FOUND |
404 | No collection with that id. |
JOB_NOT_FOUND |
404 | No pull job with that id. |
INGEST_NOT_FOUND |
404 | Reserved for server-side pack ingestion. |
USER_NOT_FOUND |
404 | No member with that id (reviewers, assignees, member profiles). |
TOKEN_NOT_FOUND |
404 | Not used by hosted workspaces. |
WEBHOOK_NOT_FOUND |
404 | No webhook with that id. |
REVIEW_REQUEST_NOT_FOUND |
404 | No review request with that id. |
VALIDATION |
422 | The request is malformed or breaks a rule: bad field, bad cursor, too many items, invalid media. details.errors lists field failures for body and query validation. |
CONFLICT |
409 | Uniqueness or state conflict: duplicate name (details.existing_id for tags), stale face (details.current_object_id), delete blocked because the entry was pulled, an open review round exists (details.open_request_id). |
LOCKED |
409 | Another person holds the check-out lock (details.holder, details.lock). |
DENYLISTED |
403 | The blob’s hash is on the workspace denylist. |
HASH_MISMATCH |
409 | Uploaded bytes do not match the declared hash. |
BLOB_MISSING |
409 or 404 | A commit references bytes that were never uploaded (details.missing, 409), or a download asks for an unknown hash (404). |
LINEAGE_CONFLICT |
409 | The object belongs to a different lineage than the entry (restore, pinned pull, version-pinned comment or annotation). |
PROBABLE_LOST_STAMP |
none | Never returned as an error. Surfaces as probable_lost_stamp: true on push items. |
STALE_FACE |
409 | A push would replace a current version you did not observe (details: lineage_id, expected_face_object_id, actual_face_object_id, content_hash). Re-sync before pushing again. |
ENGINE_TOO_OLD |
409 | A pull job’s closure contains assets saved by a newer engine than the claimant (details.objects). |
JOB_STATE |
409 | Illegal pull-job transition, lost claim, or stale claim token (details.status). |
DRY_RUN_REQUIRED |
409 | An admin maintenance sweep was called without a recent dry run. |
UPGRADE_REQUIRED |
426 | The request declared a different API major version. |
UNAVAILABLE |
503 | The library is temporarily unable to take the request (for example details.reason: "policy_unavailable" right after it starts). Retry. |
INTERNAL |
500 | Unexpected error. Details stay in server logs; retry once, then contact support with the time of the request. |
EMBEDDINGS_UNAVAILABLE |
503 or 409 | Visual search is temporarily unavailable (503), or the entry you asked for similar props to has no image embedding yet (409, details.entry_id). |
Device sign-in can also return INVALID_REQUEST (400) when device_code is missing.
Rate limits
Section titled “Rate limits”The API front door limits each API key over a sliding one-minute window:
| Traffic | Limit per key |
|---|---|
| Most requests | 240 per minute |
Upload and media processing: /push/*, /blobs/*, POST /entries/media/presign and /finalize, and /entries/{id}/preview, /thumbnail and /metadata |
2,400 per minute |
The two budgets are separate, so a large upload does not starve interactive calls made with the same key. Over the limit you get:
HTTP/1.1 429 Too Many RequestsRetry-After: 30Content-Type: application/json; charset=utf-8
{"error": "Rate limit exceeded for this key. Back off and retry."}Signing in is limited separately: see device sign-in.
What counts toward plan allowances
Section titled “What counts toward plan allowances”REST API calls never count toward a plan allowance. Search, browsing, downloads, thumbnails, uploads and every other REST request are uncounted (they are still rate limited).
The only counted requests are MCP tool calls (tools/call on /mcp), which count against the workspace’s monthly agent-call allowance. When that allowance is used up, MCP tool calls return an AGENT_CALL_LIMIT tool error (a JSON-RPC batch gets a plain 429 with used and limit in the body); searching from the apps and the REST API is unaffected. See MCP errors and limits. See Plans and limits.
Retrying
Section titled “Retrying”| Response | Retry? |
|---|---|
429 |
Yes, after Retry-After seconds (or details.retry_after_seconds). Add jitter if you run several workers. |
503 |
Yes, after Retry-After when present, otherwise with exponential backoff starting around 2 seconds. |
500 INTERNAL |
Once, then stop and report. |
409 codes |
No. The state changed; read it again and decide. JOB_STATE on a pull job means your claim is gone. |
4xx otherwise |
No. Fix the request. |
Uploads and pushes are designed to be retried safely: blob uploads are idempotent by hash, and a commit either registers the whole batch or nothing. See Push (uploads).
