# Send a message to a patient

> Sends a message to a patient by patient_id or phone number. With OAuth it's sent as the connected admin, and their conversation is marked as read; with an API key it's sent as the key, like "Reminder service". BloomText finds the patient (or creates one for a new phone number), finds or starts the conversation on the phone number, and sends. Messages go by secure link unless channel is sms.

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

`POST /patient-messages`

Sends a message to a patient by `patient_id` or phone number. With OAuth it's sent as the connected admin, and their conversation is marked as read; with an API key it's sent as the key, like "Reminder service". BloomText finds the patient (or creates one for a new phone number), finds or starts the conversation on the phone number, and sends. Messages go by secure link unless `channel` is `sms`.

**Credentials:** [API key](https://www.bloomtext.com/developers/api/api-keys/) or an [org admin’s OAuth token](https://www.bloomtext.com/developers/api/oauth/) · **Scope:** `messages.write` · **Idempotent:** With `Idempotency-Key`

### Headers

- `Idempotency-Key` (UUID, required): A UUID you generate for each write. Retrying with the same key returns the original result for 24 hours. Reusing a key with a different method, path, or body returns 409.

### Body parameters

A message to a patient.

- `to` (Recipient, required): Who to message. Provide exactly one of patient_id or phone.

  Child attributes:
- `patient_id` (UUID): The patient to message.

- `phone` (string): A mobile number. BloomText finds the patient with this number, or creates one.

- `patient` (PatientDetails): Details for a patient BloomText creates while sending. Ignored when the phone number matches an existing patient.

  Child attributes:
- `first_name` (string): First name.

- `last_name` (string): Last name.

- `date_of_birth` (date): Date of birth.

- `email` (string): Email address.

- `mrn` (string): The patient's ID in your system.

- `body` (string, required): Message text, up to 1,600 characters.

- `channel` (secure, sms): `secure` (the default) sends a text or email with a secure link, and the message stays in BloomText. `sms` sends the text itself as a regular SMS.

- `from` (string): The clinic phone number to send from, in E.164 format, like +15125550100. Defaults to the default number: the one set on the API key, or the admin's own. See List phone numbers.

### Returns

Returns `201 Created` with the sent [Message object](https://www.bloomtext.com/developers/api/reference/message-object/) in `message`, plus `patient_created` and `conversation_created` to say whether BloomText created either one.

### Errors

Failed requests return `application/problem+json` [problem details](https://www.bloomtext.com/developers/api/errors/). The errors specific to this endpoint:

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | The request is malformed, such as invalid JSON. |
| 401 | `invalid_token` | The API key or access token is missing, expired, or revoked. |
| 403 | `insufficient_scope` | The credential doesn’t have the scope this endpoint needs. |
| 404 | `not_found` | The `patient_id` doesn't exist in the organization, or `from` isn't one of its phone numbers. |
| 409 | `idempotency_key_reused` | The Idempotency-Key was already used with a different request. |
| 409 | `idempotency_request_in_progress` | A request with this Idempotency-Key is still running. Retry shortly. |
| 409 | `ambiguous_recipient` | The phone number belongs to more than one patient. `candidates` lists them; send again with a `patient_id`, or add `patient.mrn`. |
| 422 | `validation_failed` | A field failed validation. See `field_errors`. |
| 422 | `from_required` | There are several phone numbers and no default. Pass `from`; see List phone numbers. |
| 422 | `no_contact_method` | The patient has no phone number for `sms`, or no phone or email for `secure`. |
| 422 | `patient_opted_out` | The patient replied STOP to texts from this number. Send with `secure` by email, or wait for them to reply START. |
| 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. |

```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="Request"
const response = await fetch('https://api.bloomtext.com/v1/patient-messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"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."}),
})

if (!response.ok) throw new Error(`BloomText API error: ${response.status}`)
const data = await response.json()
```

```python filename="Request"
import os
import uuid

import requests

response = requests.post(
    "https://api.bloomtext.com/v1/patient-messages",
    headers={
        "Authorization": f"Bearer {os.environ['BLOOMTEXT_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={"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."},
)
response.raise_for_status()
data = response.json()
```

```go filename="Request"
package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/google/uuid"
)

func main() {
	body := bytes.NewBufferString(`{"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."}`)
	req, err := http.NewRequest("POST", "https://api.bloomtext.com/v1/patient-messages", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY"))
	req.Header.Set("Idempotency-Key", uuid.NewString())
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	out, _ := io.ReadAll(res.Body)
	fmt.Println(res.Status, string(out))
}
```

```ruby filename="Request"
require "net/http"
require "json"
require "securerandom"

uri = URI("https://api.bloomtext.com/v1/patient-messages")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}"
request["Idempotency-Key"] = SecureRandom.uuid
request["Content-Type"] = "application/json"
request.body = { 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." }.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end
data = JSON.parse(response.body)
```

```json filename="Response · 201 Created"
{
  "message": {
    "id": "4cbfdb50-6b7d-45d4-94b3-05a52ce3f4d1",
    "conversation": {
      "id": "e5c3b7b8-8f08-4d5a-9af1-0d11b0f4b7a0",
      "title": "Jane Doe"
    },
    "sender": {
      "id": "b7e1c3a9-4d2f-4a8b-9c6e-0f1d2e3a4b5c",
      "name": "Reminder service",
      "kind": "api_key"
    },
    "patient": {
      "id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77",
      "name": "Jane Doe"
    },
    "channel": "secure",
    "phone_number": null,
    "delivery": {
      "status": "queued",
      "error": null,
      "updated_at": "2026-09-21T15:04:05Z"
    },
    "campaign_id": null,
    "created_at": "2026-09-21T15:04:05Z",
    "edited_at": null,
    "type": "text",
    "body": "Hi Jane, Dr. Example moved your appointment to Friday at 9:00. Reply here with any questions.",
    "file": null,
    "reply_to_message_id": null,
    "unread": false,
    "sent_via": null
  },
  "patient_created": false,
  "conversation_created": false
}
```
