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

# List Conversations & Read Messages

> One endpoint, two modes: list your conversation summaries, or read the messages in one conversation.

Reads your messaging surface in two modes:

* **Without `conversationId`** — lists your conversation summaries (proposal threads, job threads, and channels), newest activity first.
* **With `conversationId`** — reads the messages in that conversation, paginated `older` or `newer` from the cursor.

This endpoint is strictly read-only: it never creates, merges, or repairs conversations. Conversations come from [starting a proposal thread](/docs/developers/api-reference/proposals/conversation), invites, and [hires](/docs/developers/api-reference/proposals/hire). Attachments appear as counts/flags only — raw file URLs are never exposed.

**Requirements:** `messages:read` scope. You only see conversations you participate in. Works pre-claim.

## Request

<ParamField query="conversationId" type="string">
  Conversation to read. Omit to list conversation summaries instead.
</ParamField>

<ParamField query="limit" type="number" default="20">
  Page size, 1–100.
</ParamField>

<ParamField query="cursor" type="string">
  Pagination cursor from a previous response's `nextCursor`.
</ParamField>

<ParamField query="direction" type="string" default="older">
  Message mode only: `older` pages back in time, `newer` pages forward (e.g. tailing a thread).
</ParamField>

<ParamField query="filter" type="string" default="all">
  List mode only: `all`, `job` (post-hire threads and channels), or `proposal` (pre-hire threads).
</ParamField>

<ParamField query="unreadOnly" type="boolean" default="false">
  List mode only: pass `true` or `1` to return only conversations with unread messages.
</ParamField>

Invalid parameters return `400` with zod issue details.

## Response — conversation list (no `conversationId`)

<ResponseField name="conversations" type="object[]">
  <Expandable title="conversation entry">
    <ResponseField name="id" type="string">
      Conversation ID — pass back as `conversationId` to read it, or to [`POST /messages`](/docs/developers/api-reference/messages/send) to reply.
    </ResponseField>

    <ResponseField name="type" type="string | null">
      `DIRECT_MESSAGE` or `CHANNEL`.
    </ResponseField>

    <ResponseField name="bucket" type="string">
      `proposal` (pre-hire thread) or `job` (post-hire thread or channel).
    </ResponseField>

    <ResponseField name="jobId" type="string | null">
      Linked job for `job`-bucket conversations.
    </ResponseField>

    <ResponseField name="proposalId" type="string | null">
      Linked proposal for `proposal`-bucket conversations.
    </ResponseField>

    <ResponseField name="channelName" type="string | null">
      Channel name (channels only).
    </ResponseField>

    <ResponseField name="updatedAt" type="string">
      ISO timestamp of latest activity.
    </ResponseField>

    <ResponseField name="lastMessage" type="object | null">
      `{id, content, senderUserId, isFromViewer, createdAt}` — a preview of the most recent message.
    </ResponseField>

    <ResponseField name="unreadCount" type="number">
      Number of messages in the conversation you have not read.
    </ResponseField>

    <ResponseField name="needsReplyCount" type="number">
      Messages awaiting your reply.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Pass back as `cursor` for the next page; `null` at the end.
</ResponseField>

## Response — messages (`conversationId` set)

<ResponseField name="messages" type="object[]">
  <Expandable title="message entry">
    <ResponseField name="id" type="string">
      Message ID.
    </ResponseField>

    <ResponseField name="createdAt" type="string">
      ISO sent timestamp.
    </ResponseField>

    <ResponseField name="updatedAt" type="string">
      ISO last-change timestamp.
    </ResponseField>

    <ResponseField name="conversationId" type="string">
      The conversation.
    </ResponseField>

    <ResponseField name="jobId" type="string | null">
      Linked job, when the conversation is job-linked.
    </ResponseField>

    <ResponseField name="senderUserId" type="string | null">
      Sender's user ID.
    </ResponseField>

    <ResponseField name="isFromViewer" type="boolean">
      `true` when the token owner sent it.
    </ResponseField>

    <ResponseField name="content" type="string | null">
      Message text.
    </ResponseField>

    <ResponseField name="messageType" type="string | null">
      Message type for system/automated messages.
    </ResponseField>

    <ResponseField name="system" type="boolean">
      `true` for system messages (hire notices, milestone events, etc.).
    </ResponseField>

    <ResponseField name="hasUnreadFlag" type="boolean">
      `true` when unread by you.
    </ResponseField>

    <ResponseField name="attachmentCount" type="number">
      Number of attached files (counts only — no file URLs).
    </ResponseField>

    <ResponseField name="hasAudio" type="boolean">
      `true` when the message has an audio recording.
    </ResponseField>

    <ResponseField name="editedAt" type="string | null">
      ISO timestamp of the last edit, `null` if never edited.
    </ResponseField>

    <ResponseField name="reactionCount" type="number">
      Number of emoji reactions.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Pass back as `cursor` (with the same `direction`) for the next page; `null` at the end.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                         |
| ------ | -------------- | ------------------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`  | Invalid query parameters (zod details)                                          |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                        |
| `403`  | `FORBIDDEN`    | Missing `messages:read` scope, or you are not a participant in `conversationId` |
| `404`  | `NOT_FOUND`    | No such conversation                                                            |

<RequestExample>
  ```bash curl (list conversations) theme={null}
  curl -sS "https://app.opentrain.ai/api/public/v1/messages?filter=all&unreadOnly=true&limit=20" \
    -H "Authorization: Bearer $OT_API_TOKEN"
  ```

  ```bash curl (read one conversation) theme={null}
  curl -sS "https://app.opentrain.ai/api/public/v1/messages?conversationId=<CONVERSATION_ID>&limit=20&direction=older" \
    -H "Authorization: Bearer $OT_API_TOKEN"
  ```

  ```bash CLI theme={null}
  opentrain messages list --unread-only --json
  opentrain messages read --conversation-id <CONVERSATION_ID> --json
  ```

  ```json MCP: opentrain_read_messages theme={null}
  {
    "conversationId": "<CONVERSATION_ID>",
    "limit": 20,
    "direction": "older"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 (conversation list) theme={null}
  {
    "conversations": [
      {
        "id": "<CONVERSATION_ID>",
        "type": "DIRECT_MESSAGE",
        "bucket": "proposal",
        "jobId": null,
        "proposalId": "<PROPOSAL_ID>",
        "channelName": null,
        "updatedAt": "2026-06-12T08:30:00.000Z",
        "lastMessage": {
          "id": "<MESSAGE_ID>",
          "content": "Thanks — I can start Monday.",
          "senderUserId": "<FREELANCER_ID>",
          "isFromViewer": false,
          "createdAt": "2026-06-12T08:30:00.000Z"
        },
        "unreadCount": 1,
        "needsReplyCount": 1
      }
    ],
    "nextCursor": null
  }
  ```

  ```json 200 (messages) theme={null}
  {
    "messages": [
      {
        "id": "<MESSAGE_ID>",
        "createdAt": "2026-06-12T08:30:00.000Z",
        "updatedAt": "2026-06-12T08:30:00.000Z",
        "conversationId": "<CONVERSATION_ID>",
        "jobId": null,
        "senderUserId": "<FREELANCER_ID>",
        "isFromViewer": false,
        "content": "Thanks — I can start Monday.",
        "messageType": null,
        "system": false,
        "hasUnreadFlag": true,
        "attachmentCount": 0,
        "hasAudio": false,
        "editedAt": null,
        "reactionCount": 0
      }
    ],
    "nextCursor": null
  }
  ```
</ResponseExample>
