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

# Get All Assistant Session Messages

> Learn how to find session IDs for a project within a date range and export the full message timeline for each assistant chat session.

Every assistant conversation is a **chat session** identified by an ID. This recipe walks through a common export pattern:

1. Resolve a **project ID** (by name or from a known UUID).
2. **List session IDs** for that project within a **created\_at** date range.
3. **Fetch every message** in each session (user turns, assistant replies, and tool calls).

Each message expands into one or more typed **events** (`user_message`, `assistant_message`, `tool_call`, `tool_result`, `compaction`), so tool calls and results appear inline rather than on a separate endpoint.

<Note>
  All endpoints require **admin** access and operate at **tenant** scope: with an admin platform access token or dashboard session, you can list and read **any** session in your workspace, not only ones you started.
</Note>

## Endpoints used

| Method & path                                           | Purpose                                                                                 |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `GET /api/v1/projects`                                  | List projects in your tenant (to resolve a `project_id` by name).                       |
| `GET /api/v1/assistants/sessions`                       | List chat sessions; filter by `project_id`, date range, navigator, and playground flag. |
| `GET /api/v1/assistants/sessions/{session_id}/messages` | Get a page of a session's message timeline.                                             |

### Resolve the project ID

If you already have a project ID, skip this step.

`GET /api/v1/projects` returns paginated projects.

```json theme={null}
{
  "items": [
    {
      "id": "7e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
      "name": "Acme Corp",
      "description": "Production deployment for Acme",
      "created_at": "2024-01-01T00:00:00Z"
    }
  ],
  "meta": { "size": 20, "total": 1, "has_next": false, "has_prev": false }
}
```

### List sessions for a project and date range

`GET /api/v1/assistants/sessions` returns sessions for your workspace. Use **`project_id`** plus **`timestamp_start`** / **`timestamp_end`** to restrict to sessions **created** within a range (both bounds are inclusive, ISO 8601):

| Query parameter         | Notes                                                                |
| ----------------------- | -------------------------------------------------------------------- |
| `project_id`            | ID of the project.                                                   |
| `timestamp_start`       | Inclusive lower bound on `created_at` (e.g. `2026-06-01T00:00:00Z`). |
| `timestamp_end`         | Inclusive upper bound on `created_at` (e.g. `2026-06-30T23:59:59Z`). |
| `navigator_instance_id` | Further restrict to one navigator instance.                          |
| `is_playground`         | `true` / `false` to include only playground or production sessions.  |
| `limit`                 | Page size, default `50`, max `200`.                                  |
| `offset`                | Skip N results for pagination (use with `count` to walk all pages).  |

```json theme={null}
{
  "sessions": [
    {
      "session_id": "0b3c1d52-9f4a-4c7e-bb71-2f1e6a0d9c34",
      "session_name": "Help with Q4 planning",
      "navigator_name": "Support Agent",
      "navigator_instance_id": "9a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "project_id": "7e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
      "model_name": "claude-sonnet-4-6",
      "is_playground": false,
      "message_count": 12,
      "created_at": "2026-06-09T18:33:24Z",
      "last_message_at": "2026-06-09T18:41:02Z"
    }
  ],
  "count": 1
}
```

<Note>
  **`count`** is the total number of sessions matching your filters, not just the current page.
</Note>

<Note>
  If `timestamp_start` is after `timestamp_end`, the API returns **400**.
</Note>

<Tip>
  You can also grab a single session ID from the **Session Logs** page in the Caylex Platform UI — it's the ID in the URL of an open session.
</Tip>

### Reading the messages

`GET /api/v1/assistants/sessions/{session_id}/messages` returns the canonical paginated envelope plus a top-level `session` block identifying who the session belonged to:

| Query parameter | Default | Notes                                                                                                                                                 |
| --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `view`          | `raw`   | `raw` = every event, including Caylex meta-tools.<br />`resolved` = drop meta-tools and unwrap `invoke_tools` into the real tools it ran (see below). |
| `size`          | `20`    | Messages per page (1–100). Each message can expand into several events.                                                                               |
| `cursor`        | (none)  | Opaque cursor from a previous page's `meta.next_cursor`; omit for the first page.                                                                     |

