Skip to content

Errors, paging and retries

Core uses a common error format and cursor pagination across its API. Use request ids to trace problems and idempotency keys to recover completed calls when retrying.

The request format is OpenAI's

The body of POST /responses is OpenAI's own Responses body. A client you send to OpenAI today you send to Core unchanged: you change the address to your base URL and the key to your secret, and nothing about what you send.

Core also accepts a few optional fields of its own in that body, such as agent, timezone, tool_search, tool_variables and hosting. Request fields lists them; a request that leaves them out behaves as it would at OpenAI. POST /messages takes Anthropic's body with Core's own fields in the same way (Connect a coding agent).

What Core offers on top rides in headers. The main one is X-Core-Subject, the person a turn is for: set it once on your client and every request carries it.

client = OpenAI(base_url=CORE_URL, api_key=CORE_KEY, default_headers={"X-Core-Subject": subject_json})

A header rather than a body field or a query parameter is deliberate: attributes are the part of a request that no proxy along the way may put in a log line. See Building for people for what goes in it, and user when the id alone is all you have.

Header Sent by What it carries
X-Core-Subject you the person a request is for, with their attributes
Idempotency-Key you your name for one attempt, so a retry is answered once (Idempotency keys)
If-Match you the version you read, on a write that replaces a policy (a PATCH /keys/{id}, a PUT or DELETE of an organisation's policy) or a thread's working state; a stored version that changed since is 409 with reason_code: "stale_version"
X-Core-Thread-Id you and Core on /messages, the thread a stored turn belongs to (Continue a conversation on /messages)
X-Core-Previous-Response-Id you and Core on /messages, the response this turn continues
X-Core-Check Core on a turn a live check stopped, which check and why, such as stage=input, check="no-passwords", code=input_check_failed (What your application receives)
X-Core-Response-Id Core on a failed /messages turn, the id of the response it left behind, for GET /responses/{id}
X-Request-Id Core the id of this request (below)

A second kind of bearer. Besides your key (ack_...), Core accepts a subject token (ast_...) in Authorization: your key narrowed to one person for at most an hour, which your backend asks for and hands to a browser or a device that must never hold the key (A token for the browser).

The routes you administer with — keys, prompts, skills, knowledge — follow Core's own conventions: cursor-paged lists, 204 on delete, 201 with Location on stored creation. Those are below.

Request ids

Core adds X-Request-Id: req_... to its responses, including errors. It generates this id rather than accepting a caller-supplied value. Save it in your logs and include it in problem reports. The same id appears in the error body and related Core records. Errors returned before a request reaches Core may have no request id.

Error format

HTTP errors use the application/problem+json format, on every route except the five model-call routes described below:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "The prompt was not found.",
  "request_id": "req_5Tn8..."
}

type says which kind of failure this is. Where your deployment publishes this guide it is the address of the kind's entry in the error catalogue, such as https://docs.example.nl/en/reference/errors/#not-found; elsewhere it is about:blank. The catalogue lists every kind, with what to do about each.

Validation errors (422) also include an errors list:

{
  "status": 422,
  "errors": [{ "field": "tools.0.type", "message": "This tool type is not available for this key.", "type": "value_error" }]
}

Each entry identifies a rejected field. A field beginning with body. indicates a request parsing error, such as a missing field or incorrect type. A field without that prefix, such as collection_ids or config.nodes[2].type, indicates that parsing succeeded but Core rejected the value.

The model-call routes answer the vendor's error

POST /responses, GET /responses/{id}, POST /responses/{id}/tool_outputs, POST /responses/{id}/cancel and POST /messages answer errors in the shape the client library of the API they are compatible with reads. An OpenAI or Anthropic SDK pointed at Core therefore raises the same exception it would against the vendor, with the message and the field it refused in it.

The four /responses routes answer OpenAI's error object:

{
  "error": {
    "message": "The agent fixes this field; send no value of your own, or the same one.",
    "type": "invalid_request_error",
    "param": "temperature",
    "code": "invalid_request"
  }
}

param names the field the refusal is about, without the body. prefix Core's own errors list uses. code is Core's own code for the refusal, such as invalid_request, policy_denied or usage_cap_exceeded.

POST /messages answers Anthropic's error object. It has no field for a parameter, so the name of the field stands in front of the message:

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "temperature: The agent fixes this field; send no value of your own, or the same one."
  },
  "request_id": "req_5Tn8..."
}

These five routes never answer 422. A request they refuse — a field they cannot honour, a missing one, a value the field does not allow — is 400, the status both vendors validate a body with. Every other route in Core keeps 422 and its errors list, so which shape you read follows from the route you called rather than from what went wrong.

The statuses themselves are the ones in the table below, and WWW-Authenticate and Retry-After are sent the same way. A missing or invalid key is refused by Core itself on these routes, so that 401 arrives in the vendor's shape too, with WWW-Authenticate: Bearer.

