# Check a broadcast before sending it

> Answers what POST /v1/broadcasts would do with the same body, without sending anything: how many people it reaches, who is skipped, the first five texts filled in, and when the last one should go.

`POST /v1/broadcasts/preview`

Answers what `POST /v1/broadcasts` would do with the same body, without sending anything: how many people it reaches, who is skipped, the first five texts filled in, and when the last one should go.

Needs a key with the `messages:read` scope. [Keys and scopes](https://usebluebot.com/docs/concepts/api-keys)

Send an `Idempotency-Key` so a retry never acts twice. [Idempotency](https://usebluebot.com/docs/concepts/idempotency)

## Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Any string of up to 255 characters, new for each request. A retry with the same key gets the first answer back for 24 hours, with `Idempotent-Replayed: true`. Up to 255 characters |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | No | The words. Up to 2,000 characters |
| `template_id` | string | No | A saved template to use instead of `text`. Starts with `tpl_` |
| `tags` | array of strings | No | Up to 20 items |
| `contact_ids` | array of strings | No | Up to 500 items |
| `recipients` | array of object | No | Up to 500 items |
| `recipients[].to` | string | Yes | A phone number (US numbers may leave off +1; `(555) 555-0123` works) or an email address an Apple ID uses. 3 to 320 characters |
| `recipients[].first_name` | string | No | Up to 100 characters |
| `recipients[].last_name` | string | No | Up to 100 characters |
| `recipients[].variables` | map of string | No | Values for this person’s `{{variables}}`. |
| `send_at` | timestamp | No |   |

## Example

```bash
curl -X POST "https://usebluebot.com/api/v1/broadcasts/preview" \
  -H "Authorization: Bearer $BLUEBOT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70" \
  -d '{
    "text": "Hi {{first_name|there}}, open house at 412 Oak St this Saturday 1–4.",
    "tags": [
      "buyer"
    ]
  }'
```

```ts
import BlueBot from '@usebluebot/sdk';

const client = new BlueBot({ apiKey: process.env.BLUEBOT_API_KEY });

const preview = await client.broadcasts.preview({
  text: 'Hi {{first_name|there}}, open house at 412 Oak St this Saturday 1–4.',
  tags: ['buyer'],
}, { idempotencyKey: '0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70' });
console.log(preview);
```

```python
import os
from bluebot import BlueBot

client = BlueBot(api_key=os.environ["BLUEBOT_API_KEY"])

preview = client.broadcasts.preview(
    text="Hi {{first_name|there}}, open house at 412 Oak St this Saturday 1–4.",
    tags=["buyer"],
    idempotency_key="0f6d8c2e-5b1a-4c7e-9d3f-2a8b6e1c4d70",
)
print(preview)
```

## Response

`200`: What it would do.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | "broadcast_preview" | Yes |   |
| `recipient_count` | integer | Yes | Everyone it would go to, counting people on your stop list. |
| `stopped_count` | integer | Yes | People on your stop list, who won’t get it. |
| `new_people_count` | integer | Yes | People you have never texted, who count toward your daily limit for new people. |
| `duplicate_count` | integer | Yes | Rows dropped because the same person was already in the list. |
| `samples` | array of object | Yes | The first five people’s texts. |
| `samples[].handle` | string | Yes | A phone number in E.164 (`+15555550123`) or an email address an Apple ID uses (iMessage only). |
| `samples[].name` | string or null | Yes |   |
| `samples[].text` | string | Yes | Their text, filled in. |
| `samples[].missing` | array of strings | Yes | Variables they had no value for, which were left empty. |
| `estimate` | BroadcastEstimate or null | Yes | When the broadcast’s texts should go, worked out with your sending rules. |
| `estimate.first_at` | timestamp or null | Yes | When the first text should go. |
| `estimate.last_at` | timestamp or null | Yes | When the last text should go, given your pace, quiet hours and daily limits. |
| `estimate.days` | integer | Yes | How many days the sending spans in your time zone. |
| `estimate.waits_for` | array of strings | Yes | Holds the texts will meet on the way: `quiet_hours`, `daily_new_people_limit`, `platform_daily_limit`. |
| `estimate.blocked_by` | string or null | Yes | A hold with no end that stops the broadcast from going, such as `plan_lapsed`. |
| `estimate.mac_online` | boolean | Yes | Whether your Mac is online now. The estimate assumes it stays on. |

The answer, `200`:

```json
{
  "object": "broadcast_preview",
  "recipient_count": 42,
  "stopped_count": 1,
  "new_people_count": 3,
  "duplicate_count": 0,
  "samples": [
    {
      "handle": "+15555550123",
      "name": "Maria Alvarez",
      "text": "Hi Maria, open house at 412 Oak St this Saturday 1–4.",
      "missing": []
    }
  ],
  "estimate": {
    "first_at": "2026-11-14T15:00:00.000Z",
    "last_at": "2026-11-14T15:14:20.000Z",
    "days": 1,
    "waits_for": [],
    "blocked_by": null,
    "mac_online": true
  }
}
```

## Errors

Every refusal has the same envelope and a page of its own. These are the ones this operation can give.

| Code                                                    | Status | What it means                                                                                                                                    |
| ------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`unauthorized`](https://usebluebot.com/docs/errors/unauthorized)             | 401    | The request carried no key, or a key BlueBot doesn’t know: mistyped, revoked, or a test key sent to the live API.                                |
| [`insufficient_scope`](https://usebluebot.com/docs/errors/insufficient_scope) | 403    | The key doesn’t carry the scope the endpoint needs. Each endpoint’s reference names its scope.                                                   |
| [`rate_limited`](https://usebluebot.com/docs/errors/rate_limited)             | 429    | Too many requests. A key may make 120 requests a minute. The `RateLimit` header says how many are left, and `Retry-After` how long to wait.      |
| [`internal`](https://usebluebot.com/docs/errors/internal)                     | 500    | Something went wrong on BlueBot’s side. It was reported. Send the request again; with an Idempotency-Key that is always safe.                    |
| [`invalid_request`](https://usebluebot.com/docs/errors/invalid_request)       | 400    | Something in the request doesn’t fit: a missing or malformed field, a bad id, or a combination the endpoint refuses. `details` names each field. |
| [`not_found`](https://usebluebot.com/docs/errors/not_found)                   | 404    | There is no such thing on this account. Ids are prefixed by kind, so a message id starts with `msg_`.                                            |
| [`limit_reached`](https://usebluebot.com/docs/errors/limit_reached)           | 409    | The account has as many of something as it may have.                                                                                             |

## Try it
