Agents API
POST /v1/agents/{id}/chat/completions — run a stored agent. The request body is a normal chat completion payload, but the agent supplies the model, system prompt (free-text or a library prompt), skills, tools/MCP and optional knowledge-base retrieval. The model field in your body is ignored.
See Agents for how to compose one.
An agent whose model is realtime-capable can also be spoken to — see Realtime.
Three kinds of agent need the acting end user in X-Relay-User-Id (an Entra object id; X-User-Id is accepted as a legacy alias), and it is the same header in every case, so an agent that is two of these needs one header, not two:
- A dataset agent, which also needs the
data.queryscope on the key, cannot stream, and returns arelay_datasetobject with the SQL that ran and the rows it returned. See Dataset agents. - A connector agent, whose tools read that user's own Microsoft 365 account and which needs the
connectors.usescope. See Connectors. - An agent that is not public, where the gateway checks the user against the agent's creator and its share list. See Agents for how the audience is set and why that id and not another.
Without the header these return 400 user_header_required; without the scope, 403 insufficient_scope.
#List agents
GET /v1/agents — the agents this key can run, so an app can discover them instead of hard-coding ids.
curl http://localhost:5300/v1/agents \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "X-Relay-User-Id: $END_USER_ID"
The user header is optional here even though a private agent's run requires it: listing without one returns the public agents, which is the useful answer for a client that has not yet picked a user.
{ "object": "list", "data": [
{
"id": "a1b2c3…",
"name": "support-assistant",
"description": "Answers returns and delivery questions",
"model": "gpt-4o",
"model_allowed": true,
"realtime": false,
"published_version": 3,
"grounded": true,
"retrieval_top_k": 4,
"skills": 2,
"tools": 1,
"mcp_servers": 0,
"dataset": false,
"connectors": false,
"public": true,
"requires_user_header": false,
"user_header": null,
"temperature": 0.3,
"max_tokens": 800,
"pii_redaction": false,
"web_search": false
}
] }
Only enabled agents appear, and only those that are global (no workspace) or owned by the key's workspace — the same visibility rule as the prompt library. Non-public agents appear only when X-Relay-User-Id names their creator or a user they are shared with.
A few fields are worth knowing:
| Field | Meaning |
|---|---|
model_allowed | false when the key's model allow-list excludes the agent's model, so a run would return 403 model_not_allowed. Reported rather than filtered on — an agent hidden for this reason looks deleted, while an unexplained 403 looks like a bug. |
realtime | Whether the agent's model speaks the bidirectional-audio protocol, so the agent can be opened as a voice session. Reported here because the agent's model may not be in the key's own GET /v1/models view, so an app offering a "talk to an agent" picker could otherwise only discover the answer from a 400 unsupported_capability after the user had already chosen. Filter on this, unlike model_allowed — an agent that cannot be spoken to is not a permissions problem to explain, it simply has no voice option. |
published_version | The published version that will actually run. Null means the live draft is in effect, because a run prefers a published snapshot whenever one exists. |
grounded | Whether a knowledge base is attached. retrieval_top_k is present only when it is — otherwise it is a stored default that never takes effect. |
public | false when the agent is shared with named users rather than open to every caller. Because the listing already omits the ones this user may not run, a false here means "you can see this because the user you named created it or was shared it", not "you might be refused". |
dataset / connectors | Whether a dataset is pinned / a connector is attached — the two kinds of agent that act as the end user and need an extra scope. |
requires_user_header | Whether a run needs X-Relay-User-Id. True for a dataset agent (to resolve per-user data access), a connector agent (its tools act as that user) or a non-public one (to check the share list); any one reason is enough, and user_header gives the header name. |
web_search | Whether the agent can search the web during a run. Needs no extra scope or header. See Agents. |
skills / tools / mcp_servers | Counts, not ids. |
This is a directory, not a definition: the system prompt, the prompt id and the skill / tool / MCP ids are deliberately not returned. They are authoring detail, the prompt can carry sensitive instructions, and the ids name resources that may belong to another team. Nothing but id is needed to invoke an agent. Compose and inspect agents in the panel under Agents.
#Request
curl -X POST http://localhost:5300/v1/agents/AGENT_ID/chat/completions \
-H "Authorization: Bearer $RELAY_API_KEY" -H "Content-Type: application/json" \
-d '{
"messages": [{ "role": "user", "content": "How do I return an item?" }],
"stream": false
}'
- The agent's settings win. Its model replaces whatever
modelyou send, its temperature and max tokens apply as defaults, and if it has tools of its own they replace anytoolsin your body. - The message is optional. If the agent's prompt already contains the user turn (a chat-type prompt), or the system prompt is self-contained, you can omit it.
- Variables. If the agent's prompt has
{{variables}}, pass them as a top-levelvariablesobject; the gateway compiles the system prompt before running (the variables are not forwarded upstream):
{ "messages": [{ "role": "user", "content": "…" }],
"variables": { "company": "Spinneys", "language": "English" } }
- Attachments. A top-level
file_idsarray names uploaded files whose text is injected after any retrieved knowledge; the agent's PII redaction applies to it. An unreadable id fails the run with404 file_not_foundrather than being skipped. - Images go in as content parts exactly as on chat completions. Thread history keeps text only, so an image is not remembered on the next turn.
#Conversation memory
Add ?thread=<id> to give the agent multi-turn memory. Prior turns for that thread are prepended, and this turn is persisted afterwards (only on success, and only role + content). The response includes X-Relay-Thread-Id, and the thread id doubles as the trace's session id unless you sent X-Relay-Session-Id yourself.
POST /v1/agents/{id}/chat/completions?thread=my-thread-123
Threads are visible in the panel under Conversations.
#Tools & MCP
If the agent has MCP servers, workflows, a connector or a dataset attached, the gateway runs a tool loop: it calls the model, executes any requested tool itself, feeds the result back, and repeats. The budget is Gateway:ToolMaxIterations (default 5) for MCP and workflow tools, Gateway:Connectors:MaxIterations (default 8) for a connector agent and Gateway:Data:MaxIterations (default 6) for a dataset agent. Plain registry tools (no backend of their own) are advertised to the model, and their tool_calls are returned to you to execute. A registry tool whose name collides with an attached workflow or connector tool fails the run with 409 tool_name_conflict. See Skills, tools & MCP.
#Streaming vs. tool loop
- An agent with none of the above (no MCP, workflow, connector or dataset) and no
?thread=streams like a normal chat call. - Otherwise the run is non-streamed and returns a single JSON completion:
stream: trueis ignored, except for a dataset agent, which refuses it with400 stream_not_supportedso the caller knows.
#Response
Standard chat completion shape, plus relay_web_search on a non-streamed run of a web search agent that searched (queries, pages found, citations and billed search count, summed over the tool loop — the shape is in Chat completions), plus X-Relay-Request-Id, X-Relay-Trace-Id, X-Relay-Observation-Id, X-Relay-Cost-Usd (non-streamed runs only), X-Relay-Cache when caching is on, X-Relay-Thread-Id for threads, and X-Relay-Dataset-Id / X-Relay-Sql-Queries / X-Relay-Rls-Applied for a dataset agent.