Skip to content

Make a model call

Send POST /responses to run a turn with a language model. A turn includes the model call and any tool calls needed to produce a response. This page describes the request, the response, streaming and the errors you meet; every other page under Calling Core builds on it.

You need an API key and a model id that key's policy permits. If you have not sent a request yet, start at Make your first call.

Request fields

{
  "model": "<a model id>",
  "instructions": "You answer in one sentence.",
  "input": [{ "role": "user", "content": "What is a bearer token?" }],
  "temperature": 0.2,
  "max_output_tokens": 400
}
Field Description
model Model id. GET /models lists the models your key is allowed to use.
input A string or list of items. A string is treated as one user message.
instructions Instructions for this turn, with developer authority.
temperature, max_output_tokens Optional settings passed to the model. A model that refuses a sampling parameter fails the turn instead of ignoring it: the current Claude models reject temperature, so leave it out for those. Gemini models accept it, except gemini-3.5-flash-lite and gemini-3.6-flash, which have fixed sampling and ignore it.
text Output format, for example {"format": {"type": "json_schema", "name": "...", "schema": {...}}}.
reasoning Optional. What this turn asks of the model's own thinking: {"effort": "low" \| "medium" \| "high" \| "xhigh" \| "max", "summary": "auto" \| "concise" \| "detailed" \| "none"}. Both parts are optional. An effort you leave out is not sent, so the model keeps its own default. summary asks for the thinking back in readable form; left out, your key's or organisation's setting decides (Response fields). "summary": "none" asks for no summary on this turn whatever those settings say, and every model accepts it: the model still thinks at its effort and the reasoning tokens still count in usage. A model reached over an OpenAI-compatible chat-completions API has no way to ask for a summary, and what comes back depends on the model's server (Response fields); a model whose client could never return one, an Azure deployment on chat completions, refuses a request that asks with a 400 naming reasoning.summary. A model that refuses an effort fails the turn, like a sampling parameter it refuses.
agent Optional. Names a stored agent whose definition this turn is built on: its model, instruction, pinned prompt, skills and tools. Its skills and tools are added to the request's; any other field it fixes and the request also sets is refused with 400, naming that field in error.param. With an agent that fixes a model, model may be left out.
timezone Optional, a Core extension. An IANA zone name such as Europe/Amsterdam: the timezone a prompt's built-in date and time values are read in for this turn. Leave it out and the timezone of the schedule firing the turn applies, otherwise your organisation's, otherwise UTC. A zone Core does not know returns 400 with timezone in error.param.
tool_search Optional, a Core extension. {"mode": "router"} offers the model two tools, builtin__tool_search and builtin__tool_run, in place of the tools Core runs for this turn and your own function tools, so a turn can reach many more tools than a model accepts in one call; {"mode": "direct"} offers every tool as usual. top_k (1 to 20) and budget_tokens set how many tools one search returns and how large its answer may be. Left out, defer_loading: true on any tool or OpenAI's {"type": "tool_search"} entry asks for router (coding agents), and otherwise the agent's choice applies, then your key's. Your key's tooling.tool_search.enabled must be on, and your organisation must allow tool search, or router returns 400 with tool_search.mode in error.param. See The tool router.
helper Optional, a Core extension. Names one of your own helpers, such as acme.summary, whose work this turn is: every usage row the turn writes carries the name, and without model (and with no agent fixing one) the turn runs on your organisation's helper model. Two to four dotted lowercase segments, at most 64 characters; names under assist. are Core's own. A name outside those rules returns 400 with helper in error.param. The response does not repeat it. See Build your own helper.
metadata Up to 16 string key/value pairs, stored and returned without interpretation.
tool_variables Optional, a Core extension. Values for the call variables the turn's tools declare, as a flat object of names to strings, numbers or booleans, such as your customer's account id or own token for your API. The model never sees them and the response never repeats them; each tool receives only the ones it declares, and a name no tool declares is ignored. A required one that is missing, or a value its declaration does not admit, returns 400 with tool_variables.<name> in error.param. See Pass a customer's value to a tool.
hosting Optional. Requires the model answering this turn to run in a named place, for example {"region": "eu"}. region is a jurisdiction (eu, us, in-cluster, global) or a two-letter country code. Only an exact match on the model's own region or country satisfies it: nl does not satisfy eu, and eu does not satisfy nl. The requirement also holds every other model called for this turn: the one that summarises the thread's older messages, the embedding model a knowledge search uses, an image or transcription tool's model and a knowledge tool's model steps. Those do not fail the turn when they run elsewhere: the thread's messages are sent unsummarised, a knowledge search that needs the embedding model is unavailable, and a tool answers Denied by policy. The audio model that transcribes an input_audio part is the exception: when no audio model runs in the region, the request is refused 403.

