Files
coder/docs/ai-coder/agents/chats-api.md
T
Ethan 4751416b29 fix!: persist structured chat errors (#24919)
**Breaking change for changelog:**

> `codersdk.Chat.last_error` now returns a structured `ChatError` object
(`{message, kind, provider, retryable, status_code, detail}`) instead of
a plain string. The chats API is experimental
(`/api/experimental/chats`), so this ships without a deprecation cycle;
consumers reading `chat.last_error` as a string must update to read
`chat.last_error.message`. SDK/generated TypeScript terminal error
payloads now use the single `ChatError` type; the live stream error
payload type is renamed from `ChatStreamError` to `ChatError`.

Persisted chat errors now carry the same provider-specific detail (kind,
provider, retryable, HTTP status, optional detail) as the live stream,
so refreshing a failed chat rehydrates with the full structured error
instead of a one-line headline.

Existing rows are migrated in place: legacy text errors are wrapped into
`{message, kind: "generic"}` so already-errored chats still render, and
rows with `last_error IS NULL` stay NULL. Internally, persisted fallback
decoding now reuses the existing `chaterror.KindGeneric` constant, with
no JSON value change.

Closes CODAGT-239
2026-05-05 12:56:06 +10:00

15 KiB

Chats API

Note

The Chats API is in beta. Endpoints live under /api/experimental/chats and may change without notice.

The Chats API lets you create and interact with Coder Agents programmatically. You can start a chat, send follow-up messages, and stream the agent's response — all without using the Coder dashboard.

Authentication

All endpoints require a valid session token:

curl -H "Coder-Session-Token: $CODER_SESSION_TOKEN" \
  https://coder.example.com/api/experimental/chats

Quick start

Create a chat with a single text prompt:

curl -X POST https://coder.example.com/api/experimental/chats \
  -H "Coder-Session-Token: $CODER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "<your-org-id>",
    "content": [
      {"type": "text", "text": "hello world"}
    ]
  }'

The response is the newly created Chat object:

{
  "id": "a1b2c3d4-...",
  "organization_id": "...",
  "owner_id": "...",
  "workspace_id": null,
  "build_id": null,
  "agent_id": null,
  "parent_chat_id": null,
  "root_chat_id": null,
  "last_model_config_id": "...",
  "title": "hello world",
  "status": "waiting",
  "diff_status": null,
  "created_at": "2025-07-17T00:00:00Z",
  "updated_at": "2025-07-17T00:00:00Z",
  "archived": false,
  "pin_order": 0,
  "mcp_server_ids": [],
  "labels": {},
  "has_unread": false,
  "client_type": "api"
}

If a chat later ends in error, the same Chat shape includes a structured last_error object. For brevity, unchanged nullable IDs are omitted here:

{
  "id": "a1b2c3d4-...",
  "title": "hello world",
  "status": "error",
  "last_error": {
    "message": "Azure OpenAI is rate limiting requests.",
    "kind": "rate_limit",
    "provider": "azure",
    "retryable": true,
    "status_code": 429,
    "detail": "Retry after 30 seconds."
  },
  "created_at": "2025-07-17T00:00:00Z",
  "updated_at": "2025-07-17T00:00:30Z",
  "archived": false,
  "pin_order": 0,
  "mcp_server_ids": [],
  "labels": {},
  "has_unread": false,
  "client_type": "api"
}

The agent begins processing the prompt asynchronously. Use the stream endpoint to follow its progress.

Core workflow

A typical integration follows three steps:

  1. Create a chatPOST /api/experimental/chats with your prompt.
  2. Stream updates — Open a WebSocket to GET /api/experimental/chats/{chat}/stream to receive real-time events as the agent works.
  3. Send follow-upsPOST /api/experimental/chats/{chat}/messages to add messages to the conversation. Messages are queued if the agent is busy.

Endpoints

Create a chat

POST /api/experimental/chats

