# Campaigns

One message to a segment. The dashboard calls it a campaign; the API calls the same thing a broadcast, at `/broadcasts`, with the `broadcasts:read` and `broadcasts:write` scopes. A broadcast is a draft until it is sent, passes a compliance gate on the way out, and becomes one ordinary email per recipient — each with its own events.

## Lifecycle

`draft` → `scheduled` or `queued` → `sending` → `sent`. A cancel from `scheduled` or `queued` ends in `canceled`; a cancel while `sending` stops at the next page of recipients, and the sends already created go out. Only a draft can be updated or deleted.

## The gate

A send is refused — and creates nothing — unless all of these hold, checked in this order. The first three and a missing body answer `422`; paused marketing mail and a team that cannot send answer `403`. `GET /broadcasts/{id}/checklist` runs the same checks without sending.

| Needs | Meaning |
| --- | --- |
| A verified sending domain | `from` is on a domain of yours verified for sending. |
| A postal address | Set once under Settings → Sender. It goes in the footer we add to a body with no unsubscribe link. |
| A segment | `segment_id` names a live segment. |
| Marketing sends switched on | Marketing mail can be paused platform-wide; while it is, every campaign send is refused. |
| A team in good standing | A team that cannot send transactional mail cannot send a campaign either. |
| A body | `html`, `text`, or a published template. |

## Recipients

- The segment's members, minus anyone globally unsubscribed, opted out of the broadcast's topic, on the suppression list, or deleted. Each exclusion is recorded with its reason.
- There is no single results endpoint. `GET /broadcasts/{id}/recipients` lists who was sent, delivered, opened, clicked, bounced, complained, unsubscribed or suppressed, by `type`; `GET /broadcasts/{id}/clicked-links` counts clicks; and `GET /emails/metrics` with the broadcast as a filter gives the totals.
- Every recipient is an ordinary send: it counts toward your marketing allowance (never the transactional quota), carries `List-Unsubscribe` headers for one-click unsubscribe, and raises the same `email.*` events a transactional send does.

## Merge variables

Bodies and the subject may carry `{{{KEY}}}` or `{{{KEY|default}}}`. A missing value takes the inline default, then the property's `fallback_value`, then the empty string. Values are HTML-escaped in `html` and written as-is in `text` and `subject`.

| Variable | Value |
| --- | --- |
| `FIRST_NAME` | The contact's first name. |
| `LAST_NAME` | The contact's last name. |
| `EMAIL` | The contact's address. |
| `<PROPERTY_KEY>` | Any declared contact property, upper- or lower-case as declared. |
| `UNSUBSCRIBE_URL` | A one-click global unsubscribe link for this recipient. |
| `PREFERENCES_URL` | The hosted preference page for this recipient. |

A body with neither `{{{UNSUBSCRIBE_URL}}}` nor `{{{PREFERENCES_URL}}}` gets a plain footer appended — an unsubscribe link and your postal address — so no broadcast leaves without both.

## Endpoints

### `POST /broadcasts`

A draft, or — with `send: true` — a broadcast queued straight through the gate.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | A label for the dashboard. Not shown to recipients. |
| `segment_id` | string | The segment the broadcast goes to. |
| `from` (required) | string | `Name <address>` on one of your verified domains. |
| `subject` (required) | string | The subject line. Merge variables are allowed. |
| `reply_to` | string[] | Where replies go. |
| `html` | string | The HTML body. At least one of `html` and `text` before sending. |
| `text` | string | The plain-text body. |
| `send` | boolean | Send now (or at `scheduled_at`) instead of leaving a draft. Defaults to `false`. |
| `scheduled_at` | string | ISO 8601 or a phrase such as "in 2 hours", read as UTC. Between one minute and thirty days out. Only with `send: true`. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `audience_id` | string | Deprecated alias of `segment_id`, accepted for compatibility. Use `segment_id`. |
| `preview_text` | string | The inbox preview line. |
| `topic_id` | string | Scope the broadcast to a topic: only contacts opted in to it receive it. |
| `template` | object | `{ id }` of a published template to copy the content from, instead of `html` and `text`. |

Create a broadcast:

