Docs

Errors

Every error shape the API returns, the codes worth branching on, which ones are worth retrying, and the details each one carries

This is the one reference for errors. It covers the shapes a failure can take, the codes you will see, and what to do about each. Paging and idempotency are in Pagination and errors; the 429 budgets are in Rate limits.

Error codes at a glance

Every error.code the public API returns, in one line each. The sections below carry the detail. Branch on the HTTP status first (see the shapes), then on code.

CodeStatusWhat it meansWhat to do
invalid_input400The body or query failed the operation's input schema.Fix the payload from details.errors[]. Do not retry as is.
invalid_argument400Well-formed input that names something unknown or a combination that cannot work.Fix the arguments. Do not retry as is.
invalid_query_parameter400A read's query parameter has a bad enum or integer value.Fix the parameter.
validation_failed400An eval definition failed the authoring checks.Fix the fields it names. POST /evals/validate checks without storing.
invalid_sql400A Search filter read was refused by the query gate.Change the query. Retrying is pointless.
connector_not_connectable400The connector is retired, planned, or not available here.Pick another connector from GET /connections/catalog.
mixed_eval_versions400A training-data export spans more than one eval version.Narrow the filter to one version.
unauthenticated401No credential, or one that is malformed, revoked or expired.Re-authenticate, then retry.
forbidden403The credential lacks the scope or role the operation needs.Get a credential with the scope named in message.
not_on_public_api403The operation is not on the public surface for an external credential.Terminal for that credential. Use an operation from the tool catalog.
surface_forbidden403The member's console access does not include this family (/connections, for example).Ask a workspace admin for access.
not_found404The resource does not exist, or belongs to another workspace.Check the id and the workspace the key is bound to.
conflict409The state does not allow the call: a Chat that is not active, a research job that is not paused or already ended, a connector that is already connected, or an idempotency_key reused with a different body.Read the current state. The same call fails the same way.
slug_conflict409An agent or eval slug is already taken.Pick another slug.
credential_conflict409The workspace already holds a credential for this connector.Reconnect the existing connection instead.
setup_needed409Research or an eval on a workspace with nothing connected. Refused before any quota is counted.Show message and connect_url. Do not retry until a source is connected. See setup_needed.
rate_limited429A per-minute, in-flight or warehouse-read budget refused the call, or the Optimizer is briefly stopped.Wait Retry-After (details.retry_after_seconds), then retry.
quota_exceeded429A monthly cap for the verb family is used up.Do not retry. It resets next month, or support raises the cap.
operation_error500The operation threw.Retry with backoff.
internal_error500A server fault, or a research job or batch could not be queued.Retry with backoff.
query_failed500The warehouse faulted while running a Search read.Retry with backoff.
read_error500A read failed on the server.Retry with backoff.
db_error, encryption_failed, credential_conflict_cleanup_failed500A store fault while saving an eval or a connector credential.Retry with backoff.
optimizer_unconfigured, batch_unavailable503The Optimizer or its batch queue does not run in this environment.Contact Amdahl. Retrying does not help.

These come back as HTTP 200 with success: false, so branch on success, not on the status:

CodeOperationWhat it meansWhat to do
over_capevals.exportMore than 5,000 rows matched.Narrow the range, set limit, or download the file (GET /evals/export.csv), which has no cap.
library_agent_lockedagents.update_agent, agents.delete_agentThe target is a code-defined Amdahl library agent.Do not retry. Save your own agent instead.
enqueue_failed, substrate_failedchat.startThe first turn could not be started.Retry.
fire_failedroutines.run_now, subscriptions.test_fireThe fire was skipped.Read message, then retry.
run_not_found, write_failedevals.record_feedbackThe run is unknown, or the feedback could not be saved.Check the run id; retry a write_failed.

Three operations report a failure in another field, also with HTTP 200. messages.optimize returns ok: false with a reason (optimizer_timeout, optimizer_error, optimizer_unconfigured); see When ok is false. agents.resume returns status: "not_found", "invalid_state" or "invalid_input", and agents.cancel returns status: "not_found" or "cannot_cancel"; see Agents. Over MCP a refusal can also be isError text, such as invalid_uri on a URI read or insufficient_scope.