```json theme={null}
{
  "session": {
    "session_id": "0b3c1d52-9f4a-4c7e-bb71-2f1e6a0d9c34",
    "session_name": "Help with Q4 planning",
    "user_email": "jane@acme.com",
    "tenant_user_id": null,
    "project_id": "7e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
    "navigator_instance_id": "9a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "model_name": "claude-sonnet-4-6",
    "is_playground": false,
    "created_at": "2026-06-09T18:33:24Z",
    "last_message_at": "2026-06-09T18:41:02Z"
  },
  "items": [
    {
      "message_id": "96aaba1d-c787-4e5f-afd0-1759b07569bc",
      "role": "user",
      "message_type": "text",
      "sequence_number": 1,
      "timestamp": "2026-06-09T18:33:24Z",
      "events": [
        {
          "type": "user_message",
          "event_id": "96aaba1d-…:0",
          "text": "what services do we have? can you check my email?"
        }
      ]
    }
  ],
  "meta": {
    "size": 20,
    "total": 12,
    "next_cursor": "eyJzb3J0Ijog…",
    "has_next": false,
    "has_prev": false
  }
}
```

<Note>
  `size` and `total` count **messages**, not events. One message can hold several events (e.g. an assistant turn with text plus parallel tool calls), so the `items` array's combined event count is usually larger than `size`.
</Note>

