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 | GET /me/messages |
| Credential | An API key, or an org admin’s OAuth token | A person’s OAuth token |
| Reads | Patient conversations, across the organization | The person’s own conversations: team chats and their patient chats |
| Conversations list | GET /patient-conversations | GET /me/conversations |
For example, the last week:
Patient API
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'{
"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:
group_by=message (default)
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.
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"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. |
To see which conversations were active before you read them, use GET /patient-conversations or GET /me/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). Followpagination.next_cursorinpage[after]untilhas_moreisfalse. See 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:
JavaScript
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
}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. 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.