Check the status before you read error

Failures do not all share one shape, and error is not always an object. An error handler written against the object shape alone reads body.error.code as undefined on the string-shaped bodies, which fails silently: it will not recognise an expired key. Branch on the HTTP status first, then on code.

1. Operation refusals: 4xx, inside the data envelope. The request reached an operation, and the operation refused it. success is false and the typed error sits beside it:

json
{
  "data": {
    "success": false,
    "error": {
      "code": "invalid_argument",
      "message": "Unknown filter field \"not_a_field\" on surface \"deals\". Known fields: deal_id, …",
      "details": { "field": "not_a_field", "surface": "deals" }
    }
  }
}

2. Gate and schema errors: bare envelope. The request was refused before an operation ran (a missing scope, an off-surface operation, a body that failed schema validation) or the operation threw. There is no data wrapper:

json
{
  "error": {
    "code": "invalid_input",
    "message": "Input failed schema validation",
    "details": {
      "errors": [{ "path": "$.limit", "message": "must be <= 200", "value": 500 }]
    }
  }
}
  • code: a stable machine-readable string. Branch on it.
  • message: a human-readable description. Safe to log and show. Do not pattern-match on it; the text can change.
  • details: optional, and its shape depends on the code. Present only when the server has something structured to attach.

3. Authentication and routing failures: 401 / 404, bare, and error is a string. These short-circuit ahead of the typed handler, so they carry no code and no details:

json
{
  "error": "Unauthorized",
  "message": "Valid OAuth token or API key required. Send an API key as \"Authorization: Bearer <key>\" or \"X-API-Key: <key>\"; send an OAuth token as \"Authorization: Bearer <token>\"."
}

An unmatched route returns 404 in the same string shape with no message. Treat both as terminal from the status alone.

The global per-IP limiter's 429 is a fourth shape: a plain-text body, not JSON. See Rate limits.

Codes

CodeStatusWhen it firesRetry?
invalid_input400The request body or query failed the operation's input schema. details.errors[] lists each problem as { path, message, value? }.No. Fix the payload.
invalid_argument400The input is well-formed but names something that does not exist or cannot combine: an unknown field, operator, eval slug or run id. Usually inside the data envelope (shape 1).No. Fix the arguments.
unauthenticated401No credential, or one that is malformed, revoked or expired. Most 401s are the string shape above; unauthenticated is what the few enveloped ones say.After re-auth.
forbidden403The credential is valid but lacks the scope or role the operation needs. The missing scope is named in message (Missing required scope(s): evals:execute). There is no details.No. Get a credential with the scope.
not_on_public_api403The operation exists but is not on the public surface for an external credential (API key or external OAuth token). Only the operations in the tool catalog are reachable from outside the console.No. Terminal for that credential.
not_found404The resource does not exist, or belongs to another workspace. See Read 404s.No.
rate_limited429A per-user or per-workspace budget refused the call: the warehouse-read budget behind Search, the eval export's one-a-minute limit, or a Message Optimizer limit (details.reason is workspace_in_flight_limit, workspace_rate_limit or optimizer_busy), or the Message Optimizer is down and calls to it are stopped for a short time (details.reason is optimizer_unavailable). JSON body with details.retry_after_seconds. A batch submit refused with details.reason: "batch_backlog_limit" carries no wait: the workspace has 5,000 batch items queued, and it clears when an earlier batch finishes.Yes, after Retry-After.
quota_exceeded429A monthly cap on a verb family (search / evals / messages) is exhausted for the workspace. The messages cap is 1,000 by default; the others are set by an operator. details carries the family, the limit and usage. On a Message Optimizer batch submit it also carries pending, requested and remaining: the batch did not fit in what is left.No. Terminal until the month rolls or the cap is raised.
operation_error500 (usually)The operation threw. This is the catch-all for a handler fault. A few operations tag their thrown error with a more specific code and status, which then replaces this one.Yes, with backoff.
internal_error500A fault on one of the hand-written routes (the eval export files, for example).Yes, with backoff.
query_failed500The warehouse faulted while running a Search read.Yes, with backoff.

