Error codes
Preview Every API error
carries a stable machine code, a broad type that maps to an HTTP status, a
param pointing at the offending field where relevant, and a doc_url that links
straight to the matching anchor on this page. Match on code in your client — it’s
the part that won’t change.
the error envelope
Section titled “the error envelope”{ "error": { "type": "not_found", "message": "site not found", "code": "site.not_found", "param": "site_id", "doc_url": "https://docs.managed.dev/reference/error-codes/#site.not_found" }, "request_id": "req_01J9F2KQ"}The doc_url anchor is built from the code — site.not_found links to
#site.not_found on this page — so an error always points the reader at its own
explanation. Always log the request_id; it’s what support uses to find your
request.
error types and statuses
Section titled “error types and statuses”The type is the coarse category and fixes the HTTP status. There are eight:
type |
HTTP | Meaning |
|---|---|---|
invalid_request |
400 |
The request is malformed — a bad field, a missing parameter, a reused idempotency key. |
authentication |
401 |
The key is missing, malformed, expired, or revoked. |
permission |
403 |
You’re authenticated and can see the resource, but your scope doesn’t allow the action. |
not_found |
404 |
The resource doesn’t exist — or you can’t see it (existence hiding). |
conflict |
409 |
The request conflicts with current state, or the runtime can’t perform it. |
rate_limit |
429 |
You exceeded a rate-limit bucket. Honor Retry-After. |
quota_exceeded |
402 / 429 |
A plan quota is exhausted (sites, storage, envs). |
api_error |
500 |
Something went wrong on our side. Safe to retry idempotent requests. |
common codes
Section titled “common codes”code is the precise, stable identifier — there are many, namespaced by resource;
these are the ones you’ll handle most.
code |
type |
HTTP | When you see it |
|---|---|---|---|
site.not_found |
not_found |
404 |
No site with that id, or your key can’t see it. |
environment.not_found |
not_found |
404 |
No such environment under the site. |
environment.capability_unsupported |
conflict |
409 |
The route exists but this runtime can’t perform the action — e.g. installing a plugin on a static site. |
auth.invalid_key |
authentication |
401 |
Key missing, malformed, expired, revoked, or blocked by its IP allowlist. |
scope.insufficient |
permission |
403 |
You can see the resource but your key lacks the scope for this action. |
rate_limit.exceeded |
rate_limit |
429 |
A per-key or per-team bucket is empty. Wait Retry-After seconds. |
idempotency.key_reused |
invalid_request |
400 |
An Idempotency-Key was reused with a different request body. |
quota.exceeded |
quota_exceeded |
402 |
A plan limit (sites, storage, environments) is reached. |
validation.failed |
invalid_request |
400 |
A field failed validation; param names which one. |
conflict.state |
conflict |
409 |
The resource is in a state that forbids the action — e.g. deleting an environment mid-deploy. |
the 404-vs-403 split
Section titled “the 404-vs-403 split”The most important distinction in the error model is when you get a 404 instead
of a 403 — it’s deliberate, and it preserves existence hiding:
404for things you can’t see. If your key is constrained to one site and you request another, you getsite.not_found— not “forbidden”. The API never confirms the existence of a resource outside your resource constraint. A capability that’s structurally absent on a runtime is also a404.403for things you can see but can’t act on. If you can see a resource (it’s within your constraint) but your key lacks the scope for the action, you getscope.insufficient— a403. This isn’t a leak, because you could already enumerate the resource.
Ordering guarantees you only ever receive a 403 for something you were already
entitled to know exists. See errors for the full model.