# Send messages to patients in a batch

> Sends up to 60 messages in one request. Each item is the body of a single send and is checked the same way: recipient, channel, phone number, and opt-out. The batch counts as one request toward the per-second and per-minute request limits, and each message counts toward the per-minute message limit. All of an organization's API keys share these limits. One Idempotency-Key covers the whole batch: retry with the same key and the same body to get the same results without sending anything twice.

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

`POST /messages:batch`

Sends up to 60 messages in one request. Each item is the body of a single send and is checked the same way: recipient, channel, phone number, and opt-out. The batch counts as one request toward the per-second and per-minute request limits, and each message counts toward the per-minute message limit. All of an organization's API keys share these limits. One `Idempotency-Key` covers the whole batch: retry with the same key and the same body to get the same results without sending anything twice.

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

Messages to send in one request.

- `messages` (array of MessageSend, required): Up to 60 messages, each the body of a single send.

  Child attributes:
- `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.

- `body` (string, required): Message text: up to 1,600 characters for `sms`, or 40,000 for `secure`. A longer one returns `422 too_long`.

- `channel` (secure, sms, required): Required; there's no default. `sms` sends the text itself as a regular SMS from the clinic's number. `secure` sends a text or email with a secure link, and the message stays in BloomText.

- `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.

Any other field returns `422 unknown_field`. See [Unknown fields are rejected](https://www.bloomtext.com/developers/api/errors/#unknown-fields-are-rejected).

### Returns

Returns `207 Multi-Status` with one result per message, in the order you sent them: an `id` and `status: queued`, or an `error` with a problem details object. One message's error never fails the others.

### 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 or malformed, or the access token has expired or been revoked. |
| 401 | `key_revoked` | An org admin revoked the API key. |
| 401 | `key_expired` | The API key’s expiry has passed. |
| 403 | `insufficient_scope` | The credential doesn’t have the scope this endpoint needs. |
| 403 | `plan_required` | The organization's plan doesn't include patient messaging. The whole batch is refused and nothing is sent. |
| 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. |
| 422 | `validation_failed` | `messages` is empty or holds more than 60 messages. Errors in individual messages come back in their results instead. |
| 422 | `unknown_field` | A message has a field the API doesn't take. The whole batch is refused, naming the message (`/messages/3/...`), and nothing is sent. |
| 429 | `rate_limited` | The batch's messages don't fit in what's left of the organization's per-minute message limit. Nothing was sent; retry with the same `Idempotency-Key` after `Retry-After`. |
| 429 | `rate_limited` | Too many requests. Retry after `Retry-After` seconds. |

```bash filename="Request"
curl -X POST "https://api.bloomtext.com/v1/messages:batch" \
  -H "Authorization: Bearer $BLOOMTEXT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

```js filename="Request"
const response = await fetch('https://api.bloomtext.com/v1/messages:batch', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BLOOMTEXT_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(),
  },
})

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

```go filename="Request"
package main

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

	"github.com/google/uuid"
)

func main() {
	req, err := http.NewRequest("POST", "https://api.bloomtext.com/v1/messages:batch", nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("BLOOMTEXT_API_KEY"))
	req.Header.Set("Idempotency-Key", uuid.NewString())

	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/messages:batch")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("BLOOMTEXT_API_KEY")}"
request["Idempotency-Key"] = SecureRandom.uuid

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 · 207 undefined"
{
  "results": [
    {
      "index": 0,
      "id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55",
      "status": "queued",
      "error": {
        "type": "string",
        "title": "string",
        "status": 400,
        "code": "string",
        "detail": "string",
        "request_id": "8b9c2d2f-6b1a-44f4-a7b1-0d6d3f2d6f55",
        "field_errors": [
          {
            "path": "string",
            "code": "string",
            "message": "string"
          }
        ],
        "candidates": [
          {
            "id": "5a1c9e2b-7d44-4f1a-9c3e-2b8d6f0a1e77",
            "name": "Jane Doe"
          }
        ],
        "numbers": [
          {
            "number": "+15125550100",
            "team": "Front desk"
          }
        ],
        "number": "string",
        "since": "2026-09-21T15:04:05Z",
        "length": 0,
        "max": 0
      }
    }
  ]
}
```