Three more codes belong to one call each:

  • invalid_sql (400) on a warehouse read: the SQL the gate refused must change. Retrying is pointless.
  • over_cap on POST /evals/export: more than 5,000 rows matched. It comes back as success: false inside the data envelope with HTTP 200, so branch on success. See Export runs.
  • mixed_eval_versions (400) on a training-data export: the filter spans more than one eval version. See Export runs as training data.

Per-operation codes

Some operations define richer codes on top of the set above. They use the same envelope; only the code is more specific.

agents.resume (POST /agents/:session_id/resume)

A resume answers 200 and reports a failure in status, not in error.code:

statusMeaning
invalid_inputThe resume payload did not match pending_input_schema. validation_error names the mismatch.
invalid_stateThe run is not in awaiting_input. A repeat resume against a run that already resumed lands here, never as a double-apply. current_status says where the run is.
not_foundUnknown run id, or one that belongs to another workspace.

The agent library (/agents CRUD)

CodeStatusMeaning
slug_conflict409An explicit slug on create collides with an existing agent (or shadows an Amdahl library agent). Pick another slug.
library_agent_locked200The target is a code-defined Amdahl library agent; update and delete are refused. It comes back with success: false and HTTP 200, so branch on success.

Authoring (POST /evals, PATCH /evals/:slug)

validation_failed carries the exact field errors when a definition fails the authoring checks. POST /evals/validate returns the same errors as { valid: false, errors: [...] } without storing anything.

Search (search.query, search.fields)

Past parameter validation Search does not use error envelopes for degradations. Every partial outcome is a typed field on a success: true result (detail.internal.status, corpus.empty_reason, coverage: null). The error codes it returns are invalid_argument, the warehouse-read codes above, and the gate codes. One case to know: on the plain-language (fuzzy) lane, running out of the warehouse-read budget comes back as 200 with detail.internal.status: "failed", not as a 429. See Rate limits.

setup_needed: the workspace has nothing connected

setup_needed means the workspace has no connected source (no CRM, no call recorder), so there is nothing for the call to read. It has one reason, no_sources_connected, and it comes in two forms. Both carry the same field:

FieldNotes
reasonAlways no_sources_connected.
messagePlain words for the person who asked, with the link in them. Show it as written.
connect_urlThe workspace's Connections page in the console, where a member connects a source.

The hint: 200, not an error. search.query answers an empty result with success: true, and adds setup_needed beside results. A count over nothing is one row of zeros ([{ "count": 0 }]) and carries it too. research.start, evals.run, messages.optimize (with evidence: "workspace") and the accepted messages.optimize_batch (when an item asks for evidence: "workspace") add the same field to a success when the work still goes ahead.

json
{
  "data": {
    "success": true,
    "mode_ran": "fuzzy",
    "results": [],
    "setup_needed": {
      "reason": "no_sources_connected",
      "message": "This workspace has no data connected yet, so there is nothing to search. Connect your CRM or call recorder at https://console.amdahl.ai/acme/settings/connections, then ask again.",
      "connect_url": "https://console.amdahl.ai/acme/settings/connections"
    }
  }
}

The refusal: 409. When the workspace also holds no conversation themes, research.start and an evals.run that could only end not_applicable (empty_corpus) are refused before any quota is counted. No job starts. The refusal is shape 1: error.code is setup_needed, error.details holds reason and connect_url, and the full field rides beside error, so a client that already relays the hint relays this one with no new rule.

json
{
  "data": {
    "success": false,
    "error": {
      "code": "setup_needed",
      "message": "This workspace has no data connected yet, so there is nothing to search. Connect your CRM or call recorder at https://console.amdahl.ai/acme/settings/connections, then ask again.",
      "details": {
        "reason": "no_sources_connected",
        "connect_url": "https://console.amdahl.ai/acme/settings/connections"
      }
    },
    "setup_needed": {
      "reason": "no_sources_connected",
      "message": "This workspace has no data connected yet, so there is nothing to search. Connect your CRM or call recorder at https://console.amdahl.ai/acme/settings/connections, then ask again.",
      "connect_url": "https://console.amdahl.ai/acme/settings/connections"
    }
  }
}

An eval run is not refused when it can grade without the workspace's conversations: with include_external: true, with evidence_from_run or candidate_from_run, or when the eval has a case of only rule or structure graders.

