> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tented.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Managing Email Blasts

> Create a blast, attach an audience and an approved email, then schedule or send it through the public API.

## Endpoints

```bash theme={null}
GET    /v1/email-blasts
POST   /v1/email-blasts
GET    /v1/email-blasts/{blastId}
PATCH  /v1/email-blasts/{blastId}
DELETE /v1/email-blasts/{blastId}
PUT    /v1/email-blasts/{blastId}/audience
PUT    /v1/email-blasts/{blastId}/email
POST   /v1/email-blasts/{blastId}/schedule
POST   /v1/email-blasts/{blastId}/send-now
POST   /v1/email-blasts/{blastId}/unschedule
POST   /v1/email-blasts/{blastId}/archive
GET    /v1/email-blasts/{blastId}/contacts
POST   /v1/email-blasts/{blastId}/contacts/export
GET    /v1/email-blasts/{blastId}/contacts/export/{exportId}
```

A blast is a one-time send of an email to an audience. The typical workflow is:

1. Create a draft blast
2. Attach an audience with `PUT .../audience`
3. Attach an email with `PUT .../email`
4. Schedule it or send it now

Blast `status` moves through `draft` → `scheduled` → `sending` → `sent`, with `failed`, `cancelled`, and `archived` as side exits. The audience and email are only editable while the blast is a `draft`.

## Create a Blast

```bash theme={null}
POST /v1/email-blasts
```

| Field | Type | Required | Notes |
| - | - | - | - |
| `name` | `string` | Yes | Internal display name, `1`-`200` characters |
| `description` | `string \| null` | No | Maximum `1000` characters |

Returns `201 Created` with the blast object.

## Set the Audience

```bash theme={null}
PUT /v1/email-blasts/{blastId}/audience
```