<Tip>
  The full request and response schema for these endpoints is browsable in the interactive API reference at [https://developers.caylex.ai](https://developers.caylex.ai).
</Tip>

## `raw` vs `resolved`

The agent talks to the Caylex Navigator through **meta-tools** (`suggest_tools`, `get_tool_schemas`, `invoke_tools`, `list_skills`, …). `invoke_tools` is the one that runs the real downstream tools.

* **`view=raw`** (default) shows exactly what happened, meta-tools included, useful for debugging the agent loop.
* **`view=resolved`** drops the pure meta-tools and **unwraps `invoke_tools`** into the actual server tool calls and results it performed, so you see business tools like `gmail_search_messages` directly.

In `resolved` mode, unwrapped `tool_call` events carry `server_name`, `intent`, and `parent_tool_use_id`; unwrapped `tool_result` events carry `tool_name` and `parent_tool_use_id`.

## The procedure

<Steps>
  <Step title="Resolve the project ID">
    `GET /api/v1/projects` and find the `id` for your project name — or use a `project_id` you already have from provisioning or the UI.
  </Step>

  <Step title="List sessions in the date range">
    `GET /api/v1/assistants/sessions?project_id={id}&timestamp_start=…&timestamp_end=…` and collect each `session_id`. Page with `limit` / `offset` while `offset + len(sessions) < count`.
  </Step>

  <Step title="Fetch messages for each session">
    For every `session_id`, call `GET /api/v1/assistants/sessions/{session_id}/messages?size=100` (add `view=resolved` to hide meta-tools).
  </Step>

  <Step title="Page each session until done">
    While `meta.has_next` is `true`, call again with `cursor=meta.next_cursor`, accumulating `items` until `has_next` is `false`.
  </Step>
</Steps>

## Full script

This resolves a project by name, lists **all** assistant sessions created in June 2026 for that project, then exports the complete message timeline for each session. Pass `view=resolved` to get business tools instead of Caylex meta-tools.

<Tabs>
  <Tab title="Python">
    ```python export_project_sessions.py theme={null}
    import os

    import requests

    BASE_URL = "https://api.caylex.ai/api/v1"
    HEADERS = {"Authorization": f"Bearer {os.environ['CAYLEX_PLATFORM_TOKEN']}"}

    PROJECT_NAME = "Acme Corp"
    TIMESTAMP_START = "2026-06-01T00:00:00Z"
    TIMESTAMP_END = "2026-06-30T23:59:59Z"


    def get_items(path: str, params: dict | None = None) -> list[dict]:
        """Fetch a cursor-paginated list endpoint (returns all items)."""
        params = dict(params or {})
        params.setdefault("size", 100)
        items: list[dict] = []
        cursor: str | None = None
        while True:
            page_params = {**params}
            if cursor:
                page_params["cursor"] = cursor
            resp = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=page_params)
            resp.raise_for_status()
            page = resp.json()
            items.extend(page["items"])
            if not page["meta"]["has_next"]:
                break
            cursor = page["meta"]["next_cursor"]
        return items


    def project_id_by_name(name: str) -> str | None:
        return next((p["id"] for p in get_items("/projects") if p["name"] == name), None)


    def list_sessions_for_project(
        project_id: str,
        *,
        timestamp_start: str,
        timestamp_end: str,
    ) -> list[dict]:
        """Page through GET /assistants/sessions with limit/offset."""
        sessions: list[dict] = []
        limit = 200
        offset = 0
        while True:
            resp = requests.get(
                f"{BASE_URL}/assistants/sessions",
                headers=HEADERS,
                params={
                    "project_id": project_id,
                    "timestamp_start": timestamp_start,
                    "timestamp_end": timestamp_end,
                    "limit": limit,
                    "offset": offset,
                },
            )
            resp.raise_for_status()
            page = resp.json()
            sessions.extend(page["sessions"])
            total = page["count"]
            offset += len(page["sessions"])
            if offset >= total or not page["sessions"]:
                break
        return sessions


    def get_all_messages(session_id: str, fmt: str = "raw") -> list[dict]:
        """Page through the whole timeline and return every message item."""
        items: list[dict] = []
        cursor: str | None = None
        while True:
            params = {"view": fmt, "size": 100}
            if cursor:
                params["cursor"] = cursor
            resp = requests.get(
                f"{BASE_URL}/assistants/sessions/{session_id}/messages",
                headers=HEADERS,
                params=params,
            )
            resp.raise_for_status()
            page = resp.json()
            items.extend(page["items"])
            if not page["meta"]["has_next"]:
                break
            cursor = page["meta"]["next_cursor"]
        return items


    if __name__ == "__main__":
        project_id = project_id_by_name(PROJECT_NAME)
        if not project_id:
            raise SystemExit(f"project not found: {PROJECT_NAME!r}")

        sessions = list_sessions_for_project(
            project_id,
            timestamp_start=TIMESTAMP_START,
            timestamp_end=TIMESTAMP_END,
        )
        print(f"{len(sessions)} sessions in range for {PROJECT_NAME}")

        for s in sessions:
            session_id = s["session_id"]
            messages = get_all_messages(session_id, fmt="resolved")
            print(f"\n{s['session_name']!r} ({session_id}): {len(messages)} messages")
            for m in messages:
                for e in m["events"]:
                    if e["type"] in ("user_message", "assistant_message"):
                        print(f"  [{m['role']}] {e['text'][:80]}")
                    elif e["type"] == "tool_call":
                        print(f"  [tool_call] {e['name']}")
                    elif e["type"] == "tool_result":
                        status = "error" if e.get("is_error") else "ok"
                        print(f"  [tool_result] {e.get('tool_name') or ''} ({status})")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript exportProjectSessions.ts theme={null}
    const BASE_URL = "https://api.caylex.ai/api/v1";
    const AUTH = { Authorization: `Bearer ${process.env.CAYLEX_PLATFORM_TOKEN}` };

    const PROJECT_NAME = "Acme Corp";
    const TIMESTAMP_START = "2026-06-01T00:00:00Z";
    const TIMESTAMP_END = "2026-06-30T23:59:59Z";

    async function getItems(path: string, params: Record<string, string> = {}) {
      const items: any[] = [];
      let cursor: string | null = null;
      do {
        const qs = new URLSearchParams({ ...params, size: "100" });
        if (cursor) qs.set("cursor", cursor);
        const res = await fetch(`${BASE_URL}${path}?${qs}`, { headers: AUTH });
        if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
        const page = await res.json();
        items.push(...page.items);
        cursor = page.meta.has_next ? page.meta.next_cursor : null;
      } while (cursor);
      return items;
    }

    async function projectIdByName(name: string) {
      return (await getItems("/projects")).find((p) => p.name === name)?.id as
        | string
        | undefined;
    }

    async function listSessionsForProject(
      projectId: string,
      timestampStart: string,
      timestampEnd: string,
    ) {
      const sessions: any[] = [];
      const limit = 200;
      let offset = 0;
      let total = 0;
      do {
        const qs = new URLSearchParams({
          project_id: projectId,
          timestamp_start: timestampStart,
          timestamp_end: timestampEnd,
          limit: String(limit),
          offset: String(offset),
        });
        const res = await fetch(`${BASE_URL}/assistants/sessions?${qs}`, { headers: AUTH });
        if (!res.ok) throw new Error(`list sessions failed: ${res.status}`);
        const page = await res.json();
        sessions.push(...page.sessions);
        total = page.count;
        offset += page.sessions.length;
      } while (offset < total && sessions.length > 0);
      return sessions;
    }

    async function getAllMessages(sessionId: string, fmt = "raw") {
      const items: any[] = [];
      let cursor: string | null = null;
      do {
        const params = new URLSearchParams({ view: fmt, size: "100" });
        if (cursor) params.set("cursor", cursor);
        const res = await fetch(
          `${BASE_URL}/assistants/sessions/${sessionId}/messages?${params}`,
          { headers: AUTH },
        );
        if (!res.ok) throw new Error(`get messages failed: ${res.status}`);
        const page = await res.json();
        items.push(...page.items);
        cursor = page.meta.has_next ? page.meta.next_cursor : null;
      } while (cursor);
      return items;
    }

    const projectId = await projectIdByName(PROJECT_NAME);
    if (!projectId) throw new Error(`project not found: ${PROJECT_NAME}`);

    const sessions = await listSessionsForProject(
      projectId,
      TIMESTAMP_START,
      TIMESTAMP_END,
    );
    console.log(`${sessions.length} sessions in range for ${PROJECT_NAME}`);

    for (const s of sessions) {
      const messages = await getAllMessages(s.session_id, "resolved");
      console.log(`\n${s.session_name} (${s.session_id}): ${messages.length} messages`);
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    BASE="https://api.caylex.ai/api/v1"
    AUTH="Authorization: Bearer $CAYLEX_PLATFORM_TOKEN"

    # 1. Resolve project ID by name (pick the matching id from items[])
    curl "$BASE/projects?size=100" -H "$AUTH"

    PROJECT_ID="7e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b"

    # 2. List sessions for the project within a created_at range
    curl -G "$BASE/assistants/sessions" -H "$AUTH" \
      --data-urlencode "project_id=$PROJECT_ID" \
      --data-urlencode "timestamp_start=2026-06-01T00:00:00Z" \
      --data-urlencode "timestamp_end=2026-06-30T23:59:59Z" \
      --data-urlencode "limit=200" \
      --data-urlencode "offset=0"

    # If count > limit, repeat with offset=200, offset=400, … until you have all session_ids.

    SESSION_ID="0b3c1d52-9f4a-4c7e-bb71-2f1e6a0d9c34"

    # 3. First page of messages for one session (add &view=resolved to hide meta-tools)
    curl "$BASE/assistants/sessions/$SESSION_ID/messages?size=100" -H "$AUTH"

    # 4. Next page: pass the previous response's meta.next_cursor
    curl "$BASE/assistants/sessions/$SESSION_ID/messages?size=100&cursor=$NEXT_CURSOR" \
      -H "$AUTH"
    ```
  </Tab>
</Tabs>

<Tip>
  To reconstruct the agent's reasoning, match each `tool_call` to its `tool_result` by `tool_use_id`. They share that ID even when they land in different messages (parallel tool calls return out of order). In `resolved` mode, the unwrapped events share the parent `invoke_tools` ID via `parent_tool_use_id`.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Provision a customer project" icon="diagram-project" href="/cookbooks/provision-customer-projects">
    Stand up a project per customer and capture the returned `id` for session exports.
  </Card>

  <Card title="Analytics overview" icon="chart-line" href="/analytics/overview">
    Aggregate metrics (queries, tool calls, error rates) across sessions.
  </Card>

  <Card title="Background Agent Tasks" icon="robot" href="/background-tasks/agent-tasks">
    Run the agent server-to-server; each task is backed by a chat session you can read here.
  </Card>

  <Card title="REST API Reference" icon="book" href="https://developers.caylex.ai/">
    Full request/response schemas for these endpoints.
  </Card>
</CardGroup>