Some answers still arrive as problem+json on these five routes. An unexpected 500 is one. The others come from the gateway in front of Core, before Core sees the request: the gateway's own rate limit (429) and its time limit (408) on all five, and a body larger than 1 MiB (413) on POST /responses/{id}/cancel, which takes no body worth that. The gateway's answers carry the title of their status and no X-Request-Id, which is how you tell them apart from Core's own answers. On the three routes that carry a conversation -- POST /responses, POST /messages and POST /responses/{id}/tool_outputs -- a body that is too large is refused by Core itself, 413 in the vendor's shape: they take up to 8 MiB unless your deployment set another limit, and the message names the limit. A body over 1 MiB is only read for a valid key; with a missing or wrong key it is refused 401 without being read. POST /messages also answers 500 in Anthropic's envelope, with type: api_error, when a turn fails at the model, in pseudonymisation or on structured output; only a 500 nothing anticipated is problem+json.

HTTP status codes

Status Meaning Action
400 A model-call route refused the request (above). Read error.message and the field in error.param.
401 Authentication failed. Check the API key secret.
403 Policy denied access, the operation is not open to your role, or the organisation or one above it is suspended; a suspension carries reason_code: "organisation_suspended" Read detail and resolve the restriction.
404 The resource is unavailable or its capability is disabled. Check GET /key, then the resource id.
408 The request did not arrive in full within 300 seconds; the gateway answers this (time limit). Send a smaller body, or send it over a faster connection.
409 The operation conflicts with the current state, or your organisation is still being set up or being deleted (The tenant is not ready.). Read detail; check for duplicate names or an invalid state transition. A delete of something still in use lists what uses it (below). For a tenant that is not ready, try again later.
413 The request body or upload exceeds the size limit. Reduce its size.
429 A monthly cap of yours is reached, you are sending requests too fast, or you hold as much open at once as you may (event streams, workflow runs or triggers). Wait as long as Retry-After says when it is present, and otherwise retry with increasing delays. A cap answers with an X-Request-Id header and a detail naming which of the two caps it was; the edge's own rate limit carries neither. See your monthly cap.
422 Request validation failed on a route other than the five above. Correct the fields listed in errors.
500 An unexpected error occurred. Save the request id; report persistent failures.
503 A required dependency is unavailable. Retry with increasing delays.

The error catalogue splits these statuses into the kinds type names: a 429 from a monthly cap and one from the rate limit are two kinds, for instance.

A 401 includes WWW-Authenticate: Bearer. A 403 means authentication succeeded but the operation is not allowed. Internal exception text is never included in a 500 response.

Deleting something that is still in use

Core does not delete a thing another thing still names, and nothing is deleted along with it. The DELETE answers 409 and says what still uses the thing, so you can take those references away and send the delete again:

{
  "title": "Conflict",
  "status": 409,
  "detail": "The skill is named by an agent; remove it from the agent first.",
  "conflicts": [
    { "where": "agents", "id_or_name": "agent_7Kc2...", "label_or_condition": "helpdesk",
      "reason": "links skill skill_3Rt9...", "reason_code": "links_skill", "count": null }
  ],
  "withheld": [{ "where": "agents", "count": 2 }]
}
Field What it says
conflicts[].where the kind of thing that uses it, such as agents, indexes, graphs, keys or retrieval tools
conflicts[].id_or_name, label_or_condition its id and its name; for a graph type, the type's name in both
conflicts[].reason_code why it is in the way, from a fixed list your client can switch on: names_tool, names_toolset, delegates_to_agent, links_skill, selects_collection, ingests_with_pipeline, runs_pipeline, fills_graph, injects_memory, extraction_on, has_entities, has_relations, names_routing_config, default_routing_config, outside_vocabulary. The last one comes with the 422 of a vocabulary that stored rows contradict, never with a delete. reason is the same in a sentence for people, and its wording may change
conflicts[].count a number where one is meant: how many entities or relations of a graph type a new schema would drop. Otherwise null
withheld what uses the thing and your key may not read, counted per kind (agents, indexes, graphs, keys), without id or name

conflicts holds at most the first twenty, and detail then ends with More are not listed. when there are more. The delete of a routing configuration is the exception: it lists up to a hundred keys that still name it, and its detail does not say when there are more. An agent, an index or a graph your key's policy does not let it read, or a key when yours lacks the management capability, is never listed: it is counted in withheld, and a count there is exact as long as no more than a hundred things of that kind use the thing. A search tool is always listed. A delete can therefore answer 409 with an empty conflicts; ask somebody who can read those things to take the reference away.

