> ## Documentation Index
> Fetch the complete documentation index at: https://docs.guild.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Conversations

> Start a chat with an agent using an account API key, poll for its replies, and send follow-ups.

An account API key can hold a full conversation with an agent: start a chat, read the agent's replies, and send follow-ups. This is the surface a partner backend integrates against most often, so the whole flow is spelled out here — the underlying endpoints are also documented individually under **Sessions** in the sidebar.

<Note>
  This is a different flow from [API triggers](/platform/api-triggers). A trigger API key runs one agent on demand and is scoped to a single trigger. An account key holds an open-ended conversation with any agent installed in the account's workspaces, scoped by `sessions:write`.
</Note>

## Start a chat

```http theme={null}
POST /workspaces/{workspace_id_or_name}/sessions
```

Requires `sessions:write` on a workspace the key's account owns. The scope is deliberately `sessions`, not `workspaces`, so workspace scope alone can't quietly grant conversations.

#### Request body

| Field            | Type          | Required | Description                                                                                                                        |
| ---------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `session_type`   | string        | Yes      | Must be `"chat"` — the only value a key may pass. `time`, `webhook`, `api_trigger`, and `agent_test` sessions are `403` for a key. |
| `agent_id`       | string (uuid) | Yes      | The agent to converse with.                                                                                                        |
| `initial_prompt` | string        | Yes      | The first message. The agent begins executing it immediately.                                                                      |

#### Example

```bash theme={null}
curl -X POST https://api.guild.ai/v1/workspaces/acme~support/sessions \
  -u "<api_key_id>:<api_key_secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "session_type": "chat",
    "agent_id": "<agent_id>",
    "initial_prompt": "Summarize open tickets tagged urgent."
  }'
```

#### Response

Returns `201` with the session. The key is recorded as the session's `initiator`, serialized as `type: "api_key"`:

```json theme={null}
{
  "id": "3fa2c1e0-9b4e-4b3a-8f1a-2b6b8b6a2b31",
  "workspace_id": "8c1e2f3a-4b5c-4d6e-9f0a-1b2c3d4e5f6a",
  "initiator_id": "<api_key_id>",
  "name": null,
  "created_at": "2026-08-27T14:02:11.483Z",
  "updated_at": "2026-08-27T14:02:11.483Z"
}
```

## Read the conversation

```http theme={null}
GET /sessions/{session_id}/events?from_id=<last_seen_event_id>
```

Requires `workspaces:read`, plus `agents:read` when the session ran an agent — see [Fetch session events](/api-reference/sessions/fetch-session-events).

Events default to newest-first (`sort_by=-id`) with a limit of 20, so a naive read returns the tail of the conversation in reverse. For polling, pass `from_id`: it's an exclusive cursor (`id > from_id`), so each poll returns only what happened since the last one. Event ids are UUIDv7 and therefore time-ordered.

The agent's reply arrives as a `runtime_done` event whose `content.text` carries the message. One turn can emit **several** `runtime_done` events, because subtasks and tool runs complete with empty content (`{}`) before the top-level turn does. Read the reply from the `runtime_done` that carries `content.text`, not simply the first one to appear. Events persist when a turn completes, not while the model is streaming — a poll during generation returns nothing new until the turn finishes.

If you would rather not poll, the same events are available over a WebSocket — see [Stream the conversation](#stream-the-conversation).

#### Example

```bash theme={null}
curl -u "<api_key_id>:<api_key_secret>" \
  "https://api.guild.ai/v1/sessions/<session_id>/events?from_id=<last_seen_event_id>"
```

#### Response

```json theme={null}
{
  "items": [
    {
      "id": "9d2f4a1b-6e3c-4a2f-8b1d-5c6e7f8a9b0c",
      "type": "runtime_done",
      "content": { "text": "Three tickets are tagged urgent: #482, #491, #503." },
      "task_id": "f1a04e6c-7b2a-4e1d-9c3a-1a2b3c4d5e6f",
      "created_at": "2026-08-27T14:02:39.221Z",
      "updated_at": "2026-08-27T14:02:39.221Z"
    }
  ],
  "pagination": { "total_count": 1, "limit": 20, "offset": 0, "has_more": false }
}
```

Poll again with `from_id=9d2f4a1b-...` to only see events after this one.

## Stream the conversation

Instead of polling, connect to the session events WebSocket and receive each event as it is written.

```text theme={null}
wss://api.guild.ai/v1/sessions/{session_id}/events/ws
```

Requires the same scopes as the polling read: `workspaces:read`, plus `agents:read` when the session ran an agent. Privacy is evaluated identically too — a session the key cannot see closes the connection with `404` rather than reporting an authorization error.

Authenticate with the same API key, sent as an `Authorization: Basic` header on the connection request.

<Warning>
  **A socket opened without `from_id` carries only events created after the connection opens.** It does not replay anything that already happened, so a client that connects after starting a session can silently miss the agent's reply. Pass `from_id` with the last event id you processed — the socket then replays the backlog after that cursor before switching to live delivery. That is also how you resume after a reconnect.
</Warning>

<Note>
  The `Authorization` header is the only way to authenticate this connection. A browser's built-in `WebSocket` cannot set request headers, so connect from a server-side or native client instead.
</Note>

#### Query parameters

| Parameter | Description                                                                                                                                                    |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from_id` | Replay events after this event id, then stream live. Omit it and you get live events only. Ephemeral draft ids are rejected — use the id of a persisted event. |
| `types`   | Comma-separated event types to receive. Omit it for every type. An unrecognized type is rejected with a message listing the valid ones.                        |

#### Example

```bash theme={null}
websocat -H "Authorization: Basic $(printf '%s' '<api_key_id>:<api_key_secret>' | base64)" \
  "wss://api.guild.ai/v1/sessions/<session_id>/events/ws?from_id=<last_seen_event_id>"
```

Each message is a single event, in the same shape as one entry of the `items` array returned by `GET /sessions/{session_id}/events`.

<Note>
  This endpoint is not listed in the sidebar or in `openapi.yaml`: OpenAPI cannot describe WebSockets. It is part of the public API all the same.
</Note>

## Send a follow-up

```http theme={null}
POST /sessions/{session_id}/events
```

Requires `sessions:write` — see [Post a follow-up event to a session](/api-reference/sessions/post-a-follow-up-event-to-a-session).

The message is authored by the key itself (`author.type: "api_key"`); a key may never author as anyone else. The `agent_id` field is accepted but ignored for a key — it only retargets for user viewers — so a key cannot switch which agent answers mid-conversation. Start a new session instead.

#### Example

```bash theme={null}
curl -X POST https://api.guild.ai/v1/sessions/<session_id>/events \
  -u "<api_key_id>:<api_key_secret>" \
  -H "Content-Type: application/json" \
  -d '{"content": "What about tickets tagged high, not just urgent?", "mode": "text"}'
```

Read the agent's reply the same way as above — poll `GET /sessions/{session_id}/events?from_id=...` until a new `runtime_done` event appears.

## Containment

A key converses only in the chat sessions it initiated. Posting into any other session — another key's chat, a person's chat, a trigger session — is `404`, not `403`: a key cannot inject messages into a human's conversation, and cannot even learn that the other session exists.

Reading is broader than writing: with `workspaces:read`, a key can read every session in its account's unrestricted workspaces, it just cannot speak in them.
