# Read messages

> Read BloomText messages in a time range with GET /patient-messages or GET /me/messages, one by one or grouped by conversation, and keep up with new ones.

Source: https://www.bloomtext.com/developers/api/read-messages/

Read every message in a time range, newest first, one by one or grouped by conversation. Each path has one endpoint for this. Both take the same range, cursor, and grouping, and differ only in whose conversations they read.

| | Patient API | OAuth |
| --- | --- | --- |
| Endpoint | [`GET /patient-messages`](https://www.bloomtext.com/developers/api/reference/list-patient-messages/) | [`GET /me/messages`](https://www.bloomtext.com/developers/api/reference/list-messages/) |
| Credential | An [API key](https://www.bloomtext.com/developers/api/api-keys/), or an org admin's OAuth token | A person's [OAuth](https://www.bloomtext.com/developers/api/oauth/) token |
| Reads | Patient conversations, across the organization | The person's own conversations: team chats and their patient chats |
| Conversations list | [`GET /patient-conversations`](https://www.bloomtext.com/developers/api/reference/list-patient-conversations/) | [`GET /me/conversations`](https://www.bloomtext.com/developers/api/reference/list-conversations/) |

For example, the last week:

```bash filename="Request"
curl -G https://api.bloomtext.com/v1/patient-messages \
  -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \
  --data-urlencode 'after=2026-09-21T00:00:00Z' \
  --data-urlencode 'before=2026-09-28T00:00:00Z'
```

```bash filename="Request"
curl -G https://api.bloomtext.com/v1/me/messages \
  -H "Authorization: Bearer $BLOOMTEXT_ACCESS_TOKEN" \
  --data-urlencode 'after=2026-09-21T00:00:00Z' \
  --data-urlencode 'before=2026-09-28T00:00:00Z'
```

```json filename="Response · 200 OK"
{
  "data": [
    {
      "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1",
      "conversation": { "id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0", "title": "Jane Doe" },
      "patient": { "id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77", "name": "Jane Doe" },
      "sender": { "id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77", "name": "Jane Doe", "kind": "patient" },
      "created_at": "2026-09-27T16:02:00Z",
      "body": "Hi, can I move my Thursday appointment to Friday morning?",
      "...": "..."
    }
  ],
  "pagination": { "next_cursor": "eyJpZCI6IjRjYmZkYjUwIn0", "has_more": true }
}
```

Results include everyone's messages in each conversation (staff, patients, and API keys), not only the connected person's. The rest of this page applies to both endpoints.

## Choose the range

| Parameter | Meaning |
| --- | --- |
| `after` | Messages sent at or after this time. |
| `before` | Messages sent before this time. |

Both take an RFC 3339 timestamp, like `2026-09-21T13:00:00Z`, or a date, like `2026-09-21`, which means midnight in the organization's time zone. Leave `before` out to read up to now.

With an API key, every read needs a bound: pass `after`, `patient_id`, or `conversation_id`, or the request returns `400 range_required`.

## One by one, or grouped by conversation

`group_by` decides the order:

Messages one by one, newest first, across all conversations: the fastest way to see what's recent. Each message carries its `conversation`, so you can tell which chat it belongs to.

```text
Jane Doe        4:02 PM  "Can I move my Thursday appointment?"
Front desk      3:55 PM  "Room 2 is open again"
John Doe        3:41 PM  "Thanks, see you then"
Jane Doe        3:30 PM  "Hi, it's Jane"
```

Every message in the range from one conversation, then the next, most recently active conversation first. Within each, messages follow `order`. Use it to read a chat as a whole.

```text
Jane Doe        4:02 PM  "Can I move my Thursday appointment?"
Jane Doe        3:30 PM  "Hi, it's Jane"
Front desk      3:55 PM  "Room 2 is open again"
John Doe        3:41 PM  "Thanks, see you then"
```

Both return the same flat list of messages; only the order changes. `order=asc` flips it to oldest first, which reads more naturally when you summarize a period.

## Narrow it down

| Parameter | Returns |
| --- | --- |
| `conversation_id` | Every message in one conversation. |
| `patient_id` | Every message in all of one patient's conversations, on any phone number. |
| `q` | Messages matching words and filters, like `refill from:patient`. See [Search messages](https://www.bloomtext.com/developers/api/search/). |

To see which conversations were active before you read them, use [`GET /patient-conversations`](https://www.bloomtext.com/developers/api/reference/list-patient-conversations/) or [`GET /me/conversations`](https://www.bloomtext.com/developers/api/reference/list-conversations/) with the same `after` and `before`. Each conversation comes back with its latest message and how many messages fell in the range.

## When there's too much

- Pages hold up to 100 messages (`page[limit]`, default 50). Follow `pagination.next_cursor` in `page[after]` until `has_more` is `false`. See [Pagination](https://www.bloomtext.com/developers/api/pagination/).
- The cursor holds your place. It remembers the range, grouping, and where you were when you started, so messages that arrive while you page don't shift pages or appear twice.
- One big conversation doesn't crowd out the rest. With `group_by=conversation`, a chat with 2,000 messages in the range spans several pages, and the next chat starts where it ends.
- Narrow the range if you need only part of it. A day at a time is easy to page through.

## Keep up with new messages

There's no separate change feed. To pick up what's new, ask again with `after` set to the newest `created_at` you've processed:

```js filename="keep-up.mjs"
const API = 'https://api.bloomtext.com/v1'

// Run on a schedule. `since` is the newest created_at from the last run.
export async function readNew(credential, since, handle) {
  let cursor = null
  let newest = since
  do {
    const url = new URL(`${API}/patient-messages`) // or /me/messages with OAuth
    url.searchParams.set('after', since)
    url.searchParams.set('order', 'asc')
    url.searchParams.set('page[limit]', '100')
    if (cursor) url.searchParams.set('page[after]', cursor)

    const response = await fetch(url, { headers: { Authorization: `Bearer ${credential}` } })
    if (!response.ok) throw new Error(`BloomText API error: ${response.status}`)
    const { data, pagination } = await response.json()
    for (const message of data) {
      await handle(message) // keyed by message.id, so a repeat is harmless
      newest = message.created_at
    }
    cursor = pagination.has_more ? pagination.next_cursor : null
  } while (cursor)
  return newest // save for the next run
}
```

```python filename="keep_up.py"
import requests

API = "https://api.bloomtext.com/v1"


def read_new(credential: str, since: str, handle) -> str:
    """Run on a schedule. `since` is the newest created_at from the last run."""
    params = {"after": since, "order": "asc", "page[limit]": 100}
    newest = since
    while True:
        response = requests.get(f"{API}/patient-messages", headers={"Authorization": f"Bearer {credential}"}, params=params)
        response.raise_for_status()
        page = response.json()
        for message in page["data"]:
            handle(message)  # keyed by message["id"], so a repeat is harmless
            newest = message["created_at"]
        if not page["pagination"]["has_more"]:
            return newest  # save for the next run
        params["page[after]"] = page["pagination"]["next_cursor"]
```

`after` includes messages sent at exactly that time, so the last run's newest message comes back once more. Handle messages by `id` and the repeat does nothing.

With an API key and `/patient-messages`, this picks up every patient's replies, including replies to [campaigns](https://www.bloomtext.com/developers/api/campaigns/). Add `q=from:patient` to skip your own sends.

A read returns each message as it is now: an edited message shows its latest text and `edited_at`, and deleted messages don't appear. A range can't tell you that something changed after you read it; read it again if you need the current state.