```sh
curl -X POST "https://api.rasket.com/broadcasts" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "September product update",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "from": "Acme <news@send.acme.example>",
  "subject": "What shipped in September",
  "reply_to": ["hello@acme.example"],
  "html": "<p>Hi {{{FIRST_NAME|there}}},</p><p>Here is what shipped in September.</p><p><a href=\"{{{UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>"
}'
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "September product update",
    segment_id: "78261eea-8f8b-4381-83c6-79fa7120f1cf",
    from: "Acme <news@send.acme.example>",
    subject: "What shipped in September",
    reply_to: ["hello@acme.example"],
    html: "<p>Hi {{{FIRST_NAME|there}}},</p><p>Here is what shipped in September.</p><p><a href=\"{{{UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/broadcasts",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "September product update",
    "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
    "from": "Acme <news@send.acme.example>",
    "subject": "What shipped in September",
    "reply_to": ["hello@acme.example"],
    "html": "<p>Hi {{{FIRST_NAME|there}}},</p><p>Here is what shipped in September.</p><p><a href=\"{{{UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>"
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "broadcast",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}
```

- `from` and `subject` are required; everything else can wait for an update. Sending needs a segment and a body.
- Bodies may carry `{{{KEY}}}` or `{{{KEY|default}}}` over `FIRST_NAME`, `LAST_NAME`, `EMAIL`, every contact property key, `UNSUBSCRIBE_URL` and `PREFERENCES_URL`, rendered per recipient.
- A body with neither `{{{UNSUBSCRIBE_URL}}}` nor `{{{PREFERENCES_URL}}}` gets a footer with an unsubscribe link and your postal address appended, so no broadcast leaves without one.
- With `send: true`, a gate refusal creates nothing.

### `GET /broadcasts`

Every broadcast, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `search` | string | Only broadcasts whose name or subject contains this, ignoring case. At most 200 characters. |
| `status` | string | Only broadcasts in this status: `draft`, `scheduled`, `queued`, `sending`, `sent`, `canceled` or `failed`. |

List broadcasts:

```sh
curl -X GET "https://api.rasket.com/broadcasts" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/broadcasts",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
      "name": "September product update",
      "audience_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
      "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
      "status": "sent",
      "created_at": "2026-09-08T22:22:17.595Z",
      "sent_at": "2026-09-09T09:00:04.118Z"
    }
  ]
}
```

- `scheduled_at` and `sent_at` are absent until they are set, never `null`. Deleted drafts are not listed.

### `GET /broadcasts/{id}`

One broadcast, with its content and status.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

Retrieve a broadcast:

```sh
curl -X GET "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "name": "September product update",
  "audience_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "from": "Acme <news@send.acme.example>",
  "subject": "What shipped in September",
  "reply_to": ["hello@acme.example"],
  "preview_text": "Three new features and a faster editor.",
  "status": "draft",
  "created_at": "2026-09-08T22:22:17.595Z",
  "html": "<p>Hi {{{FIRST_NAME|there}}},</p><p>Here is what shipped in September.</p><p><a href=\"{{{UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>",
  "text": null,
  "topic_id": null
}
```

- `status` is one of `draft`, `scheduled`, `queued`, `sending`, `sent`, `canceled` or `failed`.

### `PATCH /broadcasts/{id}`

Change a draft. Only the fields present are touched.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | A label for the dashboard. Not shown to recipients. |
| `segment_id` | string | The segment the broadcast goes to. |
| `from` | string | `Name <address>` on one of your verified domains. |
| `subject` | string | The subject line. Merge variables are allowed. |
| `reply_to` | string[] | Where replies go. |
| `html` | string | The HTML body. At least one of `html` and `text` before sending. |
| `text` | string | The plain-text body. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `audience_id` | string | Deprecated alias of `segment_id`, accepted for compatibility. Use `segment_id`. |
| `preview_text` | string | The inbox preview line. |
| `topic_id` | string | Scope the broadcast to a topic: only contacts opted in to it receive it. |

Update a broadcast:

```sh
curl -X PATCH "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "subject": "What shipped in September — and what is next"
}'
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subject: "What shipped in September — and what is next"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "subject": "What shipped in September — and what is next"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "broadcast",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}
```

- A broadcast past `draft` is `400 validation_error`; cancel it first if it has not started sending.

### `DELETE /broadcasts/{id}`

Remove a draft.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

Delete a broadcast:

```sh
curl -X DELETE "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "broadcast",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "deleted": true
}
```

- Drafts only. A broadcast that was sent keeps its recipients and metrics; one that was scheduled must be canceled instead.

### `POST /broadcasts/{id}/send`

Queue a draft now, or schedule it.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `scheduled_at` | string | ISO 8601 or a phrase such as "in 2 hours", read as UTC. Between one minute and thirty days out. Omit it — or send no body at all — to send now. |

Send a broadcast:

```sh
curl -X POST "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/send" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduled_at": "2026-09-12T09:00:00Z"
}'
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    scheduled_at: "2026-09-12T09:00:00Z"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/send",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "scheduled_at": "2026-09-12T09:00:00Z"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}
```