This applies to a skill an agent links, an agent another agent calls as a tool, a toolset, a connector, an MCP server, a search tool or an alias an agent names, a collection an index selects, a pipeline an index, a graph or a search tool runs, an index a key injects memory from or for which memory extraction is switched on, and a graph schema that leaves out a type the graph still holds facts of. Only the latest version of an agent counts.

Request time limit

The gateway gives every request 300 seconds to arrive in full, body included, and answers 408 when it has not. Only a large body over a slow connection comes near that limit. A model call (POST /responses, POST /messages, POST /responses/{id}/tool_outputs) takes up to 8 MiB by default, which needs an upload speed of about 0.2 Mbit/s to arrive in time; a file upload to POST /files takes up to 25 MiB by default, which needs about 0.7 Mbit/s. A retry meets the same limit unless the body is smaller or the connection faster.

Limits on free text

A field that takes free text has a limit, and a request over it is refused 422 with the name of the field and the limit in the message:

Field Limit
instructions of an agent, and template of a prompt version 262,144 characters
A value in a subscription's config 16,384 characters
metadata of a collection or a source 64 pairs, keys of at most 128 and values of at most 2,048 characters
The request of a schedule 256 KiB, serialised
tool_variables of a request (call variables) 64 names, a string of at most 4,096 characters (8,192 for a secret), 64 KiB in all; 400 on POST /responses and POST /messages
query of a search 4,096 characters

A turn's own instructions and input have limits as well, refused 400 on POST /responses: Limits. On POST /messages the same 262,144 characters hold for system, and a longer one is refused 400 naming system.

Cursor pagination

List endpoints return items and a cursor for the next page:

{ "items": [...], "next_cursor": "eyJ2IjoxLCJr..." }

Pass next_cursor as the cursor query parameter. Use limit to set the page size. A null next_cursor means you have reached the end.

curl -s "$CORE_URL/threads?limit=50" -H "Authorization: Bearer $CORE_KEY"
curl -s "$CORE_URL/threads?limit=50&cursor=eyJ2IjoxLCJr..." -H "Authorization: Bearer $CORE_KEY"

Keep the cursor unchanged. Its format is versioned and may change. Offset pagination is not supported; cursor pagination avoids shifting page boundaries when new records are inserted.

Sorting a list

A list that can be sorted takes sort, the name of a column, and order, asc or desc. The columns it offers are name, created_at and updated_at where the resource has them; the API reference lists them for each route. A column the route does not offer is a 422 naming query.sort. With sort and no order the list reads ascending. With neither, it keeps its usual order, which is newest first for schedules and evaluations and oldest first for the others; order alone reads that list in the direction you name. The version lists of agents and prompts take order too, without sort: they read oldest first, and order=desc reads them newest first. The version lists of toolsets, tool aliases, MCP servers and API, mail and SQL connectors read newest first, and order=asc reads them oldest first.

curl -s "$CORE_URL/skills?sort=name&order=asc&limit=50" -H "Authorization: Bearer $CORE_KEY"

Rows with the same value in the sorted column come in a fixed order, so no row is skipped or shown twice from one page to the next. Names are compared the way the database compares text, which is case-sensitive. A cursor is only good for the sort and order it came from: sending it with another gives a 422 naming query.cursor, so start again without a cursor when you change either.

Idempotency keys

Send an Idempotency-Key header on POST /responses and reuse it when retrying the same call. Once Core has committed the original result, it can recognize a retry:

Original response Replay with the same key
Stored (201) 200 with the stored response; no additional model call
Stateless (200) 409; there is no stored response to return

Leave storage enabled if you need to recover the answer after a connection failure. A completed stateless call can be recognized, but its output cannot be recovered.

Avoid sending concurrent requests with the same key. If a retry arrives before the first call commits, both requests can invoke the model and incur usage; the conflicting write returns 409. Idempotency does not guarantee exactly one provider call.

Retry handling

For model calls, use the idempotency handling above before retrying:

Situation Handling
503 or a connection error without a response Retry with increasing delays.
500 Retry with increasing delays; report the request id if failures persist.
Rate-limit refusal before Core, without X-Request-Id Wait before retrying. Each credential has its own rate budget.
Other 4xx Correct the request or permissions before trying again.
Response status: failed Inspect the response error. Another model attempt is a new turn and can incur additional usage.

Core limits its own provider retries. A timed-out provider call may already have produced billable output.

Resource authorship

Skills, prompts and their versions, tools, MCP servers, files, knowledge sources, indexes, pipelines and search tools include created_by. It contains the creating key's id, or operator: followed by the console user's identity.

Use this field for attribution and sorting. Ownership belongs to the organisation; created_by does not grant or restrict access. Other keys in the organisation can use its resources subject to their capabilities and policy.

API reference

The API reference points at the two explorers your deployment publishes: ReDoc to read every route with its request and response shapes, Swagger UI to try one with your key. Its Base URL section gives the address every route is called at.