Input items support the roles user, developer, system, delegate and assistant. Put application instructions in instructions or a developer message. Put end-user content in a user message, including requests phrased as commands. Core labels this content untrusted and never sends it as a system instruction; a key that holds trust_envelope also tells the model that it is data.

A user message can also carry files, images and recordings as content parts: input_file and input_image (Files), and input_audio, a recording Core transcribes before the model reads it, with the audio model the part names in model or else the first one your key admits (Transcribe audio).

Choose a model

GET /models lists the chat models your key is allowed to target. The answer carries the same models twice, for two kinds of reader:

Field What it holds
items, next_cursor Core's own list, cursor-paged, with the full entry shown below
object, data, has_more, first_id, last_id the list the OpenAI and Anthropic SDKs read, so client.models.list() works with either. data holds every chat model your key may target in one page, so has_more is always false

An entry of data is short: id, object and type (both model), owned_by, display_name (the id again), and created and created_at, which are always 0 and the Unix epoch because Core records no creation time for a model. Read items for everything else. With ?kind=image, ?kind=embedding or ?kind=audio (below), data is empty and those models are in items only.

Each entry of items carries where the model runs, which lab trained it, the chain your turns on it fall over along (fallback), and what it costs your organisation right now:

{
  "id": "gpt-5.6-sol",
  "object": "model",
  "owned_by": "azure-openai-eu",
  "region": "eu",
  "country": "nl",
  "lab": "openai",
  "fallbacks": ["claude-sonnet-5"],
  "price": {
    "input_eur_per_million_tokens": "2.50",
    "output_eur_per_million_tokens": "10.00",
    "cached_input_eur_per_million_tokens": "0.25",
    "cache_write_eur_per_million_tokens": null,
    "eur_per_unit": null,
    "effective_from": "2026-09-01T00:00:00Z",
    "is_tenant_override": false,
    "listed": {
      "currency": "EUR",
      "input_per_million_tokens": "2.50",
      "output_per_million_tokens": "10.00",
      "cached_input_per_million_tokens": "0.25",
      "cache_write_per_million_tokens": null,
      "per_unit": null
    },
    "exchange_rate": null
  },
  "deprecation": null
}

lab is the lab that trained the model — anthropic, openai, google, meta, mistral, perplexity, black_forest or artific — as your operator states it, and null when none is stated. It is not where the model runs: that is region, and owned_by is the provider serving it.

price is null when nothing prices the model; usage on such a model is still recorded and simply costs nothing until a price exists (usage). Amounts are euro as exact decimal strings. cached_input_eur_per_million_tokens is the rate for input the provider read from its prompt cache and cache_write_eur_per_million_tokens the rate for input it wrote into it (prompt caching); null means no separate rate, so those tokens cost the plain input rate. eur_per_unit is set instead of the token amounts for a model charged per unit: per image for an image model (?kind=image, below), per minute of audio for an audio model (?kind=audio). is_tenant_override is true when your organisation is charged its own price rather than the deployment's, and effective_from is the instant the price in force began — a price announced for a future date is not shown here until it applies.

