Rate limits and errors
Rate limit headers, 429 handling, the stable error codes the API returns, and how the SDKs and CLI retry.
Rate limits
The API limits requests per token and, for anonymous calls, per IP. Responses subject to a limit carry:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests left in the window |
X-RateLimit-Reset | When the window resets, as a Unix timestamp in seconds |
Retry-After | On a 429: seconds to wait before retrying |
A request over the limit answers 429 with code rate_limited. Wait for Retry-After and try again; do not retry in a tight loop. Authenticated requests get higher limits than anonymous ones, so send a token from servers and CI.
Some actions have their own per-user limits on top (commenting, for example), which answer the same 429.
Error format
{ "error": { "code": "not_found", "message": "Not found", "requestId": "..." } }details is present when there is more to say, for example the list of validation issues. Branch on code, never on message.
Error codes
Request errors
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_failed | The body, query or path failed validation; details lists each issue with its path |
| 400 | invalid_json | The body is not valid JSON |
| 400 | invalid_cursor | The pagination cursor is malformed |
| 413 | payload_too_large | The body is over the endpoint's limit (1 MB by default) |
| 415 | json_required | A cookie-authenticated write was not sent as JSON |
| 422 | invalid_package, unknown_tags, license_not_allowed, … | The request is well-formed but not acceptable |
| 429 | rate_limited | Too many requests; see Retry-After |
| 500 | internal | A bug on our side; the response carries only the requestId |
Permission and state errors
These come from the permission policy and are the same across the API, the website and the MCP server.
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated | Sign in or send a token |
| 403 | token_scope | The token lacks the scope this action needs |
| 403 | token_forbidden | No token may do this (for example minting tokens) |
| 403 | not_owner | Only the owner (or an admin) may do this |
| 403 | role_required | Needs a moderator or admin |
| 403 | account_suspended, account_banned | The account cannot make changes |
| 403 | blocked | The owner has blocked you |
| 403 | comments_locked | The thread is locked |
| 403 | edit_window_closed | Comments can be edited for 24 hours |
| 403 | eval_required | The label requires a passing eval run on the target version |
| 403 | version_yanked | A label cannot point at a yanked version |
| 403 | immutable, in_use, invalid_state | The item cannot change in this way right now |
| 404 | not_found | Missing, or something you are not allowed to see (private and draft items answer 404, not 403) |
| 410 | deleted | The item was deleted (a tombstone) |
Registry conflicts
| Status | Code | Meaning |
|---|---|---|
| 409 | slug_taken | You already have a prompt with that slug |
| 409 | no_changes | The commit is identical to the latest version |
| 409 | stale_base | Someone committed since the baseVersion you edited; details.latest has the current number |
| 409 | version_labelled | Move the labels off a version before yanking it |
| 409 | last_version | The only live version cannot be yanked; delete the prompt instead |
| 409 | no_draft | There is no draft to commit |
Retries in the SDKs and CLI
The TypeScript and Python SDKs (and the CLI, which uses the TypeScript SDK) retry for you:
429is retried for every method.5xx, network errors and timeouts are retried only for idempotent methods (GET,HEAD,PUT,DELETE), never forPOST, so a commit is never sent twice.- Backoff is exponential with full jitter: 500 ms times 2 to the attempt, capped at 30 seconds. A
Retry-Afterheader (seconds or an HTTP date) overrides it, capped at 60 seconds. - The default is 3 retries (
maxRetries/max_retries), with a 30 second request timeout.
Every failure surfaces as an IncantoryError with status, code, message, requestId, details and retryAfter (retry_after in Python). The CLI maps them to exit codes.