Send messages to patients
Reach a patient with one request. Send a message to a patient takes a patient ID or phone number and the text. BloomText finds the patient (or creates one for a new number), finds or starts the conversation on the right clinic phone number, and sends the message.
cURL
curl -X POST https://api.bloomtext.com/v1/patient-messages \
-H "Authorization: Bearer $BLOOMTEXT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"to": { "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77" },
"body": "Hi Jane, Dr. Example moved your appointment to Friday at 9:00. Reply here with any questions.",
"channel": "secure"
}'The response holds the sent message, with its conversation, patient, channel, and delivery status, plus patient_created and conversation_created.
A send returns 201 Created. When your organization requires consent before texting and the patient’s phone hasn’t agreed yet, it returns 202 Accepted with the same body. The message is in the conversation with delivery.status consent_requested, and BloomText has texted the patient a consent request. The text goes out under the same message id when the patient replies YES or staff record their consent. Don’t send it again.
Use an API key with the messages.write scope, even when the send creates a patient. An OAuth token works the same way.
Who it comes from
Your integration sends as its API identity, the automated sender the key belongs to. Nothing is sent on behalf of a staff member.
| The patient sees | Staff see | |
|---|---|---|
| With an API identity | Its name in secure chat; the office’s number on SMS | Its name and avatar, marked as automated |
| Today’s default, without one | The clinic | The key’s name, like “Reminder service”, with an API marker |
See Messages sent through the API.
Who gets it
Set to to exactly one of:
to | What happens |
|---|---|
{"patient_id": "…"} | Sends to that patient. |
{"phone": "…"} | Looks up the phone number in your organization’s patients. |
A phone number can be in any common format, like (512) 555-0143 or +15125550143. BloomText resolves it this way:
- No patient has that number: BloomText creates one, as it does when a new number texts the clinic. Add a
patientobject withfirst_name,last_name,date_of_birth,email, ormrnto fill in the new record. - One patient has it: that patient.
- Several patients share it, like a family phone:
409 ambiguous_recipient. BloomText never guesses; the error’scandidateslists the matching patients. Send again with the rightpatient_id, or addpatient.mrnto say which one. patient.mrnpoints at someone else:409 recipient_mismatch, and nothing is sent. That’s when the MRN belongs to a patient with a different phone number, or no one has the MRN but the number’s only patient has another one. BloomText only ever texts the number you sent to, and never files a message under a patient the MRN contradicts.candidateslists the patient the MRN or the number belongs to. If their number changed, update the patient and send again, or send with theirpatient_id.
{
"to": {
"phone": "(512) 555-0199",
"patient": { "first_name": "John", "last_name": "Doe", "mrn": "MRN-21004" }
},
"body": "Welcome to Riverbend Health. Your intake form is ready.",
"channel": "sms"
}Secure or plain text
Every send names its channel. There’s no default, because the two reach the patient differently; a send without one returns 422 channel_required.
channel | The patient receives | Use it for |
|---|---|---|
secure | A text or email with a secure link. The message itself stays inside BloomText, and the patient reads and replies there. | Anything clinical or personal. |
sms | The message text as a regular SMS from the clinic’s number. Replies come back by SMS. | Logistics, like reminders, directions, or “your forms are ready.” |
{
"to": { "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77" },
"body": "Reminder: your appointment is tomorrow at 9:00 at 1200 Main St.",
"channel": "sms"
}Plain SMS puts the message on the patient’s phone and the carrier’s network, outside BloomText. Keep clinical detail out of sms messages.
Which number it comes from
Each clinic phone number belongs to a team, like “Front desk” at +1 512-555-0100 or “Billing” at +1 512-555-0200. Texts come from one of them, and the patient’s replies go back to that team’s conversation. Name the number with from:
{
"to": { "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77" },
"from": "+15125550200",
"body": "Your statement is ready. Reply here with any billing questions.",
"channel": "sms"
}- List phone numbers returns the numbers you can send from: every number in the organization, or only the key’s numbers with a phone-number limit.
- Without
from, BloomText uses the default number, markedis_default: the one set on the API key, or else the organization’s only number. A key’s default that’s no longer one of the organization’s numbers is ignored. - If there’s no default and more than one number, the request returns
422 from_required. It never picks one for you;detailandnumberslist every number you can choose:
{
"type": "https://www.bloomtext.com/developers/api/errors/#from_required",
"title": "Unprocessable Entity",
"status": 422,
"code": "from_required",
"detail": "This organization has 2 numbers. Set \"from\" to one of them: +15025550142 (WIC), +15025550199 (Immunizations). See GET /v1/phone-numbers.",
"request_id": "0b6c2d1e-5f4a-4c3b-9a8d-7e6f5a4b3c2d",
"field_errors": [],
"numbers": [
{ "number": "+15025550142", "team": "WIC" },
{ "number": "+15025550199", "team": "Immunizations" }
]
}Send again with "from": "+15025550142".
BloomText keeps one conversation per phone number and patient. Sending again from the same number continues that conversation; sending from another team’s number starts a separate one.
Reply in a conversation
With an API key, reply to a patient with this same request: name the patient and the from number, and BloomText continues their conversation on that number.
With OAuth, you can also reply by conversation ID, for example one you found by reading messages. Send a message in the conversation (POST /me/conversations/{id}/messages); it uses the conversation’s phone number and channel.
curl -X POST https://api.bloomtext.com/v1/me/conversations/e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0/messages \
-H "Authorization: Bearer $BLOOMTEXT_ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"body": "Friday at 9:00 works. You are all set."}'Delivery status
A text can fail after BloomText accepts it: a landline, a disconnected number, or a carrier block. Every message to a patient carries a delivery object that BloomText updates from the carrier’s reports, which arrive within minutes. Messages from patients have delivery: null.
delivery.status | Meaning |
|---|---|
queued | Accepted, not yet handed to the carrier. |
sent | Handed to the carrier, or a secure message whose link went by email. |
delivered | The carrier confirmed delivery. |
failed, undelivered | The text didn’t reach the patient. |
rejected | BloomText didn’t send the text: after you sent it, the patient opted out, the carrier blocked texts to them, an admin blocked their number, their number can’t receive texts, your organization requires consent and the patient hasn’t given it, or the text waited for consent and wasn’t sent. See Opt-outs and blocked numbers. |
consent_requested | Your organization requires consent before texting, and BloomText texted the patient a consent request. The text goes out when they reply YES or staff record their consent. |
When a text isn’t delivered, delivery.error_code says why and delivery.detail explains it. The common codes:
error_code | Meaning | What to do |
|---|---|---|
patient_opted_out | The patient opted out. | Stop texting them. Texts resume when the patient texts START, or when an admin clears an opt-out staff recorded. |
carrier_blocked | The patient texted STOP. | Stop texting them. The patient can text START to get texts again. |
number_blocked | An admin blocked the patient’s number. | Stop texting them; an admin can unblock it. |
number_unreachable | The patient’s number can’t receive texts (a landline or unreachable). | Ask the patient for a mobile number and update their profile, or send secure. |
consent_required | Your organization requires consent and the patient hasn’t given it. This is rare: a send that needs consent normally waits as consent_requested. | Ask the patient to reply YES, or have staff record their consent, then send again. |
consent_not_sent | The text waited for consent and wasn’t sent: the consent request couldn’t be sent, or sending the text failed. A blocked or unreachable number reports its own code instead, like carrier_blocked or number_unreachable. | Send it again if you still need to. |
30003 | The phone was off or had no signal. | Usually clears on its own. If it keeps failing, call the patient to check the number. |
30004 | The patient’s phone or carrier blocked the text. | Ask the patient whether they block unknown numbers, or send secure. Contact BloomText if many patients show this. |
30005 | Number not in service. | Get a current number from the patient and update their profile. |
30006 | A landline, or a number that can’t get texts. | Ask the patient for a mobile number, or call them. Until it’s updated, later texts to the number are refused with number_unreachable. |
30007 | The carrier’s spam filter blocked the text. | Contact BloomText, which reviews the number’s registration and wording with the carrier. Avoid link shorteners. |
30008 | The carrier gave no reason. | Try again later, and contact BloomText if it keeps happening. |
30019 | The text was too long for the patient’s carrier; emoji and accented letters lower the limit. | Send a shorter text, and contact BloomText if it keeps happening. |
30024 | The patient’s carrier hasn’t set up this organization’s number yet, or the patient just switched carriers. | Try again tomorrow, and contact BloomText if it keeps failing. |
60005 | A temporary carrier connection error. | Send the text again. |
21610 | The patient texted STOP to this number. | Stop texting them from it. The patient can text START to this number to get texts again. |
Every detail says what happened and then what to do, in at most two sentences.
Other codes are the carrier’s own; detail names them and says to contact BloomText with the code if it keeps happening.
A message to several patients, in a group conversation, also has delivery.recipients, with each patient’s status. The top-level status is the worst of them: failed if any failed, and delivered only when all were.
{
"status": "failed",
"error_code": "30005",
"detail": "Number not in service. Get a current number from the patient and update their profile.",
"updated_at": "2026-10-12T14:07:31Z"
}To follow up, read messages with after set to the start of the day and q=is:failed (or is:rejected, is:undelivered, is:queued, is:delivered), or check one message with Retrieve a patient message. Statuses arrive by polling, or by webhooks.
Getting replies and delivery status
Poll List patient messages for both replies and delivery status. With webhooks, BloomText calls your server instead. To poll:
- Ask for what’s new. Pass
afterset to the newestcreated_atyou’ve processed, withorder=ascandpage[limit]=100, and followpagination.next_cursorinpage[after]untilhas_moreisfalse. Messages are keyed byid, so seeing one twice is harmless. The full loop is in Keep up with new messages. - Poll every 1 to 5 minutes. That’s well within the rate limits, and patients rarely expect a faster answer by text. Don’t poll faster than once a minute.
- Tell replies from your own messages by
sender.kind:patientis a patient writing in;staffandapi_keyare your organization writing out. To read only replies, addq=from:patient. See Search. - Track delivery on the messages you sent. Each message carries
delivery.status:queued, thensent, thendelivered, orfailed,undeliveredorrejectedwithdelivery.error_codeanddelivery.detailsaying why. See Delivery status. Re-read recent messages until they settle; a status rarely changes after a day.
curl -G https://api.bloomtext.com/v1/patient-messages \
-H "Authorization: Bearer $BLOOMTEXT_API_KEY" \
--data-urlencode 'after=2026-10-12T14:05:00Z' \
--data-urlencode 'q=from:patient' \
--data-urlencode 'order=asc' \
--data-urlencode 'page[limit]=100'Opt-outs and blocked numbers
BloomText keeps one list of patient numbers your organization can’t text, and checks it before it queues an SMS. If the patient’s number is on it, the send fails with the reason, and nothing is sent. Each reason blocks texts from all of your organization’s numbers, whichever from you pick. Batch sends report the same codes per message.
-
The patient opted out: they replied STOP to one of your organization’s numbers, or staff recorded their opt-out in the app.
Response · 422 Unprocessable Entity{ "type": "https://www.bloomtext.com/developers/api/errors/#patient_opted_out", "title": "Unprocessable Entity", "status": 422, "code": "patient_opted_out", "detail": "This patient replied STOP on Oct 5, so texts from (502) 555-0142 and the organization's other numbers are blocked until they reply START. Stop sending them texts; a secure message can still reach them by email.", "number": "+15025550142", "since": "2026-10-05T15:04:00Z", "request_id": "0b6c2d1e-5f4a-4c3b-9a8d-7e6f5a4b3c2d", "field_errors": [] }numberis the number the text would have come from, andsinceis when the opt-out began. What to do: stop texting this patient. Only the patient can lift it, by replying START. Asecuremessage still reaches them by email, if they have one. -
The carrier is blocking texts to the patient, because it refused an earlier text after they replied STOP to that number:
422 carrier_blocked, with the samenumberandsincefields. What to do: stop texting them. The patient lifts it by texting START to one of your numbers. -
An admin blocked the number in the app:
422 number_blocked, with the samenumberandsincefields. A blocked number also can’t text your organization. What to do: stop texting them. Only an admin can unblock it, in the app. -
The number can’t receive texts, because an earlier text failed with carrier error 30006 (a landline, or a carrier that couldn’t be reached):
422 number_unreachable, with the samenumberandsincefields. What to do: update the patient’s mobile number, or sendsecure. It also clears when the patient texts one of your numbers, or when an admin clears it in the app.
If more than one applies, the code is the first of these.
To check before you send, retrieve the patient: sms_opted_out is true when the patient can’t be texted for any reason, and sms_undeliverable_reasons lists why (opted_out, carrier_blocked, admin_blocked, unreachable).
- The patient has no phone number:
422 patient_has_no_phone. What to do: update the patient, or sendsecureif they have an email address.
The same check runs again when the text goes out, because a patient can reply STOP in between. Then the message is kept, and its delivery.status becomes rejected, with the reason in delivery.error_code (patient_opted_out, carrier_blocked, number_blocked or number_unreachable).
Secure messages aren’t refused for an opt-out or a block. As with a message staff send, the link still goes by email, and the text carrying the link is skipped.
Long texts
An SMS body can be up to 1,600 characters. A longer one returns 422 too_long, with length and max:
{
"code": "too_long",
"detail": "This text is 1,750 characters; SMS allows 1,600. Shorten it, split it yourself, or send it with channel \"secure\".",
"length": 1750,
"max": 1600
}BloomText doesn’t split a long text for you. You know where your text can break, and each piece would be a separate message. Keep in mind:
- Carriers bill a long SMS as several segments, about 160 characters each, or 70 with emoji or non-Latin characters.
- The first text a patient gets from your organization may end with a short opt-out line (“Reply STOP to stop”) when it fits, so leave a little room.
- A
securemessage can be up to 40,000 characters.
Errors to handle
| Status | Code | What to do |
|---|---|---|
| 403 | plan_required | The organization’s plan doesn’t include patient messaging. Contact BloomText. A batch is refused whole. |
| 404 | not_found | The patient_id doesn’t exist in your organization, or from isn’t one of its numbers. |
| 409 | ambiguous_recipient | Pick a patient from candidates and send with patient_id, or add patient.mrn. |
| 409 | recipient_mismatch | patient.mrn and to.phone point at different patients. Update the patient’s phone, or send with patient_id. |
| 422 | channel_required | Set channel to sms or secure. See Secure or plain text. |
| 422 | from_required | Set from to one of the numbers in the error’s numbers. |
| 422 | too_long | Shorten the SMS, split it yourself, or send it secure. See Long texts. |
| 422 | patient_has_no_phone | Add a phone number, or send secure to a patient with an email address. |
| 422 | no_contact_method | A secure send to a patient with neither phone nor email. Update the patient. |
| 422 | patient_opted_out | Stop texting them; they must reply START. See Opt-outs and blocked numbers. |
| 422 | carrier_blocked | Stop texting them; they can text START to one of your numbers. |
| 422 | number_blocked | Stop texting them; an admin blocked the number, and only an admin can unblock it. |
| 422 | number_unreachable | The number can’t receive texts. Update the patient’s mobile number, or send secure. |
Every send takes an Idempotency-Key, so a retry after a timeout never texts a patient twice. For automated jobs, derive the key from something stable, like the appointment ID. See Idempotency.
Sending to many patients
To send a different message to each of many patients, like a morning’s appointment reminders, use Send messages in a batch. One request carries up to 60 messages, each the body of a single send:
curl -X POST "https://api.bloomtext.com/v1/messages:batch" \
-H "Authorization: Bearer $BLOOMTEXT_API_KEY" \
-H "Idempotency-Key: 0b8e6f6a-3c1d-4f5e-9a2b-7c4d8e1f2a30" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": { "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77" }, "body": "Reminder: your appointment is tomorrow at 9:30.", "channel": "secure" },
{ "to": { "phone": "+15125550143" }, "body": "Reminder: your appointment is tomorrow at 10:00.", "channel": "sms" },
{ "to": { "patient_id": "9f2d1c4b-0e3a-4b7c-8d6e-1a2b3c4d5e6f" }, "body": "Reminder: your appointment is tomorrow at 11:15.", "channel": "sms" }
]
}'Each message is checked exactly like a single send, and one message’s error never fails the others. The exception is a field the API doesn’t know: that’s checked for the whole body first, and any unknown field in any message returns 422 unknown_field for the batch, naming the message (/messages/3/...). Nothing is sent. See Unknown fields are rejected. The response is 207 Multi-Status, with one result per message in the order you sent them:
{
"results": [
{ "index": 0, "id": "3f1e2d4c-5b6a-4789-8a1b-2c3d4e5f6a7b", "status": "queued" },
{ "index": 1, "id": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d", "status": "queued" },
{
"index": 2,
"error": {
"type": "https://www.bloomtext.com/developers/api/errors/#patient_has_no_phone",
"title": "Unprocessable Entity",
"status": 422,
"code": "patient_has_no_phone",
"detail": "The patient has no phone number, so there's nothing to text. Add one with Update a patient, or send with channel \"secure\" if they have an email address.",
"request_id": "0b6c2d1e-5f4a-4c3b-9a8d-7e6f5a4b3c2d",
"field_errors": []
}
}
]
}- Retry the whole batch with the same
Idempotency-Keyand the same body. One key covers the batch: a retry returns the same results and sends nothing twice. Fix a failed message, then send it again in a new request with a new key. - It’s one request toward the rate limits, and each message counts toward your organization’s 60 messages per minute, shared by all its API keys. 200 reminders are 4 batches of up to 60, one a minute. After a
429, waitRetry-Afterand retry with the sameIdempotency-Key. See Rate limits. - Each queued message’s delivery status is on the message, as for a single send. See Delivery status.
- A message that waits for the patient’s consent has
status: consent_requestedinstead ofqueued. Its text goes out when the patient replies YES or staff record their consent, so don’t send it again.
To send the same message to many patients, you can also use a campaign.