listed is the same price in the currency it was recorded in, which is the currency the model's vendor invoices in, with the same amounts minus eur_ in their names. exchange_rate is null for a price in euro. For a price in another currency it names the rate Core converted by: the newest reference rate of the European Central Bank and the day it was published.

"listed": {
  "currency": "USD",
  "input_per_million_tokens": "3.00",
  "output_per_million_tokens": "15.00",
  "cached_input_per_million_tokens": "0.30",
  "cache_write_per_million_tokens": "3.75",
  "per_unit": null
},
"exchange_rate": { "units_per_euro": "1.1650", "rate_date": "2026-10-08" }

units_per_euro is how many units of the currency one euro buys. Every euro amount is the amount in listed divided by that rate, unrounded: 3.00 / 1.1650 for input here. So the euro amount changes when the ECB publishes a new rate. How that carries through to what a month costs is on A price in another currency.

deprecation is null unless your operator is retiring the model; see below.

A retired model

A model being retired carries its date and replacement in items:

"deprecation": {
  "effective_date": "2026-12-01",
  "replacement": "claude-sonnet-5",
  "in_effect": false
}

effective_date is the day (in UTC) from whose start the model no longer answers as itself. replacement is the model that takes its place then, and in_effect says whether that day had begun when you read the list.

Every response to a request naming such a model, before the date and after it, carries the header X-Core-Model-Deprecation, a refusal included:

X-Core-Model-Deprecation: "gpt-5.6-sol"; effective="2026-12-01"; replacement="claude-sonnet-5"; rerouted

rerouted is there when the turn already ran on the replacement. When the request named more than one retiring model, they follow each other, separated by commas. A streamed response carries the header from the start of the stream, and a background run on the response that creates it. A turn with no HTTP response of its own, such as a schedule's fire, carries no header; there the reroute shows in the stored response's model. A web page cannot read this header; read deprecation from GET /models.

From the date on, it depends on your organisation's and your key's policy (Retiring models):

  • Reroute (the default): the turn runs on the replacement, as if you had named it. model in the response names the replacement, and the usage is costed at its price. When your key refuses the replacement, you get the status you would have got naming it yourself, with a message naming both models: The model 'gpt-5.6-sol' was retired on 2026-12-01 and its replacement 'claude-sonnet-5' cannot take this request: ....
  • Block: on POST /responses, 400 with model in error.param and the message The model 'gpt-5.6-sol' was retired on 2026-12-01. Use 'claude-sonnet-5' instead. Name the replacement in your request.

This holds for every way a turn starts: POST /responses, POST /messages, a schedule's fire, an agent calling another agent, and a task. A background run is rerouted at the moment it is accepted, not when it is picked up: a background run accepted before the date and picked up after it still calls the retired model. Close to the date, queue no more background runs on that model; name the replacement.

Models your key reaches without naming them

A turn can call more models than the one in model: an image model through its tool, an audio model that transcribes a recording, the embedding model that searches your knowledge, memory and skills, and the model that summarises a long thread. Each is held to your key's regions, so each is listed with its region and country too.

GET /models?kind=image lists the image models, each with the tool_name it is offered as (builtin__generate_image__...). GET /models?kind=audio lists the audio models, each with the tool_name of its transcription tool (builtin__transcribe_audio__...); the same models serve POST /audio/transcriptions and input_audio (Transcribe audio). GET /models?kind=embedding lists the embedding models. Unlike the default kind=chat, these list every model your organisation has, and allowed says whether your key's model policy admits it right now; false means a call on it is refused. For an image model that is the model's half only: the tool must also be switched on and allowed (image generation). Every chat entry has kind: "chat" and allowed: true, because a chat model your key may not target is not listed.

GET /model-roles names the models Core picks for you:

