Skip to Content
API access is available to organizations with a signed BAA. Request API access →
Read messages

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 APIOAuth
EndpointGET /patient-messagesGET /me/messages
CredentialAn API key, or an org admin’s OAuth tokenA person’s OAuth token
ReadsPatient conversations, across the organizationThe person’s own conversations: team chats and their patient chats
Conversations listGET /patient-conversationsGET /me/conversations

For example, the last week:

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'
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

ParameterMeaning
afterMessages sent at or after this time.
beforeMessages 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.

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

ParameterReturns
conversation_idEvery message in one conversation.
patient_idEvery message in all of one patient’s conversations, on any phone number.
qMessages 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). Follow pagination.next_cursor in page[after] until has_more is false. 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:

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 }

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.

Last updated on