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

## Before you send

After the gate, a send runs the content checks over the saved draft. `POST /broadcasts/{id}/checks` runs them without sending, and the dashboard's preview shows them beside the email.

| Check | What it looks for |
| --- | --- |
| Variables | Fails on a `{{{KEY}}}` that is no contact field or property and has no default: it would be sent empty. |
| Links | Fails on a link that is empty, just #, or not a full web address. |
| Link types | Fails on a link that runs code. Warns on one that is http rather than https. |
| Subject | Fails when there is none. Warns past 60 characters. |
| Preview text | Warns when there is none, or past 150 characters. |
| Alt text | Warns on an image with no alt text. |
| Image weight | Warns past 1 MB of images you uploaded to Rasket. |
| Text and images | Warns when the email is mostly pictures. |
| Unsubscribe | Warns when the body has no unsubscribe link of its own. We add one anyway. |

Each check is `pass`, `warn` or `fail`. A `fail` stops the send with `422 validation_error` and the failing checks in `details.checks`. Send again with `acknowledge_checks: true` to send anyway. A warning never stops anything.

Links are read as written. We never visit them, and no request leaves for them.

## 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 plan's daily and monthly email volume like any other send, 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 contact property, upper- or lower-case as its key is written. |
| `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.

A body copied from a visual template keeps its show-when rules, and each recipient gets only the blocks their rules allow. A campaign has no trigger event, so a template's repeated items and event details stay empty here.

## 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. |
| `acknowledge_checks` | boolean | Send even though a content check fails. Without it, a failing check is `422 validation_error` with the failing checks in `details.checks`. |

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 of your own verified for sending (not your team address, which reaches only your team and is for drafts and tests); 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`.
- Then the content checks run (`POST /broadcasts/{id}/checks`). A failing one — a link that goes nowhere, a merge variable that would be sent empty, no subject — is `422 validation_error` with the failing checks in `details.checks`, unless `acknowledge_checks` is `true`.
- 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}/checks`

The pre-send checks over the saved draft. Nothing is changed and no link is visited.

#### Path parameters

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

Check a broadcast's content:

```sh
curl -X POST "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/checks" \
  -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/checks", {
  method: "POST",
  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.post(
    "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/checks",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "email_checks",
  "checks": [
    {
      "code": "unresolved_variable",
      "status": "fail",
      "title": "Variables",
      "message": "{{{FIRSTNAME}}} is not a contact field or property, so it would be sent empty. Check the spelling, or add a default: {{{FIRSTNAME|friend}}}."
    },
    {
      "code": "preheader",
      "status": "warn",
      "title": "Preview text",
      "message": "There is no preview text, so inboxes show the first words of the email beside the subject."
    },
    {
      "code": "subject",
      "status": "pass",
      "title": "Subject",
      "message": "24 characters."
    }
  ]
}
```

- Each check is `pass`, `warn` or `fail`, failures first. Only a `fail` stops a send, and `acknowledge_checks` sends anyway.
- Links are read as written, never visited. Picture weight counts the images you uploaded to Rasket.

### `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}/save-as-template`

A new published template with the broadcast's subject, HTML and text.

#### Path parameters

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

Save a broadcast as a template:

```sh
curl -X POST "https://api.rasket.com/broadcasts/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/save-as-template" \
  -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/save-as-template", {
  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/save-as-template",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

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

#### Response `201`

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

- Works on a broadcast of any status. Each call makes a new template.
- The template is named after the broadcast, or its subject if it has no name.
- `{{{MERGE}}}` variables are kept. They fill when you send the template as a broadcast, not as a single email.
- Your key needs to write templates and read broadcasts. A broadcast with no HTML is `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`.