{
  "items": [
    {"object": "model_role", "role": "embedding", "model": "text-embedding-eu",
     "region": "eu", "country": "nl", "allowed": true},
    {"object": "model_role", "role": "summariser", "model": null,
     "region": null, "country": null, "allowed": null}
  ],
  "next_cursor": null
}

summariser appears when your key has compaction and summarises (any mode but window). With no compaction.model of your own it names the summary model your deployment is configured with, if there is one; a model of null means the turn's own model summarises, and that one is decided with the turn. Once your key's or your organisation's monthly cap is reached, no new summaries are made and the thread's older turns are sent as they are. embedding appears when the deployment has an embedding model for your organisation. None of these lists takes a request's hosting.region into account: that narrows one turn.

Response fields

{
  "id": "resp_0KQ2mR...",
  "object": "response",
  "status": "completed",
  "model": "<a model id>",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [{ "type": "output_text", "text": "..." }]
    }
  ],
  "usage": {
    "input_tokens": 31,
    "input_tokens_details": { "cached_tokens": 0, "cache_write_tokens": 0 },
    "output_tokens": 22,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 53
  },
  "conversation": { "id": "thr_0KQ2mR..." }
}

There is no output_text field: the OpenAI SDK computes it from output, and so can you. conversation is the thread a stored turn's items were written to, and null for a stateless one. The id of the request that produced the answer is the X-Request-Id response header.

Read output as a list. It can contain assistant messages, function_call items for your application to execute, mcp_call items for a tool Core ran for you, and a reasoning item when the model's thinking came back with the answer. A background run that waits for approval shows each waiting call as an mcp_approval_request item, and a call decided after waiting names its request in approval_request_id. That item carries text when a summary was asked for and the model has a readable form for it. Core asks for one on every turn unless your request, your key or your organisation says otherwise. Without text its summary is [], which says the model thought and kept it to itself:

{ "id": "item_0KQ2mR...", "type": "reasoning", "summary": [] }

Models your deployment reaches through an OpenAI-compatible chat-completions API behave differently, because that API has no way to ask for a summary:

  • A server that returns its model's thinking sends it whether or not anyone asked. Core puts it where a summary would be: one summary_text of the reasoning item, the same stream events, and a thinking block on POST /messages. It is the model's own thinking, word for word and as long as it ran, not a summary written for a reader. Core returns it only on a turn that asks for a summary; on any other turn it is dropped, though its tokens are still counted in usage.
  • A server that returns no thinking gives Core nothing to build the item from, so output has no reasoning item at all, not one with an empty summary.

An Azure deployment declared on chat completions (the Azure provider of a deployment that does not set the Responses API) is neither of these: it sends no summary and no thinking, so a request that asks for a summary there is refused with a 400 naming reasoning.summary (thinking on POST /messages) rather than answered without one. The summary Core asks for on your behalf, when your key or organisation says so, is never refused, and neither is "summary": "none".

Asking for a summary does not guarantee one. OpenAI writes reasoning summaries only for an account it has verified. On a deployment whose OpenAI account is not, a turn that asks for a summary on an OpenAI model still answers 200, without the summary text.

To get no summary on one turn, send "reasoning": {"summary": "none"} on POST /responses, or "thinking": {"type": "adaptive", "display": "omitted"} on POST /messages. Both outrank your key's and your organisation's setting. On an agent that fixes reasoning, the agent's value runs instead and the response names reasoning as overridden.

Where an answer's sources are

When a turn used a knowledge tool or builtin__web_fetch, the sources travel in two places.

The tool's own result lists everything it read, whether or not the model went on to cite it:

{
  "type": "function_call_output",
  "call_id": "call_0KQ...",
  "name": "knowledge__handbook",
  "output": "[1] …",
  "citations": [{ "kind": "file", "ref": "chk_0KQ…", "title": "Handbook" }]
}

