Skip to content

Errors & limits

This content is not available in your language yet.

What the API returns when something goes wrong, what it means, and what to do about it. Read the two error shapes first — half of “the error format is inconsistent” is really “there are two families of endpoints”.

The runtime (/api/agent/*) returns:

{ "error": "not_found", "message": "Thread 'abc' not found", "details": null }

Management (/api/manage/*) returns:

{ "detail": "collection 'abc' not found" }

Both are JSON, both carry a human-readable message, and the status code means the same thing in each. A client should read the status code and treat the body as diagnostic text, not parse it for control flow.

The request was understood and refused before any work started.

The three you will actually meet: uploading a file whose extension is not one the platform accepts (see Ingest and query for the list), uploading a file whose content contradicts its extension — a ZIP renamed to .pdf — and sending an empty search query.

A spreadsheet over 200,000 rows is refused here too, rather than truncated: half a ticket export that looks complete is worse than one you were told about.

Fix the request. Retrying it unchanged will not help.

The credential was missing, malformed, or not valid.

{ "detail": "Invalid API key" }

All of these produce the same 401, deliberately:

  • No Authorization header, or one that is not Bearer <key>.
  • A key that never existed.
  • A key that has been revoked.
  • A key that has expired.
  • A key that has been regenerated — regenerating invalidates the old value immediately.
  • A key whose owner’s account has been deleted. Issue a new key from an account that still exists.

They are indistinguishable on purpose. A response that said “this key exists but is revoked” would confirm the key’s existence to whoever sent it, so the API declines to distinguish. Check the key’s status on the API Keys screen rather than inferring it from the response.

401 is returned before any resource lookup happens, so it never tells you anything about what does or does not exist.

The credential was valid, and the role it carries is not enough.

{ "detail": "editor role required" }

Roles are ordered viewer < editor < admin. Reads need only a valid key, whatever its role; writes — creating collections, uploading documents, creating agents, managing keys — need editor or above.

The other 403 is an escalation refusal, and it reads differently:

{ "detail": "cannot create a key with a role above your own" }

Nobody can grant more than they hold. That covers minting an API key with a higher role, assigning a trust tier above your ceiling, and importing a plugin that declares one. The check is server-side against your credential’s actual role, so there is no request shape that avoids it — the fix is a credential with the role you need, or an operator.

The resource does not exist for you.

{ "error": "not_found", "message": "Thread 'abc' not found", "details": null }

This is the status to understand properly, because it covers three different situations that are deliberately indistinguishable:

  • The id does not exist anywhere.
  • The id exists in another tenant.
  • The id exists in your tenant but belongs to another identity — most often a thread created by a different API key.

A cross-tenant read returns the same status code and the same message template as a genuinely nonexistent id. That is not an oversight to be worked around; it is what stops the API from being used to probe for which ids exist elsewhere.

Practically: a 404 on something you are sure exists usually means you are holding the wrong key, not that the resource is gone.

The resource already exists.

Creating a collection whose name is taken in your tenant, or an agent whose name is taken within its plugin. Names are unique per tenant, not globally, so a name being free in one tenant says nothing about another.

Either reuse the existing resource or pick a different name.

The request was well-formed JSON but the content did not pass validation.

Endpoints doing structural validation — field types, missing keys — return the standard shape, with loc pointing at the offending field:

{
"detail": [
{ "loc": ["body", "name"], "msg": "Field required", "type": "missing" }
]
}

Authoring endpoints, which validate whole agents, skills and plugins, return a richer shape instead. Every problem is reported at once, each tagged with the file and field it came from:

{
"detail": {
"message": "validation failed",
"errors": [
{
"message": "'name' must match the skill name 'web-research'",
"field": "name",
"file": "skills/web-research/SKILL.md"
}
]
}
}

Collecting every issue is the point. Fixing one problem per round trip through a plugin import is slow, so the validator does not stop at the first failure.

Something went wrong on the platform’s side.

A 500 from an endpoint that normally works generally means a misconfiguration rather than a transient fault, and retrying will not clear it. Streaming runs are the exception worth handling: a dropped connection mid-stream is recoverable, and the run itself may still be alive — see on_disconnect in Threads & runs.

Back off exponentially and cap your retries. Do not retry a 4xx.

A streamed run has its own failure channel, separate from HTTP. Once the stream opens, the response is 200 and stays 200 — a run that fails says so in the stream:

event: error
data: {"error":"RetrievalError","message":"RetrievalError: execution failed"}
event: end
data: {"status":"error"}

So a client that only checks the status code will record every failed run as a success. Read the end event’s status, and treat an error event as the reason.

error names the exception class. It is diagnostic, not a stable contract — do not branch on the string. The agent’s Traces tab shows what actually happened; see Read traces and evals.

The same applies to a run stopped by a content guardrail, except that those are not errors at all: the run ends success, and the refusal arrives as the final message. Both cases mean “no useful answer”, and neither one is an HTTP failure.

  • 5 MB per file inside a plugin.
  • 50 MB total per plugin. A plugin over either limit is rejected whole; nothing is partially imported.
  • 1024 characters for a skill’s description. The runtime truncates a longer one silently, so the limit is enforced at save time instead.
  • 100 characters for agent, skill and plugin names, which must also be a single path-safe segment: letters, digits, ., _ and -, starting with a letter or a digit.

Document uploads have no fixed size cap of their own, but a very large file takes a correspondingly long time to parse and embed. Poll for done; do not assume a timeout means failure.

Document listings take limit (1–200, default 50) and offset. Other list endpoints return a bounded page rather than everything; page through them rather than assuming one call is complete.

There is no platform-level request-rate limit today. That is not a licence to hammer the API: a run consumes model-provider capacity, and provider limits are real even when ours are not. Run concurrent work at a level you have actually measured.

max_steps caps how many model calls one run may make. It is set per agent, and a run that reaches the ceiling ends there rather than looping. You cannot raise it from a run request — like collections, trust_tier and guardrails, it is overwritten server-side with the agent’s own value on every run.

A run may also end in a refusal rather than an answer, if a content guardrail blocked it. That is not an HTTP error: the request succeeds, and the refusal arrives as the final message in the stream, naming the category that fired. Handle it as a message, not as an exception — see Read traces and evals for where to see why it fired.