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

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.

Request
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 seesStaff see
With an API identityIts name in secure chat; the office’s number on SMSIts name and avatar, marked as automated
Today’s default, without oneThe clinicThe 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:

toWhat 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 patient object with first_name, last_name, date_of_birth, email, or mrn to 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’s candidates lists the matching patients. Send again with the right patient_id, or add patient.mrn to say which one.
  • patient.mrn points 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. candidates lists the patient the MRN or the number belongs to. If their number changed, update the patient and send again, or send with their patient_id.
Request · a new number
{ "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.

channelThe patient receivesUse it for
secureA text or email with a secure link. The message itself stays inside BloomText, and the patient reads and replies there.Anything clinical or personal.
smsThe 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.”
Request · plain SMS
{ "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:

Request
{ "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, marked is_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; detail and numbers list every number you can choose:
Response · 422 Unprocessable Entity
{ "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.

Your BloomText team turns this on for your account. Book a call and we’ll set it up with you.Request access

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.

Request
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.statusMeaning
queuedAccepted, not yet handed to the carrier.
sentHanded to the carrier, or a secure message whose link went by email.
deliveredThe carrier confirmed delivery.
failed, undeliveredThe text didn’t reach the patient.
rejectedBloomText 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_requestedYour 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_codeMeaningWhat to do
patient_opted_outThe patient opted out.Stop texting them. Texts resume when the patient texts START, or when an admin clears an opt-out staff recorded.
carrier_blockedThe patient texted STOP.Stop texting them. The patient can text START to get texts again.
number_blockedAn admin blocked the patient’s number.Stop texting them; an admin can unblock it.
number_unreachableThe 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_requiredYour 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_sentThe 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.
30003The phone was off or had no signal.Usually clears on its own. If it keeps failing, call the patient to check the number.
30004The 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.
30005Number not in service.Get a current number from the patient and update their profile.
30006A 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.
30007The carrier’s spam filter blocked the text.Contact BloomText, which reviews the number’s registration and wording with the carrier. Avoid link shorteners.
30008The carrier gave no reason.Try again later, and contact BloomText if it keeps happening.
30019The 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.
30024The 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.
60005A temporary carrier connection error.Send the text again.
21610The 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.

delivery
{ "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:

  1. Ask for what’s new. Pass after set to the newest created_at you’ve processed, with order=asc and page[limit]=100, and follow pagination.next_cursor in page[after] until has_more is false. Messages are keyed by id, so seeing one twice is harmless. The full loop is in Keep up with new messages.
  2. 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.
  3. Tell replies from your own messages by sender.kind: patient is a patient writing in; staff and api_key are your organization writing out. To read only replies, add q=from:patient. See Search.
  4. Track delivery on the messages you sent. Each message carries delivery.status: queued, then sent, then delivered, or failed, undelivered or rejected with delivery.error_code and delivery.detail saying why. See Delivery status. Re-read recent messages until they settle; a status rarely changes after a day.
Request
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": [] }

    number is the number the text would have come from, and since is when the opt-out began. What to do: stop texting this patient. Only the patient can lift it, by replying START. A secure message 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 same number and since fields. 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 same number and since fields. 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 same number and since fields. What to do: update the patient’s mobile number, or send secure. 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 send secure if 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:

Response · 422 Unprocessable Entity
{ "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 secure message can be up to 40,000 characters.

Errors to handle

StatusCodeWhat to do
403plan_requiredThe organization’s plan doesn’t include patient messaging. Contact BloomText. A batch is refused whole.
404not_foundThe patient_id doesn’t exist in your organization, or from isn’t one of its numbers.
409ambiguous_recipientPick a patient from candidates and send with patient_id, or add patient.mrn.
409recipient_mismatchpatient.mrn and to.phone point at different patients. Update the patient’s phone, or send with patient_id.
422channel_requiredSet channel to sms or secure. See Secure or plain text.
422from_requiredSet from to one of the numbers in the error’s numbers.
422too_longShorten the SMS, split it yourself, or send it secure. See Long texts.
422patient_has_no_phoneAdd a phone number, or send secure to a patient with an email address.
422no_contact_methodA secure send to a patient with neither phone nor email. Update the patient.
422patient_opted_outStop texting them; they must reply START. See Opt-outs and blocked numbers.
422carrier_blockedStop texting them; they can text START to one of your numbers.
422number_blockedStop texting them; an admin blocked the number, and only an admin can unblock it.
422number_unreachableThe 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:

Request
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:

Response · 207 Multi-Status
{ "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-Key and 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, wait Retry-After and retry with the same Idempotency-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_requested instead of queued. 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.

Last updated on