Field Type Required Description
content ChatInputPart[] yes The user's prompt as one or more content parts.
organization_id uuid yes The organization this chat belongs to.
workspace_id uuid no Pin the chat to a specific workspace.
model_config_id uuid no Override the default model configuration.
mcp_server_ids uuid[] no Attach MCP servers to this chat.
labels map[string]string no Key-value labels for the chat (max 50).
client_type string no "ui" or "api". Defaults to "api".

Each ChatInputPart has a type field. The simplest form is a text part:

{"type": "text", "text": "Fix the failing tests in the auth service"}

Other part types include file (an uploaded image referenced by its file_id) and file-reference (a pointer to a file with optional line range).

Response: 201 Created with a Chat object.

Send a message

POST /api/experimental/chats/{chat}/messages

Field Type Required Description
content ChatInputPart[] yes The follow-up message content.
model_config_id uuid no Override the model for this turn.
mcp_server_ids uuid[] no Override MCP servers for this turn.

If the agent is currently processing, the message is queued automatically. The response indicates whether the message was delivered immediately or queued:

{
  "queued": false,
  "message": { "id": 42, "chat_id": "...", "role": "user", "created_at": "...", "content": [...] }
}

When queued is true, message is absent and queued_message is returned instead.

Edit a message

PATCH /api/experimental/chats/{chat}/messages/{message}

Edits a previously sent user message. The agent re-processes from the edited message onward, truncating any messages that followed it.

Field Type Required Description
content ChatInputPart[] yes The replacement message content.

The response is an EditChatMessageResponse with the edited message and an optional warnings array. When file references in the edited content cannot be linked (e.g. the per-chat file cap is reached), the edit still succeeds and the warnings array describes which files were not linked.

Stream updates

GET /api/experimental/chats/{chat}/stream

Opens a one-way WebSocket connection. The server sends events; clients must not write to the socket (doing so closes the connection).

Query parameter Type Required Description
after_id int64 no Only return events after this message ID.

Each WebSocket message is a JSON envelope with an outer type ("ping", "data", or "error") and an optional data field. For "data" envelopes the payload is a JSON array of event objects:

{
  "type": "data",
  "data": [
    {"type": "status", "chat_id": "...", "status": {"status": "running"}},
    {"type": "message_part", "chat_id": "...", "message_part": {"...":"..."}}
  ]
}

Ignore "ping" envelopes (keepalives sent every ~15 s). On first connect the server sends an initial snapshot of the chat state before switching to live events. Use after_id when reconnecting to skip messages the client already has.

Connecting to the stream also updates the caller's read cursor for unread tracking. On disconnect the cursor is advanced to the latest message.

Event types inside each batch:

Type Description
message_part A chunk of the agent's response (text, tool call, etc.).
message A complete message has been persisted.
status The chat status changed (e.g. running, waiting).
error An error occurred during processing.
retry The server is retrying a failed LLM call (includes backoff).
queue_update The queued message list changed.

Watch all chats

GET /api/experimental/chats/watch

Opens a one-way WebSocket that pushes events for all chats owned by the authenticated user. Use this to drive a sidebar or notification indicator without polling.

Each event is a JSON object with kind and chat fields:

Kind Description
created A new chat was created.
status_change A chat's status changed.
title_change A chat's title was updated.
diff_status_change A chat's diff/PR status changed.
deleted A chat was deleted.

List chats

GET /api/experimental/chats

Returns all chats owned by the authenticated user. The files field is populated on POST /chats and GET /chats/{id}. Other endpoints that return a Chat object omit it.

Query parameter Type Required Description
q string no Search query string.
label string no Filter by label as key:value. Repeat for multiple (AND logic).

Get a chat

GET /api/experimental/chats/{chat}

Returns the Chat object (metadata only, no messages). The response includes a files field (ChatFileMetadata[]) containing metadata for files that have been successfully linked to the chat. File linking is best-effort; if linking fails, the file remains in message content but will be absent from this field.

When file linking is skipped (e.g. the per-chat file cap is reached), POST /chats includes a warnings array on the Chat response and POST /chats/{chat}/messages includes a warnings array on the CreateChatMessageResponse. The warnings field is omitempty and absent when all files are linked successfully.

