# Send messages to patients

> Text a patient by phone number or patient ID in one request. BloomText finds or creates the patient, sends securely by default, and reports delivery.

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

Reach a patient with one request. [Send a message to a patient](https://www.bloomtext.com/developers/api/reference/send-message/) 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.

```bash filename="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."
  }'
```

```js filename="send.mjs"
export async function messagePatient(credential, to, body, { channel, idempotencyKey } = {}) {
  const response = await fetch('https://api.bloomtext.com/v1/patient-messages', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${credential}`,
      'Idempotency-Key': idempotencyKey ?? crypto.randomUUID(),
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ to, body, ...(channel && { channel }) }),
  })
  const result = await response.json()
  if (!response.ok) throw Object.assign(new Error(result.detail), { problem: result })
  return result // { message, patient_created, conversation_created }
}

await messagePatient(process.env.BLOOMTEXT_API_KEY, { patient_id: '5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77' },
  'Hi Jane, Dr. Example moved your appointment to Friday at 9:00. Reply here with any questions.')
```

```python filename="send.py"
import os
import uuid

import requests


def message_patient(credential: str, to: dict, body: str, channel: str | None = None, idempotency_key: str | None = None) -> dict:
    payload = {"to": to, "body": body}
    if channel:
        payload["channel"] = channel
    response = requests.post(
        "https://api.bloomtext.com/v1/patient-messages",
        headers={
            "Authorization": f"Bearer {credential}",
            "Idempotency-Key": idempotency_key or str(uuid.uuid4()),
        },
        json=payload,
    )
    result = response.json()
    if not response.ok:
        raise RuntimeError(f"{result['code']}: {result.get('detail')}")
    return result  # message, patient_created, conversation_created


message_patient(
    os.environ["BLOOMTEXT_API_KEY"],
    {"patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77"},
    "Hi Jane, Dr. Example moved your appointment to Friday at 9:00. Reply here with any questions.",
)
```

The response holds the sent [message](https://www.bloomtext.com/developers/api/reference/message-object/), with its conversation, patient, channel, and [delivery status](#delivery-status), plus `patient_created` and `conversation_created`.

Use an [API key](https://www.bloomtext.com/developers/api/api-keys/) or an [org admin's OAuth token](https://www.bloomtext.com/developers/api/oauth/). The request needs only the `messages.write` scope, even when it creates a patient.

## Who it comes from

| Sent with | The patient sees | Staff see |
| --- | --- | --- |
| An API key | The clinic | The key's name, like "Reminder service" |
| OAuth | The admin | The admin, with a "via" label naming your app |

With OAuth, sending also marks the conversation as read for the admin, as replying in BloomText does. See [Messages sent through the API](https://www.bloomtext.com/developers/api/concepts/#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 `patient` object with `first_name`, `last_name`, `date_of_birth`, `email`, or `mrn` to fill in the new record. If `patient.mrn` matches an existing patient, BloomText uses that patient instead of creating one.
- **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.

```json filename="Request · a new number"
{
  "to": {
    "phone": "(512) 555-0199",
    "patient": { "first_name": "John", "last_name": "Doe", "mrn": "MRN-21004" }
  },
  "body": "Welcome to Example Clinic. Your intake form is ready."
}
```

## Secure or plain text

Messages to patients are secure unless you set `channel` to `sms`:

| `channel` | The patient receives | Use it for |
| --- | --- | --- |
| `secure` (default) | 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." |

```json filename="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"
}
```

> **Warning:** 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`:

```json filename="Request"
{
  "to": { "patient_id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77" },
  "from": "+15125550200",
  "body": "Your statement is ready. Reply here with any billing questions."
}
```

- [List phone numbers](https://www.bloomtext.com/developers/api/reference/list-phone-numbers/) returns the numbers you can send from: every number in the organization for an API key, or the admin's teams' numbers for OAuth.
- Without `from`, BloomText uses the default number, marked `is_default`: the one set on the API key when it was created, or the admin's own default.
- If there's no default and more than one number, the request returns `422 from_required`. Pass `from`.

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](https://www.bloomtext.com/developers/api/read-messages/). [Send a message](https://www.bloomtext.com/developers/api/reference/create-message/) in the conversation (`POST /me/conversations/{id}/messages`); it uses the conversation's phone number and channel.

```bash filename="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` status that BloomText updates from the carrier.

| `delivery.status` | Meaning |
| --- | --- |
| `queued` | Accepted, not yet handed to the carrier. |
| `sent` | Handed to the carrier. |
| `delivered` | The carrier confirmed delivery. |
| `opened` | The patient opened the secure link. |
| `failed`, `undelivered` | The message didn't reach the patient. `delivery.error` says why, like `landline`, `invalid_number`, or `opted_out`. |

To follow up on failures, [read messages](https://www.bloomtext.com/developers/api/read-messages/) with `after` set to the start of the day and `q=is:failed`, or check one message with [Retrieve a patient message](https://www.bloomtext.com/developers/api/reference/get-patient-message/).

## Opt-outs

A patient who replies STOP to texts from a phone number stops getting SMS from that number. Their patient record shows `sms_opted_out: true`, and sending them `channel: sms` returns `422 patient_opted_out`. Secure messages by email still reach them. When they reply START, texts resume.

## Errors to handle

| Status | Code | What to do |
| --- | --- | --- |
| 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`. |
| 422 | `from_required` | Pass a `from` number from [List phone numbers](https://www.bloomtext.com/developers/api/reference/list-phone-numbers/). |
| 422 | `no_contact_method` | The patient has no phone number for `sms`, or neither phone nor email for `secure`. [Update the patient](https://www.bloomtext.com/developers/api/reference/update-patient/). |
| 422 | `patient_opted_out` | Send with `channel: secure` so it goes by email, or wait for the patient to reply START. |

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](https://www.bloomtext.com/developers/api/idempotency/).

## Sending to many patients

Every send names one patient; there's no "send to everyone" call. To send the same message to many patients, use a [campaign](https://www.bloomtext.com/developers/api/campaigns/): add the recipients, start it, and BloomText sends to each one in turn and tracks delivery. For a different message per patient, loop over this endpoint within your [rate limits](https://www.bloomtext.com/developers/api/rate-limits/), and queue the sends rather than firing hundreds at once.