How to handle either form:

  1. Check for the field first. Read setup_needed on every response from these calls, success or not. On the 409, error.code is setup_needed as well.
  2. Show the user message and connect_url. Do not report "no results", and do not show a failure the user cannot act on. Connecting a source is the fix, and the link goes to the page that does it.
  3. Do not retry until a source is connected. The same call answers the same way until then. Backoff does not help, and a retried search still counts as a search call.
  4. Do not answer from your own knowledge. An agent must not fill the gap with general knowledge as if it came from the user's data. Say the workspace has nothing connected yet.

The field is absent when the workspace has a connected source (also one that is broken, or one that was disconnected after it synced), when rows came back, and when the server could not check. An empty search without it is an honest empty: read corpus.empty_reason. The field goes away on the next call after the first source connects; the data appears once that source's first sync lands. More in the FAQ and in Connections.

Read 404s name the shapes that ARE valid

When a URI-addressed read returns nothing, the error lists what the scheme does serve:

json
{
  "error": {
    "code": "not_found",
    "message": "Not found: eval_run://missing-run. Valid eval_run:// shapes: eval_run://<id>, eval_run://list, eval_run://<id>/report, eval_run://<id>/compare/<other_id>.",
    "details": {
      "valid_uri_shapes": [
        "eval_run://<id>",
        "eval_run://list",
        "eval_run://<id>/report",
        "eval_run://<id>/compare/<other_id>"
      ]
    }
  }
}

The URIs in this body use the registry spelling (eval_run://), which REST and the Anthropic read_resource tool accept. Over MCP, spell the two-word schemes with a hyphen (eval-run://): an underscore is illegal in a URI scheme, and standards-compliant clients throw on it before the request is sent. Both spellings resolve on the server.

You get the same body whether the row is missing or the id was never well-formed (GET /eval-runs/not-a-uuid). The two are deliberately indistinguishable, so a 404 cannot be used to probe whether an id is valid.

A URI whose scheme exists but whose sub-path nobody serves (eval_run://<id>/cases) is refused with invalid_uri rather than not_found, because the shape is wrong rather than the row missing. invalid_uri reaches you only on the URI-addressed surfaces (read_resource and MCP resources/read). Over REST an unserved path is an ordinary unmatched route.

details.valid_uri_shapes is scope-filtered: it lists only the shapes your credential may read, and is omitted when there are none. Treat it as a hint for correcting a request. operation://list is the authoritative catalog.

Security-sensitive errors

  1. 404 instead of 403 for cross-tenant access. A Chat, eval run, routine or connection that exists but belongs to another workspace returns 404 not_found, so a caller cannot tell "does not exist" from "exists but not yours". If you are sure the id is yours and get a 404, check that your key is bound to the right workspace.
  2. Masked secrets in error details. When a validation error touches a field the platform treats as sensitive (tokens, API keys, refresh credentials), details shows [MASKED] in place of the value.

Retry strategy

  • Retry: 5xx (operation_error, internal_error, query_failed) and rate_limited. Use exponential backoff with jitter: start at 500 ms, double each attempt, cap at 30 seconds, give up after 5 attempts unless your workload can tolerate more.
  • Honor Retry-After. When a 429 or 503 carries it (in seconds), wait at least that long. On a JSON 429, details.retry_after_seconds carries the same number.
  • Do not retry any other 4xx. invalid_input, invalid_argument, forbidden, not_on_public_api, not_found, quota_exceeded and setup_needed all mean this exact call will not succeed, and retrying spends rate-limit budget on top of the original failure.
  • Retrying a POST can create a duplicate. See Idempotency.

Request id

Every response carries an X-Request-Id header. Capture it when you log or surface an error; support requests that include it resolve faster.

You can also send X-Request-Id on the way in, and the platform uses your value instead of minting one, so you can correlate on an id your own system already assigned. It is echoed back on the response and exposed via CORS so browser clients can read it. Values are truncated to 128 characters and stripped of anything that cannot legally sit in a header, so send an id you generated, not user input. A UUID is the easiest id to join on: it is stored as sent, while any other value is stored in the audit trail under a fixed UUID derived from it.

See also