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

# Chat Messages

> List the visible transcript of a chat — text, thinking, tool calls, and tool results, in chronological order.

Messages belong to a [chat](/api/chats). This endpoint returns the customer-visible transcript: user messages, assistant replies, tool calls, and tool results, in the order they occurred.

Internal runtime parts (compaction, container uploads, hidden branches) and internal metadata (session IDs, model names, branching info) are never returned. Tool calls and results are normalized across transports — you'll never need to branch on where a tool ran.

***

## `GET /v1/chats/:id/messages`

List visible messages for a chat in chronological order (oldest first).

Unlike `/v1/chats` and `/v1/releases` — which return newest-first — messages are ordered ascending so you can read a transcript top-to-bottom without reversing the list.

### Request

<ParamField header="Authorization" type="string" required>
  Bearer token. See [Authentication](/api/authentication).
</ParamField>

<ParamField path="id" type="string" required>
  Chat ID (UUID).
</ParamField>

<ParamField query="limit" type="integer">
  Number of messages to return. Default `20`, max `100`.
</ParamField>

<ParamField query="starting_after" type="string">
  Cursor for pagination. Pass the `id` of the last message from the previous page.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl https://app.userjourneys.ai/api/v1/chats/7af6cf75-20ec-49f4-b113-929c01dfbe45/messages \
    -H "Authorization: Bearer uj_live_your_key_here"
  ```

  ```javascript Node.js theme={null}
  const chatId = "7af6cf75-20ec-49f4-b113-929c01dfbe45";
  const response = await fetch(
    `https://app.userjourneys.ai/api/v1/chats/${chatId}/messages`,
    {
      headers: {
        Authorization: "Bearer uj_live_your_key_here",
      },
    }
  );
  const { data, has_more } = await response.json();
  ```

  ```python Python theme={null}
  import requests

  chat_id = "7af6cf75-20ec-49f4-b113-929c01dfbe45"
  response = requests.get(
      f"https://app.userjourneys.ai/api/v1/chats/{chat_id}/messages",
      headers={"Authorization": "Bearer uj_live_your_key_here"},
  )
  data = response.json()
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "object": "list",
    "data": [
      {
        "id": "a1b2c3d4-5678-90ab-cdef-111111111111",
        "object": "message",
        "chat_id": "7af6cf75-20ec-49f4-b113-929c01dfbe45",
        "role": "user",
        "parts": [
          { "type": "text", "text": "Why did onboarding drop this week?" }
        ],
        "sources": null,
        "feedback": null,
        "usage": null,
        "created_at": "2026-04-10T15:18:03.000Z",
        "completed_at": "2026-04-10T15:18:03.000Z"
      },
      {
        "id": "b2c3d4e5-6789-01ab-cdef-222222222222",
        "object": "message",
        "chat_id": "7af6cf75-20ec-49f4-b113-929c01dfbe45",
        "role": "assistant",
        "parts": [
          { "type": "thinking", "thinking": "Pulling the onboarding funnel..." },
          {
            "type": "tool_use",
            "id": "tool_q1",
            "name": "query_bigquery",
            "summary": "Weekly onboarding funnel",
            "input": { "sql": "SELECT step, count(*) FROM onboarding GROUP BY 1" },
            "state": "output_available"
          },
          {
            "type": "tool_result",
            "tool_use_id": "tool_q1",
            "content": { "rows": [{ "step": "signup", "count": 312 }] },
            "is_error": false
          },
          {
            "type": "text",
            "text": "The signup → verify step lost 18% compared to last week."
          }
        ],
        "sources": [
          {
            "id": "src_1",
            "description": "Weekly onboarding funnel",
            "rows": 312,
            "query": "SELECT step, count(*) FROM onboarding GROUP BY 1"
          }
        ],
        "feedback": "positive",
        "usage": {
          "input_tokens": 1420,
          "output_tokens": 362,
          "cache_read_input_tokens": 800,
          "cache_creation_input_tokens": 0,
          "cost_usd": 0.0087
        },
        "created_at": "2026-04-10T15:18:09.000Z",
        "completed_at": "2026-04-10T15:18:18.212Z"
      }
    ],
    "has_more": false
  }
  ```
</ResponseExample>

Returns `404` if the chat doesn't exist, doesn't belong to your project, or is excluded from the public chats API (internal, eval, or release-generated).

Returns `400` with `Invalid parameters` when pagination inputs are malformed — for example, `limit` outside `1..100` or a non-UUID `starting_after`. The response body includes a `details` array with the Zod issue for each offending field.

Returns `400` with `No such message: <id>` if `starting_after` parses as a UUID but is not a visible message in this chat.

***

## Message object

<ResponseField name="id" type="string">
  Message ID (UUID).
</ResponseField>

<ResponseField name="object" type="string">
  `"message"`
</ResponseField>

<ResponseField name="chat_id" type="string">
  ID of the parent [chat](/api/chats).
</ResponseField>

<ResponseField name="role" type="string">
  `"user"`, `"assistant"`, or `"system"`. `system` is rare in public output.
</ResponseField>

<ResponseField name="parts" type="object[]">
  Ordered content blocks. See [Part types](#part-types).
</ResponseField>

<ResponseField name="sources" type="object[] | null">
  Citations referenced by an assistant message. `null` when there are none.

  <Expandable title="properties">
    <ResponseField name="id" type="string">Source identifier.</ResponseField>
    <ResponseField name="description" type="string">Human-readable description.</ResponseField>
    <ResponseField name="rows" type="integer | null">Row count when available.</ResponseField>
    <ResponseField name="query" type="string | null">The query body. `null` when absent, or when truncated (see `truncated`).</ResponseField>
    <ResponseField name="truncated" type="boolean">Present and `true` only when `query` was nulled because it exceeded the size cap. Absent on non-truncated sources. Check this to disambiguate "no query stored" from "query too large".</ResponseField>
    <ResponseField name="truncation_reason" type="string">Present alongside `truncated`. Currently always `"size_limit_32kb"`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="feedback" type="string | null">
  `"positive"`, `"negative"`, or `null`. User thumbs-up/down from the UI.
</ResponseField>

<ResponseField name="usage" type="object | null">
  Token and cost metrics for assistant messages. `null` on user/system messages or when metrics were not recorded.

  <Expandable title="properties">
    <ResponseField name="input_tokens" type="integer | null" />

    <ResponseField name="output_tokens" type="integer | null" />

    <ResponseField name="cache_read_input_tokens" type="integer | null" />

    <ResponseField name="cache_creation_input_tokens" type="integer | null" />

    <ResponseField name="cost_usd" type="number | null" />
  </Expandable>
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp — when the message was recorded.
</ResponseField>

<ResponseField name="completed_at" type="string | null">
  ISO 8601 timestamp — when streaming finished. `null` while still streaming.
</ResponseField>

***

## Part types

### `text`

```json theme={null}
{ "type": "text", "text": "..." }
```

### `thinking`

Internal reasoning shown to the user in a collapsible section.

```json theme={null}
{ "type": "thinking", "thinking": "..." }
```

### `redacted_thinking`

Marker for reasoning that was redacted. No content.

```json theme={null}
{ "type": "redacted_thinking" }
```

### `file`

User-uploaded file attached to a message.

```json theme={null}
{
  "type": "file",
  "url": "https://...",
  "media_type": "application/pdf",
  "filename": "brief.pdf"
}
```

`filename` is `null` if none was provided.

### `tool_use`

A tool call made by the assistant. All tool transports (server tools, MCP tools) normalize to this single shape.

```json theme={null}
{
  "type": "tool_use",
  "id": "tool_abc",
  "name": "query_bigquery",
  "summary": "Weekly retention cohort",
  "input": { "sql": "SELECT ..." },
  "state": "output_available"
}
```

<ResponseField name="state" type="string">
  One of `input_available`, `output_available`, `output_error`, `output_denied`. Transient internal lifecycle states (e.g. streaming input, approval requests) are mapped to `input_available`.
</ResponseField>

### `tool_result`

A tool result for a previous `tool_use`. All result transports (web search, code execution, MCP) normalize to this shape.

```json theme={null}
{
  "type": "tool_result",
  "tool_use_id": "tool_abc",
  "content": { "rows": [...] },
  "is_error": false
}
```

***

## Tool payload stability

The **shapes inside `tool_use.input` and `tool_result.content` are tool-specific** and may evolve as tools change. Don't hard-code field paths — read them defensively. Everything else in the response is stable and versioned.

***

## Truncation

Large payloads are capped to keep response sizes predictable. When a `tool_use.input`, `tool_result.content`, or `sources[].query` exceeds 32 KB of JSON-encoded UTF-8:

```json theme={null}
{
  "type": "tool_result",
  "tool_use_id": "tool_abc",
  "content": null,
  "is_error": false,
  "truncated": true,
  "truncation_reason": "size_limit_32kb"
}
```

Check `truncated === true` rather than hardcoding the size — the threshold may change.

Truncated sources surface the same way — `query` becomes `null` with matching `truncated`/`truncation_reason` fields:

```json theme={null}
{
  "id": "src_1",
  "description": "Weekly retention cohort",
  "rows": null,
  "query": null,
  "truncated": true,
  "truncation_reason": "size_limit_32kb"
}
```

The full content for truncated parts is not retrievable in this version.

***

## Pagination

```json theme={null}
{ "object": "list", "data": [...], "has_more": true }
```

When `has_more` is `true`, pass `starting_after` with the last message's `id` to get the next page. Results are ordered **oldest first**, so walking pages with `starting_after` traverses the transcript forward in time.