Reference an existing contact list, or create a new list and attach it in one call. This replaces any previously attached audience. Returns `200 OK` with the updated [blast object](#blast-object).

### Existing List

| Field | Type | Required | Notes |
| - | - | - | - |
| `source` | `string` | Yes | `existing` |
| `list_id` | `uuid` | Yes | An existing static or dynamic contact list |

### Inline Static List

| Field | Type | Required | Notes |
| - | - | - | - |
| `source` | `string` | Yes | `inline` |
| `kind` | `string` | Yes | `static` |
| `name` | `string` | No | `1`-`255` characters. Defaults to `"<blast name> (campaign <id>)"` |
| `description` | `string \| null` | No | Maximum `1000` characters |
| `contact_ids` | `uuid[]` | Yes | Contacts to seed the list with, maximum `500` per request |

### Inline Dynamic List

| Field | Type | Required | Notes |
| - | - | - | - |
| `source` | `string` | Yes | `inline` |
| `kind` | `string` | Yes | `dynamic` |
| `name` | `string` | No | `1`-`255` characters |
| `description` | `string \| null` | No | Maximum `1000` characters |
| `rules` | `object` | Yes | Segment rule tree — membership is computed continuously |

### Audience Rules

Dynamic audiences are defined by a rule tree of groups and conditions. Rule objects use **camelCase** keys, unlike the rest of the public API — they pass through to the segment engine verbatim.

```json theme={null}
{
  "kind": "group",
  "operator": "and",
  "conditions": [
    {"kind": "condition", "source": "contact", "field": "state", "operator": "equals", "value": "CA"},
    {"kind": "condition", "source": "list_membership", "listId": "11111111-1111-4111-8111-111111111111", "operator": "is_member"}
  ]
}
```

Each condition reads one `source`:

| Source | Selector | Operators |
| - | - | - |
| `contact` | `field` — a standard contact field key in camelCase (e.g. `leadStatus`, `tentedScore`, `createdAt`) | Per field type: strings take `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`; numbers and dates take `equals`, `not_equals`, `greater_than(_or_equal)`, `less_than(_or_equal)`; booleans take `is_true`, `is_false`. All types take `is_empty` / `is_not_empty` |
| `custom_field` | `fieldDefinitionId` — an active custom field definition ID | Same value operators as `contact` |
| `activity` | `activityType` (e.g. `form_submission`), optional `timeWindow` (`{"amount": 30, "unit": "day"}`) | `has_activity`, `has_no_activity` |
| `list_membership` | `listId` — a static list | `is_member`, `is_not_member` |

Groups combine children with `operator: "and"` (default) or `"or"` and can nest up to `5` levels deep, with at most `100` nodes per tree. Shorthand operator aliases (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `member_of`, `occurred`, ...) are accepted. `email_step` conditions are not allowed in audiences — they only work inside [flow condition steps](/api-reference/managing-triggered-flows#condition-rules).

## Set the Email

```bash theme={null}
PUT /v1/email-blasts/{blastId}/email
```

Reference an existing email with `{"source": "existing", "email_id": "..."}`, or create one inline:

| Field | Type | Required | Notes |
| - | - | - | - |
| `source` | `string` | Yes | `inline` |
| `name` | `string` | No | Defaults to `"<blast name> Email"` |
| `subject` | `string` | No | `1`-`998` characters. Must be set before the blast can send |
| `template_id` | `uuid` | No | Seed content and default headers from an approved template. Mutually exclusive with `html` |
| `html` | `string` | No | Seed content from your own HTML, up to `2 MB`. Mutually exclusive with `template_id` |
| `preview_text` | `string` | No | Maximum `200` characters |
| `from_name` | `string` | No | Maximum `120` characters. Must be set before the blast can send |
| `from_address` | `string` | No | Must be set before the blast can send; domain must be verified |
| `reply_to_email` | `string` | No | Must be set before the blast can send |

Omit both `template_id` and `html` for a blank branded scaffold. Returns `200 OK` with the updated [blast object](#blast-object).

<Info>
  The attached email must be **approved** before the blast can be scheduled or sent. Inline emails start as drafts — take the `email.email_id` from the response and approve it via [`POST /v1/emails/{emailId}/approve`](/api-reference/approving-emails#approve-and-unapprove).
</Info>

## Approval Readiness

`GET /v1/email-blasts/{blastId}` on a draft includes an `approval_readiness` object listing what still blocks sending:

```json theme={null}
{
  "approval_readiness": {
    "ready": false,
    "missing": ["email_approval", "schedule"],
    "warnings": ["empty_static_audience"]
  }
}
```

Scheduling or sending a blast that is not ready returns `400 Bad Request` with `error_code: "campaign_not_ready"` and the same `approval_readiness` details.

## Schedule a Blast

```bash theme={null}
POST /v1/email-blasts/{blastId}/schedule
```

| Field | Type | Required | Notes |
| - | - | - | - |
| `scheduled_at` | `string` | Yes | ISO-8601 datetime with timezone offset, e.g. `2026-07-10T09:00:00-07:00`. Must be in the future |
| `operational` | `boolean` | No | Defaults to `false` — see below |

Returns `200 OK` with the blast in `scheduled` status. Use `POST .../unschedule` to cancel a scheduled send and return the blast to `draft`.

<Warning>
  Setting `operational: true` marks the send as transactional: unsubscribed contacts are **not** suppressed and the unsubscribe footer is skipped. Only use it for non-marketing mail such as receipts and service notices.
</Warning>

<Note>
  **Free plan:** scheduling always succeeds, but the limits are re-checked when the schedule fires. If at send time the blast's recipient count exceeds what remains of the day's **100-email** allotment — or the workspace is over its **1,000-contact** allowance — the blast is automatically **cancelled** instead of sent: `status` becomes `cancelled`, `cancelled_reason` is set to `daily_email_limit` or `contact_allowance`, no emails go out, and `scheduled_at` keeps the originally scheduled time. A/B test deliveries are guarded the same way at their send moments. Pro and Max sends are never auto-cancelled.
</Note>

## Send Now

```bash theme={null}
POST /v1/email-blasts/{blastId}/send-now
```

Accepts the same optional `operational` flag and starts the send immediately through the async pipeline. Returns `202 Accepted` with the blast in `sending` status; poll `GET /v1/email-blasts/{blastId}` for progress counters.

On the Free plan, a send that would exceed the remaining daily 100-email allotment (or a workspace over its 1,000-contact allowance) is rejected up front with `403 Forbidden` and `error_code: "free_plan_email_limit"` — the blast stays an editable `draft`, nothing is cancelled:

```json theme={null}
{
  "error": "Sending this blast to 250 recipients would exceed the free plan's daily limit of 100 emails (100 remaining today). Upgrade to Pro or Max to send more.",
  "error_code": "free_plan_email_limit",
  "details": {
    "reason": "daily_email_limit",
    "recipient_count": 250,
    "daily_limit": 100,
    "sent_today": 0,
    "remaining_today": 100,
    "contact_count": 800,
    "contact_limit": 1000
  }
}
```

## Blast Object

`200 OK`

```json theme={null}
{
  "blast_id": "e6a1e6de-6a2e-4a3a-9a01-3f1c2b4d5e6f",
  "type": "email_blast",
  "name": "July launch",
  "description": null,
  "status": "sent",
  "audience_list_id": "11111111-1111-4111-8111-111111111111",
  "audience_source": "existing",
  "audience": {
    "list_id": "11111111-1111-4111-8111-111111111111",
    "source": "existing",
    "name": "Newsletter subscribers",
    "kind": "static",
    "member_count": 1250
  },
  "email_id": "f11ef3cf-8664-4fe5-a261-c5b4d647b7d1",
  "email_source": "existing",
  "email": {
    "email_id": "f11ef3cf-8664-4fe5-a261-c5b4d647b7d1",
    "source": "existing",
    "name": "July launch email",
    "status": "approved",
    "subject": "The July release is here",
    "current_version": 3,
    "approved_version": 3
  },
  "operational": false,
  "scheduled_at": "2026-07-10T16:00:00.000Z",
  "approved_at": "2026-07-08T09:12:00.000Z",
  "approved_email_version": 3,
  "sent_at": "2026-07-10T16:04:12.000Z",
  "send_mode": "scheduled",
  "cancelled_reason": null,
  "cancelled_at": null,
  "details": {
    "blast_recipient_count": 1250,
    "blast_sent_count": 1248,
    "blast_failed_count": 0,
    "blast_blocked_count": 2,
    "blast_qualified_count": 1250,
    "blast_audience_truncated": false,
    "blast_delivered_count": 1240,
    "blast_opened_count": 611,
    "blast_clicked_count": 187,
    "blast_bounced_count": 8,
    "blast_spam_count": 0,
    "blast_unsubscribed_count": 3
  },
  "created_at": "2026-07-08T09:00:00.000Z",
  "updated_at": "2026-07-10T16:20:00.000Z"
}
```

`cancelled_at` is stamped on every cancelled blast. `cancelled_reason` stays `null` for user-initiated cancels; it is `daily_email_limit` or `contact_allowance` when the platform auto-cancelled a scheduled Free-plan blast at send time (see [Schedule a Blast](#schedule-a-blast)) — those blasts sent nothing and keep their original `scheduled_at`.

## List Blasts

```bash theme={null}
GET /v1/email-blasts
```

| Parameter | Type | Default | Notes |
| - | - | - | - |
| `status` | `string` | `any` | `any`, `draft`, `scheduled`, `sending`, `sent`, `failed`, `cancelled`, or `archived` |
| `archived` | `string` | `exclude` | `exclude`, `include`, or `only` |
| `sort_by` | `string` | `updated_at` | `updated_at`, `created_at`, `scheduled_at`, or `name` |
| `sort_order` | `string` | `desc` | `asc` or `desc` |
| `page` | `number` | `1` | 1-based page number |
| `limit` | `number` | `25` | Page size, `1`-`100` |
| `search` | `string` | — | Case-insensitive name search |

Responses contain `blasts` and a `pagination` object with `page`, `limit`, `total`, and `totalPages`.

## Recipients and Engagement

```bash theme={null}
GET /v1/email-blasts/{blastId}/contacts
```

Page through a blast's contacts by engagement `category`: `recipients` (everyone prepared for the send, the default), `sent`, `delivered`, `opened`, `clicked`, `bounced`, `unsubscribed`, or `spam`. Also accepts `page`, `limit`, `search`, `sort` (engagement `*_at` timestamps sort contacts without that event last), and `order`.

Responses contain `contacts` and a `pagination` object:

```json theme={null}
{
  "contacts": [
    {
      "recipient_id": "01JZ9J8Q2K7X4W1R5T3V6Y0ZBM",
      "contact_id": "22222222-2222-4222-8222-222222222222",
      "display_name": "Jane Doe",
      "email": "jane@example.com",
      "contact_deleted": false,
      "sent_at": "2026-07-10T16:04:12.000Z",
      "delivered_at": "2026-07-10T16:04:15.000Z",
      "opened_at": "2026-07-10T17:22:03.000Z",
      "clicked_at": null,
      "clicked_url": null,
      "bounced_at": null,
      "unsubscribed_at": null,
      "complained_at": null
    }
  ],
  "pagination": {"page": 1, "limit": 25, "total": 611, "totalPages": 25}
}
```

### Export to CSV

```bash theme={null}
POST /v1/email-blasts/{blastId}/contacts/export?category=opened
GET  /v1/email-blasts/{blastId}/contacts/export/{exportId}
```

Starting an export returns `202 Accepted` with an `export` job. Poll the job until `status` is `completed`, then fetch the file from its `download_url`:

```json theme={null}
{
  "export": {
    "export_id": "01JZ9H2M7Q3W8R5T1V6X0YKZ4B",
    "status": "completed",
    "file_name": "july-launch-opened.csv",
    "row_count": 611,
    "error_message": null,
    "download_url": "https://...",
    "created_at": "2026-07-11T10:00:00.000Z",
    "updated_at": "2026-07-11T10:00:09.000Z",
    "completed_at": "2026-07-11T10:00:09.000Z"
  }
}
```

## Rename, Archive, Delete

* `PATCH /v1/email-blasts/{blastId}` updates `name` and/or `description`.
* `POST /v1/email-blasts/{blastId}/archive` archives a blast that is not mid-send.
* `DELETE /v1/email-blasts/{blastId}` deletes it and returns `{"blast_id": "...", "deleted": true}`. Scheduled blasts must be unscheduled first, and archived blasts cannot be deleted.

## Common Errors

| Status | Cause |
| - | - |
| `400 Bad Request` | Invalid JSON body or field validation failure |
| `400 Bad Request` | The ID belongs to a triggered flow, not a blast (`campaign_not_blast`) |
| `400 Bad Request` | Blast is not ready to schedule or send (`campaign_not_ready`, with `approval_readiness` details) |
| `400 Bad Request` | `scheduled_at` is in the past (`campaign_schedule_in_past`) |
| `401 Unauthorized` | Missing or invalid bearer token |
| `403 Forbidden` | Send Now over a Free-plan limit (`free_plan_email_limit`, with the limit details) — the blast stays a `draft` |
| `404 Not Found` | Blast does not exist in the workspace (`campaign_not_found`) |
| `409 Conflict` | Audience or email edited outside `draft` status (`campaign_not_editable`) |
| `409 Conflict` | Invalid lifecycle transition, e.g. unscheduling a blast that is not scheduled (`campaign_invalid_state_transition`) |
| `409 Conflict` | Deleting a scheduled or archived blast (`campaign_not_deletable`) |

<Card title="Next: Manage Triggered Flows" icon="arrow-right" href="/api-reference/managing-triggered-flows">
  Automate multi-step email journeys that enroll contacts on triggers.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.