kind is url for a page the model fetched -- ref is then an address you can open -- and file for a passage out of your own indexed content, where ref is the id of the chunk it came from. A tool that reads no sources answers [].

That item is on the conversation, not in the answer: read it back with GET /threads/{thread_id}/items (Continue a conversation). The answer itself carries the round as one mcp_call, the Responses API's item for a tool the server ran for you, and an mcp_call has nowhere to put a source list — which is why the annotations below are the answer's own record of what it cited.

The assistant's text says where it cited them, as annotations on the text part:

{
  "type": "output_text",
  "text": "Renewal is yearly [1].",
  "annotations": [
    { "type": "file_citation", "file_id": "chk_0KQ…", "filename": "Handbook", "index": 18 }
  ]
}

The offsets are into that same text: index for a file_citation, start_index and end_index for a url_citation, which also carries url and title. They cover the [1] marker itself, so you can turn it into a link without searching the text for it. Offsets count Unicode code points, not UTF-8 bytes and not the UTF-16 units a JavaScript string indexes by: in JavaScript, slice Array.from(text) rather than text itself when the answer may hold an emoji or another character outside the Basic Multilingual Plane.

When a turn searches more than once, the numbers carry on: a second search after three passages hands the model [4] and [5], so an answer citing both searches is annotated with each marker's own source. A turn that runs in the background (background: true) carries no annotations; its sources are on each result.

Core never guesses. A marker naming a passage the turn never retrieved gets no annotation and is left in the text as the model wrote it. A page the model read and never cited is on the result and in no annotation.

Prompt caching

A model provider can keep the start of a request it has just processed. When the next request begins with exactly the same content, the provider reads that part from its cache instead of processing it again, which is faster and cheaper. Storing it in the cache can cost extra.

How this happens depends on the model:

  • Claude models. Core asks the provider to cache the start of the request for you, whenever a later call is likely to read it back: a turn that offers tools (every tool round sends the whole request again), every model call after the first within one turn, and every turn of a stored conversation. A single call without tools on a stateless turn is not cached. An entry lasts about five minutes after it was last read.
  • Other models (OpenAI, including Azure OpenAI, Gemini and Scaleway). The provider caches on its own where it supports caching; neither you nor Core sends anything for it.

A request shorter than the provider's minimum is not cached at all, and costs nothing extra.

What the response tells you

usage.input_tokens_details says what caching did for this turn. Both numbers are already part of input_tokens:

Field Meaning
cached_tokens input the provider read from its cache, charged at the model's cached rate
cache_write_tokens input the provider wrote into its cache, charged above the plain input rate; only Claude models report it, other models answer 0

The same figures are recorded as the usage kinds cached_tokens and cache_write_tokens, and priced with the model's cached_input_eur_per_million_tokens and cache_write_eur_per_million_tokens (what your usage cost). On Claude, writing an entry costs about a quarter more than sending the same input once, and reading it back costs a fraction of that, so caching pays off once an entry is read at least once.

Get more from the cache

The cache only matches identical content from the very start of the request, in the order tools, instructions, messages. A change early in the request starts the cache over for everything after it. Between the turns of one conversation, keep these the same:

  • instructions;
  • the agent and its version, when you name one;
  • the tools and skills the turn offers, which a policy rule can also change;
  • reasoning.

Some things start the cache over whatever you send:

Response status

Check the response's status even when the HTTP request succeeds:

status Meaning
completed The turn finished. If its last output item is a function_call, your application executes that tool and sends the result in the next turn.
failed The model call failed or timed out. error.message contains a fixed Core message, not the provider's error text.
incomplete The turn stopped before it was finished: the model reached its output limit, the turn reached its tool-round limit, a live check withheld the answer, or a background run reached its cost limit. incomplete_details.reason says which (below).
requires_action A background run is paused until you submit tool results, or until you decide each call it holds for approval.
queued, in_progress A background run is waiting or running.
cancelled A background run or a streamed turn was cancelled.

