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

# Read message history

> List stored conversations and page through their messages for Numbers with hosted message storage.

<Note>
  The message history API is a beta. A team owner or admin must enroll the team on
  the **Numbers** page. Until the team is enrolled and the beta is offered to it,
  every history request returns `403 permission_denied`.
</Note>

The message history API returns conversations and messages that Polymorfa stored
for a Number with hosted message storage enabled. Use it to backfill an inbox,
render a conversation after a page reload, or reconcile your own store with what
Polymorfa retained.

## Before you start

* **Hosted message storage is on for the Number.** History contains only messages
  received or sent after storage was enabled. For any other Number, history
  requests return `404 hms_not_enabled`. Test numbers never have stored history.
* **Your team is enrolled in the beta.** Enrollment is on the **Numbers** page of
  the Console. Withdrawing blocks new history reads and does not delete stored
  messages.
* **You call from your server with an organization key or project token.**
  Client tokens are rejected with `403 permission_denied`.

| Method                                                                                                                         | Scope           |
| ------------------------------------------------------------------------------------------------------------------------------ | --------------- |
| [List conversations](/guides/communicate/api/list-chats) `GET /messaging/{session}/chats`                                      | `chats:read`    |
| [Get a conversation](/guides/communicate/api/get-chat) `GET /messaging/{session}/chats/{conversation}`                         | `chats:read`    |
| [List messages](/guides/communicate/api/list-chat-messages) `GET /messaging/{session}/chats/{conversation}/messages`           | `messages:read` |
| [Get a message](/guides/communicate/api/get-chat-message) `GET /messaging/{session}/chats/{conversation}/messages/{messageId}` | `messages:read` |

`messages:read` is a new scope. Credentials created before it existed, including
full-access credentials, do not include it. Create a credential that lists
`messages:read` explicitly.

## List conversations

Conversations are ordered by most recent activity, newest first. Each entry
carries the conversation identity, its kind, and a summary of the latest stored
message. The summary has no message body; read messages for content.

```bash theme={null}
curl "https://api.polymorfa.com/messaging/support-line/chats?limit=20&kind=direct" \
  -H "Authorization: Bearer $POLYMORFA_PROJECT_TOKEN"
```

```json theme={null}
{
  "success": true,
  "data": [
    {
      "conversation": { "id": "739182640518203", "phoneNumber": "+15550001111" },
      "kind": "direct",
      "lastActivityAt": "2026-09-18T10:05:00.000Z",
      "lastMessage": {
        "id": "739182640518977",
        "whatsapp_ids": { "linked_devices": "3EB0C767D8A1A2B5" },
        "whatsapp_id": "3EB0C767D8A1A2B5",
        "direction": "inbound",
        "type": "text",
        "timestamp": "2026-09-18T10:05:00.000Z"
      }
    }
  ],
  "hasMore": true,
  "nextCursor": "eyJ2IjoxLCJz...",
  "previousCursor": null
}
```

| Query parameter | Meaning                                                           |
| --------------- | ----------------------------------------------------------------- |
| `limit`         | Page size, 1 to 100. Default 50.                                  |
| `cursor`        | `nextCursor` from the previous page.                              |
| `kind`          | `direct`, `group`, `channel`, or `broadcast`.                     |
| `activeSince`   | Conversations with activity at or after this ISO 8601 time.       |
| `activeBefore`  | Conversations whose latest activity is before this ISO 8601 time. |

A conversation appears only while at least one of its messages is retained. A
conversation that receives a new message while you page moves to the top of the
list; it is not repeated on later pages.

## List messages in a conversation

Select the conversation by its `id` or by an E.164 phone number (URL-encode the
`+` as `%2B`). Messages are newest first by default.

```bash theme={null}
curl "https://api.polymorfa.com/messaging/support-line/chats/739182640518203/messages?limit=50&direction=inbound&types=text,image" \
  -H "Authorization: Bearer $POLYMORFA_PROJECT_TOKEN"
```

