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

# Get Session Messages

> List, filter, and poll messages on a session or a single thread.

Two endpoints share the same paginated response shape and query parameters. Use the first to list every message in a session; use the second to scope to a single thread (for example, to poll the latest follow-up's reply).

```http theme={null}
GET /rest/v1/sessions/{session_id}/messages
GET /rest/v1/sessions/{session_id}/threads/{thread_id}/messages
```

Results are sorted by `ts` (oldest first by default; pass `direction=desc` for newest first). Soft-deleted messages are excluded.

## Request

<CodeGroup>
  ```javascript JavaScript theme={null}
  // All user and assistant messages on the session.
  const url = `https://api.blocks.team/rest/v1/sessions/${sessionId}/messages?type=message&type=final_message&role=user&role=assistant`;

  // Or scope to a specific thread:
  // const url = `https://api.blocks.team/rest/v1/sessions/${sessionId}/threads/${threadId}/messages?type=final_message&role=assistant`;

  const page = await fetch(url, {
    headers: { Authorization: `ApiKey ${process.env.BLOCKS_API_KEY}` },
  }).then((r) => r.json());
  ```

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

  # All user and assistant messages on the session.
  url = f"https://api.blocks.team/rest/v1/sessions/{session_id}/messages"

  # Or scope to a specific thread:
  # url = f"https://api.blocks.team/rest/v1/sessions/{session_id}/threads/{thread_id}/messages"

  page = requests.get(
      url,
      headers={"Authorization": f"ApiKey {os.environ['BLOCKS_API_KEY']}"},
      params={"type": ["message", "final_message"], "role": ["user", "assistant"]},
  ).json()
  # For a single thread's final reply use: params={"type": "final_message", "role": "assistant"}
  ```

  ```bash cURL theme={null}
  # All user and assistant messages on the session.
  curl "https://api.blocks.team/rest/v1/sessions/$SESSION_ID/messages?type=message&type=final_message&role=user&role=assistant" \
    -H "Authorization: ApiKey $BLOCKS_API_KEY"

  # Or scope to a specific thread:
  curl "https://api.blocks.team/rest/v1/sessions/$SESSION_ID/threads/$THREAD_ID/messages?type=final_message&role=assistant" \
    -H "Authorization: ApiKey $BLOCKS_API_KEY"
  ```

  ```java Java theme={null}
  import java.net.URI;
  import java.net.http.*;

  // All user and assistant messages on the session.
  String url = "https://api.blocks.team/rest/v1/sessions/" + sessionId
      + "/messages?type=message&type=final_message&role=user&role=assistant";

  // Or scope to a specific thread:
  // String url = "https://api.blocks.team/rest/v1/sessions/" + sessionId
  //     + "/threads/" + threadId + "/messages?type=final_message&role=assistant";

  HttpResponse<String> res = HttpClient.newHttpClient().send(
      HttpRequest.newBuilder(URI.create(url))
          .header("Authorization", "ApiKey " + System.getenv("BLOCKS_API_KEY"))
          .build(),
      HttpResponse.BodyHandlers.ofString());
  ```

  ```ruby Ruby theme={null}
  require "net/http"
  require "json"

  # All user and assistant messages on the session.
  url = "https://api.blocks.team/rest/v1/sessions/#{session_id}/messages?type=message&type=final_message&role=user&role=assistant"

  # Or scope to a specific thread:
  # url = "https://api.blocks.team/rest/v1/sessions/#{session_id}/threads/#{thread_id}/messages?type=final_message&role=assistant"

  page = JSON.parse(Net::HTTP.get(URI(url), {
    "Authorization" => "ApiKey #{ENV['BLOCKS_API_KEY']}",
  }))
  ```

  ```go Go theme={null}
  // All user and assistant messages on the session.
  url := "https://api.blocks.team/rest/v1/sessions/" + sessionID +
      "/messages?type=message&type=final_message&role=user&role=assistant"

  // Or scope to a specific thread:
  // url := "https://api.blocks.team/rest/v1/sessions/" + sessionID +
  //     "/threads/" + threadID + "/messages?type=final_message&role=assistant"

  req, _ := http.NewRequest("GET", url, nil)
  req.Header.Set("Authorization", "ApiKey "+os.Getenv("BLOCKS_API_KEY"))
  res, _ := http.DefaultClient.Do(req)
  ```
</CodeGroup>

### Path parameters

<ParamField path="session_id" type="string (uuid)" required>
  The session ID. Required for both URL forms.
</ParamField>

<ParamField path="thread_id" type="string (uuid)" required>
  The thread ID. Required only on the threaded URL form. The thread must belong to `session_id`.
</ParamField>

### Query parameters

<ParamField query="type" type="string[]" default="message, final_message">
  Repeat to combine. Allowed values: `message`, `final_message`, `tool_call`. Pass `type=final_message` to fetch only completed assistant replies.
  Example: `?type=message&type=final_message&type=tool_call`. Comma-separated lists are rejected with `422`.
</ParamField>

<ParamField query="role" type="string[]" default="assistant">
  Filter by sender. Repeat to combine. Allowed values: `user`, `assistant`. Example: `?role=user&role=assistant` returns both sides of the conversation.
</ParamField>

<ParamField query="thread_id" type="string (uuid)">
  Filter to a single thread without using the threaded URL form. Ignored on the threaded URL.
</ParamField>

<ParamField query="gts" type="number">
  Cursor for incremental polling, in epoch seconds. Returns only messages whose `ts` is strictly greater than this value. Use `_links.new_messages.href` from the previous page to get a pre-built URL with this cursor already set.
</ParamField>

<ParamField query="page" type="number" default="1">
  1-indexed page number. Minimum `1`.
</ParamField>

<ParamField query="limit" type="number" default="50">
  Page size. Minimum `1`, maximum `100`.
</ParamField>

<ParamField query="direction" type="string" default="asc">
  Sort direction by `ts`. One of `asc` (oldest first), `desc` (newest first).
</ParamField>

## Response

```json theme={null}
{
  "items": [
    {
      "id": "5e1b…",
      "chat_id": "a196cec6-49cb-4c48-8e4c-2707fb5d6709",
      "chat_thread_id": "71562cd0-1f63-41b2-8382-10f2fdb1ac8b",
      "task_id": "0e7d…",
      "role": "assistant",
      "type": "final_message",
      "message": "Hi! Octopuses have three hearts — two pump blood to the gills and one to the rest of the body.",
      "ts": 1746038518,
      "created_at": "2026-04-30T18:21:42.000Z",
      "updated_at": "2026-04-30T18:21:42.000Z"
    }
  ],
  "meta": { "total": 1, "page": 1, "limit": 50, "total_pages": 1 },
  "_links": {
    "self": { "href": "https://api.blocks.team/rest/v1/sessions/a196cec6-…/messages" },
    "new_messages": { "href": "https://api.blocks.team/rest/v1/sessions/a196cec6-…/messages?gts=1746038518" }
  }
}
```

<ResponseField name="items" type="object[]">
  Page of messages.

  <Expandable title="message properties">
    <ResponseField name="id" type="string (uuid)">
      Message ID.
    </ResponseField>

    <ResponseField name="chat_id" type="string (uuid)">
      The session this message belongs to.
    </ResponseField>

    <ResponseField name="chat_thread_id" type="string (uuid) | null">
      The thread the message was sent on, if any. `null` for system notices that are not part of a thread.
    </ResponseField>

    <ResponseField name="task_id" type="string (uuid)">
      The task ID for the agent invocation that produced this message.
    </ResponseField>

    <ResponseField name="role" type="string">
      Sender role: `user` or `assistant`.
    </ResponseField>

    <ResponseField name="type" type="string">
      Message type: `message`, `final_message`, or `tool_call`.
    </ResponseField>

    <ResponseField name="message" type="string">
      The message body.
    </ResponseField>

    <ResponseField name="ts" type="number | null">
      Provider-supplied timestamp in epoch seconds. Use as the `gts` cursor for incremental polling.
    </ResponseField>

    <ResponseField name="created_at" type="string (ISO 8601)">
      When the message was created.
    </ResponseField>

    <ResponseField name="updated_at" type="string (ISO 8601)">
      When the message was last updated.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="meta" type="object">
  Pagination metadata.

  <Expandable title="properties">
    <ResponseField name="total" type="number">
      Total messages matching the filter.
    </ResponseField>

    <ResponseField name="page" type="number">
      The current 1-indexed page.
    </ResponseField>

    <ResponseField name="limit" type="number">
      The page size used for this response.
    </ResponseField>

    <ResponseField name="total_pages" type="number">
      Total page count for the current filter.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="_links" type="object">
  HATEOAS links for the page.

  <Expandable title="properties">
    <ResponseField name="self" type="object">
      Link to the endpoint **without** query parameters. Do not use it for pagination — rebuild the URL with your own `page` and filters.
    </ResponseField>

    <ResponseField name="new_messages" type="object">
      Pre-built URL with `?gts=<cursor>` appended. Poll this URL to receive only messages newer than the latest one in this page. Always present — when `items` is empty, the cursor is the server's current epoch second, so the next poll returns anything created after now.
    </ResponseField>
  </Expandable>
</ResponseField>

## Polling tips

* For the assistant's first reply, use the `_links.final_message.href` returned from [`Create Session`](/rest-api/sessions/create) — it already has the right filters applied.
* For incremental polling, use `_links.new_messages.href` from the previous response. It encodes the `gts` cursor so each poll only returns new messages.
* A finished thread's last `message` repeats its `final_message` text. When summarising a transcript, dedupe on `final_message`.

## Errors

| Status | Code | Reason |
| - | - | - |
| `404` | `NOT_FOUND` | Session does not exist or belongs to a different workspace. An unknown `thread_id` is **not** an error — it returns `200` with empty `items`. |
| `422` | `VALIDATION` | `session_id` or `thread_id` is not a UUID, or an invalid `type`, `role`, `page`, `limit`, or `direction` value was passed (including comma-separated lists). |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.