# Rehearse a broadcast (who would actually receive it)

`POST /v1/broadcasts/{id}/rehearse`

- Authentication: required (Bearer token)
- Required scope: `api.broadcasts.view`

Runs the send's own audience resolution and consent filter over the whole
audience and reports how many contacts would actually **receive** the
broadcast, and why the rest would not. **Nothing is sent and nothing is
written** — no audience snapshot, no send-log row, no change to the
broadcast — so a rehearsal is repeatable and costs a caller nothing but
the reads.

This is the number `POST /v1/broadcasts/{id}/preview` does not give you.
Preview reports the **targeted** population and applies no consent at
all; this reports the **eligible** population after suppression,
account-wide unsubscribes and topic consent are applied. Both numbers are
returned here, and `targeted_basis` says how `targeted` was counted:
`membership` is the materialized membership fold the send itself reads,
which can differ from preview's `audience.total` because preview
recompiles a segment's predicate live. The two disagree while a segment
is waiting to be re-evaluated, and that disagreement is information, not
an error.

`filtered` breaks the drops down by the same four reasons the
per-recipient results log records after a send, so a rehearsal and a
results page are readable against each other. `sample_filtered` carries
up to 25 of the dropped recipients by **contact id only** — no email
addresses, so this endpoint is not an audience export.

**The no-topic gate is reported, not raised.** A marketing broadcast with
no topic, not acknowledged as topic-less, is refused by the consent
filter at send time. Rehearsing one returns `no_topic_ack_required: true`
with `eligible: null` (and the account-tier counts still filled in),
rather than a 422 — the point of a rehearsal is to show that refusal
coming.

`template_ready` is false when the broadcast's template is not deployed
to SES. An eligible count over an undeployed template is a reach nobody
can be sent.

Rate limited per broadcast (one rehearsal per 10 seconds by default);
`429 rate_limited` carries `Retry-After`. Returns `422
validation_failed` when the audience cannot be resolved.

## Path parameters

- `id` (string<uuid>, required) — Resource UUID. An unparseable id reads as a clean `404 not_found`.

## Example request

```bash
curl -X POST 'https://api.sendops.dev/v1/broadcasts/00000000-0000-0000-0000-000000000000/rehearse' \
  -H "Authorization: Bearer $SENDOPS_API_KEY"
```

## Responses

### 200 — The rehearsal result

Content type: `application/json`

```json
{
  "broadcast_id": "00000000-0000-0000-0000-000000000000",
  "targeted": 0,
  "targeted_basis": "membership",
  "eligible": 0,
  "filtered": {
    "suppressed": 0,
    "unsubscribed_all": 0,
    "opted_out": 0,
    "not_subscribed": 0
  },
  "no_topic_ack_required": true,
  "sample_filtered": [
    {
      "contact_id": "00000000-0000-0000-0000-000000000000",
      "tier": "account",
      "reason": "suppressed"
    }
  ],
  "template_ready": true,
  "requires": [
    {
      "kind": "template",
      "key": "string",
      "status": "ok",
      "detail": "string",
      "fix": "string"
    }
  ]
}
```

### 401 — Missing, malformed, or unknown API key

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 403 — Key lacks the required scope or plan limit violated

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 404 — Resource not found

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 422 — A query parameter, path value or body field failed validation.

The body is a `validation_failed` Problem. When the refusal is about a
reference — a segment key, template slug, topic or attribute name the
organization does not have — it additionally carries `validation_code`,
`field`, `line`/`column`, `missing`, `candidates` and `next_step`, so a
client can correct the call without a second round of guessing. See
`ValidationProblem`.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ],
  "validation_code": "invalid_syntax",
  "field": "string",
  "line": 0,
  "column": 0,
  "missing": [
    {
      "kind": "segment",
      "key": "string"
    }
  ],
  "candidates": {},
  "next_step": "string"
}
```

### 429 — Per-org rate limit exceeded

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```

### 500 — Unexpected server-side failure. The `code` is `internal_error`. The
`request_id` field can be quoted to SendOps support to investigate.

Content type: `application/problem+json`

```json
{
  "type": "https://example.com",
  "title": "string",
  "status": 0,
  "detail": "string",
  "code": "invalid_key",
  "request_id": "string",
  "retry_after": 0,
  "retention_days": 0,
  "scope": "string",
  "resource": "string",
  "errors": [
    {
      "field": "string",
      "reason": "string"
    }
  ],
  "attribute_id": "00000000-0000-0000-0000-000000000000",
  "content_hash": "string",
  "differs": [
    "string"
  ]
}
```