```json theme={null}
{
  "success": true,
  "data": [
    {
      "id": "739182640518977",
      "whatsapp_ids": { "linked_devices": "3EB0C767D8A1A2B5" },
      "whatsapp_id": "3EB0C767D8A1A2B5",
      "conversation": { "id": "739182640518203", "phoneNumber": "+15550001111" },
      "direction": "inbound",
      "fromMe": false,
      "type": "image",
      "timestamp": "2026-09-18T10:05:00.000Z",
      "pushName": "Ana",
      "caption": "Receipt",
      "mimeType": "image/jpeg",
      "media": [
        {
          "id": "0e6f1c3a-5a55-4f3c-9d4f-0b1f8f2a9c11",
          "mimeType": "image/jpeg",
          "fileLength": 48213,
          "url": "/messaging/media/0e6f1c3a-5a55-4f3c-9d4f-0b1f8f2a9c11"
        }
      ]
    }
  ],
  "hasMore": true,
  "nextCursor": "eyJ2IjoxLCJz...",
  "previousCursor": null
}
```

| Query parameter | Meaning                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------ |
| `limit`         | Page size, 1 to 100. Default 50.                                                                       |
| `cursor`        | `nextCursor` or `previousCursor` from an earlier page.                                                 |
| `order`         | `desc` (newest first, default) or `asc` (oldest first).                                                |
| `since`         | Messages sent at or after this ISO 8601 time.                                                          |
| `until`         | Messages sent before this ISO 8601 time. `since` must be earlier.                                      |
| `direction`     | `inbound` (from contacts) or `outbound` (sent by the Number, including from its other linked devices). |
| `types`         | Comma-separated message types, up to 16, for example `text,image`.                                     |

`whatsapp_ids` object holds the provider references that Polymorfa observed for
the message. It contains `linked_devices`, `official_api`, or both. Use the
Polymorfa `id` to fetch a message or send a reply. The older `whatsapp_id`
field remains temporarily for integrations that still read one reference.

In a group, an inbound message includes `conversation.sender`, the author. The
message fields match `message.received`: `text`, `caption`, `mimeType`,
`filename`, `ptt`, `latitude`, `longitude`, `displayName`, `title`, `reaction`,
`reactionTo`, `edited`, `unavailable`, `unavailableReason`, and `pollOptions`
appear when the stored message has them. Messages sent through the API are stored
with their type and identifiers; their body is not part of the stored record.

## Paginate

Cursors are opaque. Pass them back unchanged, with the same path and filters. A
cursor used with different filters or a different conversation returns
`400 invalid_parameter` with `param: "cursor"`; start again without a cursor.

* `nextCursor` continues in the current `order`. It is `null` on the last page,
  and `hasMore` is `false`.
* `previousCursor` returns the page before the current one. It is `null` on the
  first page. Conversation lists do not have a previous cursor.

```ts theme={null}
let cursor: string | undefined;
do {
  const url = new URL("https://api.polymorfa.com/messaging/support-line/chats/739182640518203/messages");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  if (!response.ok) throw new Error(`history request failed: ${response.status}`);
  const page = await response.json();
  for (const message of page.data) store(message);
  cursor = page.nextCursor ?? undefined;
} while (cursor);
```

## Media

History never returns WhatsApp media keys, download paths, or storage URLs. An
image, video, audio, document, or sticker message has a `media` array. Each entry
names a file you can download with [Download media](/guides/communicate/api/download-linked-media)
using a credential with `media:read`. The array is empty when Polymorfa has no
downloadable copy of that file.

## Retention and data region

History returns only messages inside the Number's hosted storage retention
period, or under legal hold. Expired messages are not returned, and a message can
disappear between two pages when its retention period ends.

Every successful response has a `Polymorfa-Data-Region` header naming the region
that stored and served the history. History is read only in the Number's region.

## Errors

| Status | Code                  | When                                                                      |
| ------ | --------------------- | ------------------------------------------------------------------------- |
| `400`  | `invalid_parameter`   | A filter is invalid, or a cursor does not match the request.              |
| `403`  | `missing_scope`       | The credential lacks `chats:read` or `messages:read`.                     |
| `403`  | `permission_denied`   | The request used a client token, or the team is not enrolled in the beta. |
| `404`  | `hms_not_enabled`     | Hosted message storage is off for the Number.                             |
| `404`  | `resource_not_found`  | The Number, conversation, or message is not available to the credential.  |
| `503`  | `service_unavailable` | History for the Number's region is temporarily unavailable. Retry later.  |

See [Errors](/api/errors) for the full error format.
