Chat API Reference
This page documents the request and response schemas for the Chat API. For endpoint URLs and quickstart examples, see Getting Started.
Request Schema
Both the streaming (POST /chat/stream) and non-streaming (POST /chat/response) endpoints accept the same request body:
{
"agent_identifier": "string (required)",
"conversation": [ChatMessage],
"conversation_context": ConversationContext,
"bot_params": {},
"stateful": false,
"session_id": "string"
}
| Field | Type | Required | Description |
|---|---|---|---|
agent_identifier | string | Yes | The identifier of the agent to invoke |
conversation | ChatMessage[] | Yes | The conversation history — full history in stateless mode, only the new user message(s) on stateful follow-ups |
conversation_context | ConversationContext | No | Top-level context for the request |
bot_params | object | No | Runtime parameters for the agent (see below) |
stateful | boolean | No | Persist the agent's working state under the session (default false) — see Conversations |
session_id | string | No | The conversation identifier. On the first turn, omit it (the service assigns one and returns it on the agent's message) or send a new UUID; echo it on later requests, see Conversations |
Query Parameters
| Parameter | Required | Description |
|---|---|---|
tenant | Yes | The tenant to run the request against |
index_id | No | Index identifier passed through to the agent |
resume | No | Set true to resume an interrupted stateful turn — see Resuming an Interrupted Turn |
message_id | No | The turn to resume; only honored together with resume=true. New turns always get a server-assigned message_id |
request_timeout | No | POST /chat/response only: a budget in seconds; a turn that exceeds it is cancelled and answered with 504 |
delta | No | Streaming only: set true to receive Open Responses events instead of full snapshots. Also accepted by GET /chat/stream/{message_id} |
Send Message Schema
POST /chat/stream/messages?tenant={tenant} sends a message or approval decisions to a stateful conversation, whether or not a turn is running. See Sending to a Conversation.
{
"session_id": "string (required)",
"agent_identifier": "string (required)",
"message": ChatMessage,
"tool_decisions": [ToolDecision],
"conversation_context": ConversationContext,
"bot_params": {},
"user_agent_id": "string"
}
| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | The conversation |
agent_identifier | string | Yes | The agent of the turn this request starts when none is running |
message | ChatMessage | No | A user message, with any content parts (text, images, attached documents). Its message_id, when set, comes back on the user_message part where the agent read it |
tool_decisions | ToolDecision[] | No | Answers to calls waiting for approval |
conversation_context, bot_params, user_agent_id | No | As in the request schema; used only by a turn this request starts |
ToolDecision:
| Field | Type | Required | Description |
|---|---|---|---|
tool_call_id | string | Yes | The waiting call's tool_call_id |
decision | "allow_once" / "allow_session" / "deny" | Yes | See Approving Tool Calls |
params | object | No | Replaces the call's inputs when allowing |
The response is 202 with {"message_id": "string", "started": boolean}: the turn that reads what was sent, and whether this request started it. message_id is absent when nothing was sent, nothing waits, and no turn is running.
ChatMessage
A single message in the conversation. The sender field is "user" for user messages and "bot" for agent messages:
{
"sender": "user" | "bot",
"content": "string",
"message_id": "string",
"session_id": "string",
"content_parts": [ContentPart],
"evidences": [ChatMessageEvidence],
"function_call_request": FunctionCallRequest,
"function_call_response": FunctionCallResponse,
"function_specs": FunctionSpec,
"image_uri": "string"
}
| Field | Type | Required | Description |
|---|---|---|---|
sender | "user" or "bot" | Yes | Message author — "bot" denotes the agent's response |
content | string | Yes | The text content of the message |
message_id | string | No | Unique turn identifier (assigned by the server; used for stream reconnection and feedback) |
session_id | string | No | Conversation identifier (the one the client sent on the first turn, or assigned by the server; echo it on follow-up requests, see Conversations) |
content_parts | ContentPart[] | No | Structured message parts (context, text, tool calls, tables, images, messages read mid-turn) |
evidences | ChatMessageEvidence[] | No | Citation evidence linking answer spans to sources |
function_call_request | object | No | Deprecated. Legacy function call suggestion — use tool content parts instead |
function_call_response | object | No | Deprecated. Legacy function call result — use tool content parts instead |
function_specs | object | No | Deprecated. Function specifications for legacy function call pattern |
image_uri | string | No | Deprecated. Use content parts for media |
ContentPart
Each content part has a type discriminator and one corresponding data field:
| Type | Data Field | Description |
|---|---|---|
"text" | text | A text segment of the agent's response |
"context" | context | A ConversationContext block (documents the agent found or context the client attached) |
"tool" | tool | A tool call event with lifecycle status |
"table" | table | Structured tabular data |
"image" | image | An image on a user message: {"image_uri": "data:<mime>;base64,<data>"} |
"user_message" | text, message_id | In an agent message: a message the user sent while the turn ran, where the agent read it, with the message_id it was sent with |
{
"type": "text" | "context" | "tool" | "table" | "image" | "user_message",
"text": "string",
"context": ConversationContext,
"tool": ContentPartTool,
"table": ContentPartTable,
"image": {"image_uri": "string"},
"message_id": "string"
}
ContentPartTool
Represents a single tool call and its lifecycle. Agents stream these to communicate what they are doing:
| Field | Type | Required | Description |
|---|---|---|---|
tool_call_id | string | Yes | Unique identifier for this tool invocation |
name | string | Yes | Name of the tool being called |
params | object | No | Visible parameters sent to the tool |
response | object | No | Tool execution result (present when completed) |
display_text | string | No | Human-readable description of what the tool is doing/did |
status | "running" / "completed" / "error" | No | Current lifecycle state |
A call that needs the user's approval shows as a part whose response.status is confirmation_required until it is decided; a declined call's response.status is confirmation_denied, or confirmation_unavailable where nobody could be asked. See Approving Tool Calls.
See Content Parts for how to render these in a UI.
ContentPartTable
Structured tabular data returned by an agent:
| Field | Type | Description |
|---|---|---|
format | "row" or "columnar" | How the data is organized |
headers | string[] | Column headers |
rows | object[] | Row data (when format is "row") — each object maps header → value |
columns | object | Column data (when format is "columnar") — maps header → value[] |
Exactly one of rows or columns must be present.
Example (row format):
{
"type": "table",
"table": {
"format": "row",
"headers": ["Document", "Relevance"],
"rows": [
{"Document": "BERT paper", "Relevance": "High"},
{"Document": "GPT-2 paper", "Relevance": "Medium"}
]
}
}
ConversationContext
Context scoping the conversation to specific documents, tags, filters, or custom content. Can appear at the top level of the request or inside a content_parts entry with type: "context".
{
"document_context": {
"document_ids": ["string"],
"retrieval_unit": "document" | "chunk"
},
"custom_context": {
"items": [
{"document_id": "string", "content": "string or object"}
]
},
"tag_context": {
"tag_ids": ["string"]
},
"filter_context": {
"filters": {}
},
"user_document_context": {
"enabled": true
}
}
All fields combine freely.
See Passing Context for usage patterns and examples.
ChatMessageEvidence
A citation linking a span of the answer to a source document:
| Field | Type | Required | Description |
|---|---|---|---|
document_hit_url | string | Yes | URL path to fetch the source document/chunk |
text_extract | string | No | Relevant passage from the source (with <b> highlights) |
anchor_text | string | No | The citation marker in the answer text (e.g., <sup>1</sup>) |
See Handling Citations for rendering patterns.
Response Schema
Non-Streaming Response (ChatResponseItem)
{
"agent_identifier": "string",
"conversation": [ChatMessage],
"conversation_context": ConversationContext,
"function_specs": [FunctionSpec]
}
The response echoes back the full conversation with the agent's response appended as the last message.
Streaming Response (SSE)
The streaming endpoint returns Server-Sent Events. Each event has:
event: new_message
id: {message_id}:{event_index}
data: {ChatMessage as JSON}
retry: 15000
Each event contains the complete message so far — as tokens are generated, the content field grows incrementally. The final event contains the fully formed response including evidences and content_parts. Streams replayed after a reconnection end with a stream_status control event reporting the turn's outcome. With delta=true the events are Open Responses events carrying only what changed.
See Streaming for reconnection, turn resumption, cancellation, and protocol details.
bot_params
The bot_params field allows API clients to pass runtime parameters to the agent at request time. These parameters flow into the agent factory's dependency resolution — specifically, they fill agent constructor parameters that are not already set by the agent's static configuration.
How it works:
- The client sends
bot_params: {"search_scope": "internal"}in the request. - The handler sanitizes the dict (protected keys like
tenant,request_headers,index_idare always stripped). - The agent factory resolves each constructor parameter by checking the agent's static
agent_configurationfirst, then falling back tohandler_params(which includes the sanitizedbot_params). - If the agent class declares a parameter matching a key in
bot_params, and the static configuration does not already set it, the runtime value is used.
Security model:
- Static
agent_configurationalways takes precedence —bot_paramscannot override values the agent developer has locked in configuration. - For dict-typed parameters, values are deep-merged with configuration taking precedence.
- Protected system keys (
tenant,request_headers,index_id,session_id,agent_depth,sub_agent_count,sub_agent_approvals) and every tool rule source's*_rule_source_configurationblock are always stripped regardless of what the client sends, and so is thesub_agent_approvalsfield of the delegation andtasktool configuration blocks. - The sub-agent limits block
sub_agent_limits_configurationis stripped too, so only the agent's static configuration sets it.
When to use:
Use bot_params when the agent developer has designed specific constructor parameters to be overridable at runtime. Common patterns include passing a user-selected scope, language preference, or feature flag. The exact parameters available depend on the specific agent's implementation.
Error Handling
HTTP Errors
Errors from the chat service return JSON with a detail field:
{"detail": "Error description"}
| Status | Meaning | Common Causes |
|---|---|---|
400 | Bad Request | Unknown agent_identifier, agent not streamable for the streaming endpoint, malformed request body, mutually exclusive fields both set |
401 | Unauthorized | Missing or invalid API key / Bearer token (returned by the API gateway) |
403 | Forbidden | Valid credentials but insufficient permissions for this tenant, or an agent the caller may not use (returned before a stream opens) |
404 | Not Found | message_id unknown or expired — or its producer died mid-turn — when reconnecting to or cancelling a stream |
409 | Conflict | POST /chat/stream or a rewind while the conversation has a turn running; the body's detail.message_id names it |
410 | Gone | Reconnecting to a turn that finished and whose stream slot was superseded or reaped: read it from GET /conversations/{session_id} instead |
422 | Validation Error | Request body fails schema validation; or, on POST /chat/response, the model provider's content filter refused the request (body {"detail": "...", "code": "content_filter"}; do not retry the same input) |
429 | Too Many Requests | POST /chat/response hit the model provider's rate limit; retry after the Retry-After header when present |
500 | Internal Server Error | Unexpected server-side failure |
504 | Gateway Timeout | POST /chat/response exceeded the request_timeout the caller set; the turn was cancelled |
401 responses come from the API gateway and use {"message": "Unauthorized"} rather than {"detail": "..."}.
Validation Errors (422)
Validation errors include per-field details in the standard FastAPI/Pydantic format:
{
"detail": [
{
"loc": ["body", "conversation", 0, "sender"],
"msg": "value is not a valid enumeration member",
"type": "value_error.enum"
}
]
}
Streaming Errors
If the turn fails during an active SSE stream, an error event is sent with a JSON data field holding a code (RATE_LIMITED, CONTENT_FILTERED, AGENTS_STORAGE_NOT_CONFIGURED or INTERNAL) and a message. These are the streaming counterparts of the 429, 422 content_filter and 400 responses of POST /chat/response:
event: error
data: {"code": "CONTENT_FILTERED", "message": "..."}
See Error Handling for what each code means and when to reconnect.
Rewinding a Conversation
POST /conversations/{session_id}/rewind?tenant={tenant} with {"keep_count": n} keeps the conversation's first n messages and drops the rest, so a client can regenerate an answer or resend an edited prompt as the next turn. The agent's stored working state is cut at the same point; messages and approval decisions sent to the conversation and not yet read are dropped, and tools allowed for the rest of the conversation (allow_session) ask again. Only the owner can rewind, and not while a turn is running (409). The response is the conversation as kept.
Stream Control
| Endpoint | Purpose |
|---|---|
GET /chat/stream/{message_id} | Reconnect to a running or finished turn, see Reconnection |
DELETE /chat/stream/{message_id} | Cancel a running turn, see Cancellation |
DELETE /chat/stream/messages/{message_id} | Take a sent message back out of the conversation's inbox before a turn reads it, see Sending to a Conversation |
Realtime Transcription
WS /chat/ws/openai/v1/realtime?intent=transcription&tenant={tenant} streams microphone audio and returns live transcripts. It speaks the OpenAI Realtime transcription protocol, so an OpenAI Realtime client with its base URL set to <host>/chat/ws/openai/v1 connects unchanged. The server relays events between the client and the provider and changes only session.update: the session type is forced to transcription and the model to the configured one. A client's own language or languages wins over the configured hint. Non-JSON frames are dropped. Authenticate as for the other endpoints, see Getting Started.
Close Codes and Errors
Errors arrive as an OpenAI Realtime error event ({"type": "error", "event_id": ..., "error": {"type", "code", "message"}}) before the socket closes.
| Close code | error.type / error.code | Cause |
|---|---|---|
1000 | dictation_limit_reached / time_limit | The session reached max_session_seconds of wall-clock time |
1000 | dictation_limit_reached / audio_limit | The client sent audio more than 10 seconds ahead of real time |
1000 | none | The client or the provider ended the session |
1011 | server_error / transcription_unavailable | The provider failed after the socket opened (bad key, 401/429, unreachable) |
4404 | none | Realtime transcription is not set up for the tenant, or its LLM configuration is missing or not a realtime_transcription model |
Configuration
The endpoint uses an LLM configuration of type realtime_transcription, with vendor openai or azure_openai. On Azure, name is the deployment name.
{
"name": "dictation",
"vendor": "openai",
"vendor_configuration": { "openai": { "openai_api_key_ref": "openai-api-key" } },
"model_configuration": {
"name": "gpt-4o-transcribe",
"type": "realtime_transcription",
"transcription_settings": { "languages": ["nl"], "max_session_seconds": 300 }
}
}
transcription_settings field | Type | Default | Description |
|---|---|---|---|
languages | list[str] or null | null | Language hints such as ["nl", "en"]; null lets the model detect them. whisper-1 and the gpt-4o transcribe models take only the first one |
max_session_seconds | int | 300 | Wall-clock cap per session |
In a local project, realtime_transcription_setup.json (next to agent_setups.json) names the configuration from llm_configurations.json; za agents serve mounts the endpoint and reads the file on every session, so edits apply without a restart:
{ "llm_configuration_name": "dictation" }
Install the realtime extra: pip install zetaalpha.agents[realtime]. A custom host (Extending the API Server) mounts the router itself and passes a setup retriever and an LLM configuration store factory to setup_app:
from zav.agents_sdk import setup_app
from zav.agents_sdk.controllers import routers
from zav.agents_sdk.controllers.v1.realtime_transcription import realtime_transcription_router
from zav.agents_sdk.domain.realtime_transcription import FileRealtimeTranscriptionSetupRetriever
app = setup_app(
...,
llm_configuration_store_factory=my_llm_configuration_store_factory,
realtime_transcription_setup_retriever=FileRealtimeTranscriptionSetupRetriever(
"realtime_transcription_setup.json"
),
routers=list(routers) + [("", realtime_transcription_router)],
)
A retriever is a RealtimeTranscriptionSetupRetriever whose get(tenant) returns the tenant's RealtimeTranscriptionSetup, or None to turn the endpoint off for that tenant.
Deprecated Fields
| Field | Replacement |
|---|---|
function_call_request / function_call_response | content_parts with type: "tool" — see Content Parts |
function_specs | Tool specifications are now configured server-side |
image_uri | Media should be handled via content parts |