- The gate refuses with `422` unless: `from` is on a domain verified for sending; the team has a postal address (Settings → Sender); the segment resolves; and the broadcast has a body. A team that cannot send, or marketing sends paused platform-wide, is `403`.
- Recipients are the segment's members, minus anyone globally unsubscribed, opted out of the broadcast's topic, suppressed or deleted. Each is an ordinary send with `email.*` events of its own.
- The response is `{ id }` alone, as the reference document has it.

### `POST /broadcasts/{id}/cancel`

Stop a scheduled or queued broadcast.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

Cancel a broadcast:

```sh
curl -X POST "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/cancel" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/cancel",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "broadcast",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}
```

- A broadcast mid-send stops at its next page of five hundred recipients; sends already created go out. Only `scheduled` and `queued` (or `sending`) broadcasts can be canceled.

### `GET /broadcasts/{id}/recipients`

Who was sent, delivered, opened, clicked, bounced, complained, unsubscribed or suppressed.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `type` (required) | string | `sent`, `delivered`, `opened`, `clicked`, `bounced`, `complained`, `unsubscribed` or `suppressed`. |
| `email` | string | Only recipients whose address contains this. |
| `bounce_type` | string | `permanent`, `transient` or `undetermined`. Only with `type=bounced`. |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List a broadcast's recipients:

```sh
curl -X GET "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/recipients?type=clicked&limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/recipients?type=clicked&limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/recipients?type=clicked&limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "cur_01k4xq8p9m",
      "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
      "email": "ronald.williams@example.com",
      "count": 2,
      "clicked_links": [
        {
          "url": "https://acme.example/changelog",
          "clicks": 2
        }
      ]
    }
  ]
}
```

- `id` is an opaque cursor for paging, not an entity id. `count` appears for `opened` and `clicked`, `bounce_type` for `bounced`, `clicked_links` for `clicked`.
- `contact_id` is `null` for a contact deleted since the send.

### `GET /broadcasts/{id}/clicked-links`

Every URL clicked, with total and unique clicks.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List a broadcast's clicked links:

```sh
curl -X GET "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/clicked-links" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/clicked-links", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/clicked-links",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "cur_01k4xq9r2d",
      "url": "https://acme.example/changelog",
      "clicks": 318,
      "unique_clicks": 241
    }
  ]
}
```

- Requires click tracking on the sending domain; without it there is nothing to count.

### `GET /broadcasts/{id}/checklist`

Every condition of the send gate, passed or not, in the order a send checks them.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

Check a broadcast:

```sh
curl -X GET "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/checklist" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/checklist", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/checklist",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "broadcast_compliance",
  "broadcast_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "sendable": false,
  "conditions": [
    {
      "condition": "sending_domain",
      "passed": true
    },
    {
      "condition": "postal_address",
      "passed": false,
      "name": "validation_error",
      "message": "A postal address is required before sending a broadcast."
    },
    {
      "condition": "segment",
      "passed": true
    },
    {
      "condition": "marketing_enabled",
      "passed": true
    },
    {
      "condition": "team_can_send",
      "passed": true
    },
    {
      "condition": "body",
      "passed": true
    }
  ]
}
```

- A failed condition carries the `name` and `message` a send would answer with.
- It is a snapshot: `POST /broadcasts/{id}/send` runs the same gate again when you call it.

### `POST /broadcasts/{id}/duplicate`

A new draft with the same content, named `&lt;name> (copy)`.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

Duplicate a broadcast:

```sh
curl -X POST "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/duplicate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/duplicate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/duplicate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "broadcast",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17"
}
```

- The segment must still exist, or the copy is refused with `422 invalid_parameter`.

### `POST /broadcasts/{id}/test`

Send the draft to up to five inboxes.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The broadcast's ID. |

#### Headers, less common

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | Retries with the same key send the test once. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `to` (required) | string[] | One to five addresses. |

Send a test of a broadcast:

```sh
curl -X POST "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/test" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "to": ["you@acme.example"]
}'
```

```ts
const response = await fetch("https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: ["you@acme.example"]
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/test",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "to": ["you@acme.example"]
  },
)

print(response.json())
```

#### Response `202`

```json
{
  "object": "broadcast_test",
  "broadcast_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "data": [
    {
      "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
      "to": "you@acme.example"
    }
  ]
}
```

- Each test counts as a send. Variables use their defaults, and unsubscribe links are placeholders.
- Draft only (`409 resource_locked`). The token also needs `emails:send`.