A model failure is reported in the response body because Core accepted the request and ran the turn. Validation and permission failures use HTTP error codes.

A cut-off answer

An incomplete response carries incomplete_details.reason:

reason What happened What you can do
max_output_tokens The model stopped at its output limit: your max_output_tokens, or the model's own maximum. The text written so far is in output, on a message item with status: "incomplete". A tool call that was cut off mid-way is not returned. Ask for a shorter answer, raise max_output_tokens, or continue from the text you have.
max_tool_rounds The turn used all its tool rounds. Raise max_tool_rounds, or send a follow-up turn.
usage_cap_exceeded Your key's or your organisation's monthly cap was reached while the turn ran. What it produced until then is in output and is billed. Wait for the month to turn or have the cap raised; until then the next request answers 429.
cost_limit A background run, the agent tasks it started and their runs together reached the run's cost limit. What was produced until then is in output and is billed. Send a new run with a higher cost_limit_eur, or continue from what you have.
content_filter A live check of the agent withheld the answer. When every blocking output check of the agent is a pattern, a streamed answer stops where the forbidden text starts; otherwise, streamed or not, output holds no answer. The X-Core-Check header, or GET /responses/{id}/checks on a streamed turn, names the check. Show the person that no answer can be given. Ask your organisation's administrator about the check when it stops answers it should not.

A streamed turn ends with response.incomplete carrying the same object. On POST /messages a cut-off answer is stop_reason: "max_tokens", a withheld answer is stop_reason: "refusal", and a turn out of tool rounds or stopped at a cap is stop_reason: "pause_turn": the vendor has no word for a cap. A cost limit belongs to a background run, which POST /messages does not start.

Where your deployment's catalogue states a model's maximum output, a higher max_output_tokens is lowered to that maximum when the model is called. The response still shows the value you sent.

Where your organisation has a routing configuration, a model that is unavailable does not end the turn: Core runs it on the next model instead, and model names the one that answered. See Fall back to another model.

Storage and HTTP status

For a non-streaming request, 201 with a Location header means the response was stored. Retrieve it with GET /responses/{id}. A stateless call returns 200 and does not store conversation content. Storage depends on the threads capability and store setting; see Continue a conversation.

Stream the response

Set "stream": true to receive output as Server-Sent Events:

curl -N -X POST "$CORE_URL/responses" \
    -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
    -d '{"model": "<a model id>", "input": "Explain bearer tokens.", "stream": true}'

Each event contains an event: line, a data: line with JSON and a blank line. The JSON includes a sequence_number, starting at 0, that you can use to detect missing events.

Event Handling
response.created Save the response id.
response.in_progress Show that the turn is running.
response.output_item.added Start an item at output_index.
response.content_part.added Start the item's text part.
response.output_text.delta Append delta to the text received so far.
response.output_text.annotation.added One source the finished text cites, with the offsets citing it (above).
response.output_text.done Read the complete text for the item.
response.content_part.done Finish the text part.
response.output_item.done Process the completed item, such as a function_call.
response.completed, response.incomplete, response.failed Read the final response object. The stream ends here. After a cancel it is response.completed with status: cancelled.
error The turn ran but could not be stored. Read code; no result was kept.

Ignore comment lines such as : keepalive. They keep the connection open.

Model text and a reasoning summary are streamed in fragments. A thinking model's reasoning item comes first: response.output_item.added, then response.reasoning_summary_part.added, one response.reasoning_summary_text.delta per piece as the model thinks, then response.reasoning_summary_text.done, response.reasoning_summary_part.done and response.output_item.done, before the message begins. On POST /messages the same reasoning arrives as a thinking block filled with thinking_delta events. A model that thinks again after it has started its reply gets a second reasoning item: the message is finished first, the new reasoning item is added at the next output_index and streamed the same way, and text that follows it is a second message. output lists the items in that order, so read every message item and join their text, as the OpenAI SDK's output_text does, not only the first one. On POST /messages that is a second thinking block and a second text block. Refusals and tool calls arrive as complete items, with response.output_item.added followed by response.output_item.done. If the model fails during output, Core completes the current item with the text received, marks that item status: incomplete, and ends with response.failed.