Get chat messages

GET /api/experimental/chats/{chat}/messages

Returns messages for a chat in descending ID order (newest first).

Query parameter Type Required Description
before_id int64 no Only return messages with id < before_id.
after_id int64 no Only return messages with id > after_id.
limit int32 no Page size, 1 to 200. Defaults to 50.

Results are returned in descending ID order (newest first), except when only after_id is set: that shape is intended for polling and returns ASCENDING ID order so a client can advance its cursor to the largest returned ID without gaps. When both cursors are set they must satisfy after_id < before_id; otherwise the server returns 400 Bad Request.

queued_messages is only populated on the initial load (no cursor). Pass either cursor to page through history or to poll for new messages without receiving the queued snapshot on every request. The has_more flag indicates more rows exist beyond this page in the same direction.

List models

GET /api/experimental/chats/models

Returns available models. Use this to discover valid values for model_config_id.

Update a chat

PATCH /api/experimental/chats/{chat}

Updates chat metadata. All fields are optional; omitted fields are left unchanged.

Field Type Description
title string Set a new title.
archived bool true to archive, false to unarchive. Archiving clears pin_order.
pin_order int32 0 to unpin; >0 on an unpinned chat to pin it; >0 on a pinned chat to reorder.
labels map[string]string Replace all labels. Use null/omit to leave unchanged, {} to clear.

Response: 204 No Content.

Regenerate title

POST /api/experimental/chats/{chat}/title/regenerate

Regenerates the chat title using conversation context. Returns the updated Chat object.

Interrupt

POST /api/experimental/chats/{chat}/interrupt

Stops the agent's current processing loop and returns the chat to waiting status.

Manage queued messages

When a message is queued because the agent is busy, you can manage the queue:

DELETE /api/experimental/chats/{chat}/queue/{queuedMessage}

Removes a queued message before it is processed.

POST /api/experimental/chats/{chat}/queue/{queuedMessage}/promote

Promotes a queued message to be processed next.

Get diff contents

GET /api/experimental/chats/{chat}/diff

Returns the current diff/PR status for a chat, including additions, deletions, changed files, and pull request metadata when available.

File uploads

Attach images to a chat by uploading them first:

curl -X POST "https://coder.example.com/api/experimental/chats/files?organization=$ORG_ID" \
  -H "Coder-Session-Token: $CODER_SESSION_TOKEN" \
  -H "Content-Type: image/png" \
  --data-binary @screenshot.png

The response contains an id you can reference as file_id in a ChatInputPart with "type": "file". To retrieve a previously uploaded file, use GET /api/experimental/chats/files/{file}.

Supported formats: PNG, JPEG, GIF, WebP (up to 10 MB). The server validates actual file content regardless of the declared Content-Type.

Files referenced in messages are automatically linked to the chat and appear in the files field on subsequent GET /api/experimental/chats/{chat} responses.

Chat statuses

Status Meaning
waiting Idle. Newly created, finished successfully, or interrupted.
pending Queued for processing.
running Agent is actively working.
paused Agent is paused (for example, waiting for user input).
completed Agent finished and the task is complete.
error Agent encountered an error.
requires_action Agent invoked a client-provided tool and needs the result before continuing.

Configuration

Deployment-wide chat settings are read and written under /api/experimental/chats/config/*. Reading config requires authentication; writing requires deployment-admin privileges.

Auto-archive window

Chats whose newest non-deleted message is older than auto_archive_days are automatically archived by a background job. Pinned chats and chats belonging to a still-active thread are exempt. 0 disables the feature; the default is 90.

# Read
curl -H "Coder-Session-Token: $CODER_SESSION_TOKEN" \
  https://coder.example.com/api/experimental/chats/config/auto-archive-days
# { "auto_archive_days": 90 }

# Update
curl -X PUT -H "Coder-Session-Token: $CODER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"auto_archive_days": 60}' \
  https://coder.example.com/api/experimental/chats/config/auto-archive-days

Accepted range: 0 to 3650 (~10 years).