A started stream uses HTTP 200 and Content-Type: text/event-stream, including when the response will be stored. The headers are sent before the turn runs. Retrieve a stored result using the id from the first event.

Errors detected before streaming starts, such as an invalid request or disallowed model, still return an ordinary HTTP status, with OpenAI's error object as the body. Model failures after streaming starts appear as response.failed events.

If the connection closes, the turn continues and its usage is recorded. If storage is enabled, fetch the result afterwards with GET /responses/{id}. To make the turn stop, cancel it (below). Combining streaming with a background run returns 400.

Use a client that supports Server-Sent Events. The generated Python client only supports non-streaming responses, and the Swagger UI explorer shows a body only once the response has finished. If an SSE client still receives every event at once, ask your deployment administrator to check whether a proxy is buffering them, or whether the model's vendor filters streamed output before releasing it (Azure OpenAI does unless the deployment's content filter runs in asynchronous mode).

Stop a streamed turn

A stop button in your application cancels the turn with the id from response.created. No extra capability is needed: the key that started the turn may also stop it. Cancelling another key's turn or a background run needs runs; without it you get 404.

curl -s -X POST "$CORE_URL/responses/resp_0KQ2mR.../cancel" -H "Authorization: Bearer $CORE_KEY"

Core stops the turn at whatever it is waiting on: the model still sending text, or a tool still running. The answer is the stored response object with status: cancelled. It holds the items the turn had already made. A message the model was still writing is in it with the text that had arrived and status: incomplete. A client still reading the stream receives that same response as the last event, response.completed.

What is billed:

  • every round that had finished, as always;
  • for the model call that was cut: what the vendor had already reported it using. Some vendors report the input tokens at the start, others only at the end. If the vendor had reported nothing yet, nothing is recorded for that call.

Cancelling again returns the same answer, without an error. If the turn had finished before the cancel reached it, you get 409. If the turn has not stopped within a few seconds, you get 503; just cancel again. A turn without storage has no output in the cancel's answer, because Core keeps none. The stream itself shows it.

POST /messages has no cancel.

Common errors

HTTP status Cause
400 The request was refused: a field Core cannot honour, a missing one, or one an agent fixes. error.param names it. On model it can also be a retired model your policy blocks.
401 The API key secret is missing or invalid.
403 The key's policy denies the model, the organisation is suspended, or a hosting requirement of the request or the key is not met (see below).
403 on agent The key's policy requires an agent and the request names none. Name one of the key's agents.
404 A referenced model, prompt, skill or file is unavailable, or its capability is disabled.
409 The idempotency key was previously used for a stateless call.
503 A required dependency is unavailable.

Each of these carries OpenAI's error object rather than problem+json, and so do GET /responses/{id}, POST /responses/{id}/tool_outputs and POST /responses/{id}/cancel. See Errors, paging and retries for the fields and retry handling.

Hosting refusals

A hosting requirement, or the key's regions_allow policy, fails with 403 and exactly one of these four detail messages:

detail What you can change
The requested hosting region is not allowed for this key. Request a region in your key's regions_allow policy (Keys), or drop hosting.
Where this model runs is not known. This should not happen for a model GET /models lists; report it to your administrator.
The model is not hosted in a region this key allows. Pick a model from GET /models whose region or country is in your key's list, or ask your operator to widen it.
The model is not hosted in the requested region. Drop hosting.region, or pick a model that does run there (Models shows each model's hosting).

Timeouts

A synchronous turn can run for up to five minutes, including tool rounds. Set your client timeout to accommodate this, or use a background run if you cannot keep the connection open.