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

# Email

> Send broadcasts and transactional email to your customers, and operate a full email client — inboxes, threads, drafts, replies, labels, and sending domains — through the API.

**Email in Crevio is two things: a simple way to broadcast to your customers, and a full email client — inboxes, threads, messages, drafts, search, and replies — that lets you (or an agent) run your inbox programmatically.**

Use the broadcast side to blast an announcement to every paid customer, and the client side to read, reply to, and label inbound mail. You can just ask Crevio to draft and send a launch email, or wire up an agent that watches for incoming messages and replies on your behalf.

<Warning>
  **Sending a 1:1 email? Use an inbox, not a broadcast.** Broadcasts are one-to-many: they don't live in a thread, so there is nowhere to reply from. `POST /email/inboxes/{inbox_id}/messages` creates a tracked thread instead. Every account always has an inbox — no domain verification needed — so there is no reason to reach for a broadcast for a single recipient.
</Warning>

## Part 1 — Broadcasts

A broadcast is one email sent to many people, persisted end to end: you draft it, schedule it, send it, then read back who received it, who opened it, and which links they clicked.

| Endpoint                           | Purpose                                                           |
| ---------------------------------- | ----------------------------------------------------------------- |
| `POST /broadcasts`                 | Create a draft, schedule a send, or send immediately              |
| `GET /broadcasts`                  | List broadcasts (filter by `status`, `sent_after`, `sent_before`) |
| `GET /broadcasts/{id}`             | Retrieve one, with its current `stats`                            |
| `PATCH /broadcasts/{id}`           | Edit a draft or scheduled broadcast                               |
| `DELETE /broadcasts/{id}`          | Delete a draft or scheduled broadcast                             |
| `POST /broadcasts/{id}/send`       | Send now, ignoring `scheduled_at`                                 |
| `POST /broadcasts/{id}/cancel`     | Cancel a scheduled or in-flight send                              |
| `GET /broadcasts/{id}/stats`       | Poll delivery and engagement counts                               |
| `GET /broadcasts/{id}/recipients`  | Per-recipient delivery state                                      |
| `GET /broadcasts/{id}/link-clicks` | Clicks per link                                                   |

### The lifecycle

`draft` → `scheduled` → `sending` → `sent`, with `canceled` and `failed` as exits.

Omit `scheduled_at` and you get a **draft**. Pass a future timestamp and it becomes **scheduled** — a cron picks it up when it comes due, so a deploy or restart can't strand it. Pass `send_now: true` and it goes out immediately. A draft or scheduled broadcast stays editable; once it starts **sending**, the rendered body and recipient list are frozen so what you report can't drift from what recipients actually received.

### Choosing an audience

`audience` is how you say who receives a broadcast. Filters combine as a union and are deduplicated by address:

| Field                     | Purpose                                                |
| ------------------------- | ------------------------------------------------------ |
| `audience.send_to_all`    | Every customer on the account                          |
| `audience.customer_types` | `paid`, `free`, and/or `lead`                          |
| `audience.tag_ids`        | Customers carrying these tags                          |
| `audience.to`             | Literal addresses, for recipients who aren't customers |

Addresses on your suppression list — hard bounces, spam complaints, and unsubscribes — are dropped before the send is counted, so `stats.recipients` reflects what the provider will actually attempt. A broadcast with no reachable recipients is rejected rather than silently sending to nobody.

### Content

| Field          | Purpose                                                           |
| -------------- | ----------------------------------------------------------------- |
| `subject`      | Email subject. Required to send                                   |
| `body`         | Email body (HTML). Required to send                               |
| `preview_text` | The line shown after the subject in the inbox                     |
| `from`         | Override the sending address (must be on a verified domain)       |
| `reply_to`     | Override the Reply-To address                                     |
| `styled`       | Wrap the body in your branded layout (default `true`)             |
| `metadata`     | Your own key/value storage, returned as-is and filterable on list |

<Note>
  Every broadcast carries a per-recipient unsubscribe link and the RFC 8058 `List-Unsubscribe` headers that let mailbox providers show a native "Unsubscribe" button. An opt-out adds that address to your suppression list, so it is honoured by every send path afterwards. This is not optional and cannot be turned off — bulk marketing mail requires it, and it is what protects your sending reputation.
</Note>

### Worked example: broadcast to all paid customers

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.crevio.co/v1/broadcasts \
    -H "Authorization: Bearer YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "subject": "Doors to the mastermind open Monday",
      "preview_text": "Your member space unlocks Monday morning",
      "body": "Thanks for joining — our coaching mastermind kicks off Monday and your member space unlocks then.",
      "audience": { "customer_types": ["paid"] },
      "send_now": true
    }'
  ```

  ```typescript SDK theme={null}
  import { Crevio } from "@crevio/sdk";

  const crevio = new Crevio({ apiKeyAuth: "YOUR_API_TOKEN" });

  const broadcast = await crevio.broadcasts.create({
    subject: "Doors to the mastermind open Monday",
    previewText: "Your member space unlocks Monday morning",
    body: "Thanks for joining — our coaching mastermind kicks off Monday and your member space unlocks then.",
    audience: { customerTypes: ["paid"] },
    sendNow: true,
  });

  // Engagement keeps accruing after the send finishes.
  const stats = await crevio.broadcasts.stats({ id: broadcast.id });
  ```

  ```text MCP (agent) theme={null}
  Email all my paid customers letting them know the mastermind kicks off Monday.
  ```
</CodeGroup>

### Reading the results

`stats` is embedded on every broadcast and also available on its own endpoint. Rates are fractions of `sent`, not of `recipients` — a recipient never handed to the provider can't have opened anything.

```json theme={null}
{
  "recipients": 412,
  "sent": 412,
  "delivered": 404,
  "bounced": 6,
  "complained": 1,
  "failed": 0,
  "opened": 198,
  "clicked": 57,
  "unsubscribed": 3,
  "delivery_rate": 0.9806,
  "open_rate": 0.4806,
  "click_rate": 0.1383,
  "progress": 100.0,
  "status": "sent"
}
```

`opened` and `clicked` count **distinct recipients**. For per-person detail use `GET /broadcasts/{id}/recipients`, and for per-link detail use `GET /broadcasts/{id}/link-clicks`, which reports both total `clicks` and `unique_clicks`.

<Warning>
  **Treat `open_rate` as a soft signal.** Apple Mail Privacy Protection prefetches images for every Apple Mail recipient, so those register as opens whether or not anyone read the message. Open rates are inflated by an amount you can't measure. `click_rate` is the number to trust.
</Warning>

Subscribe to `broadcast.sent`, `broadcast.canceled`, and `broadcast.failed` to react without polling. See [Webhooks](/docs/developer/guides/webhooks).

### Open and click tracking

Tracking is **off until the account has its own verified email domain**, and turns itself on from there — no configuration.

Click tracking works by rewriting every link in the body to redirect through a tracking host. Whose host that is decides whether it costs you deliverability:

* URL blocklists score the hosts appearing in a message body, so a tracking host shared across many senders carries whatever reputation the worst of them earns.
* Links pointing somewhere other than the domain the mail is authenticated as is structurally what phishing looks like, and inbox providers weigh that alignment.

So Crevio never puts your links on a shared tracking host. When you verify a domain, it also claims `click.yourdomain.com` and publishes the CNAME for it — written straight into your DNS when Crevio hosts the zone, or shown alongside the other records for you to add when it doesn't. Once that record resolves, click and open tracking switch on, links stay on your domain, and the reputation is yours alone.

Until then `stats.clicked` and `link-clicks` stay empty. That's deliberate: sending on the shared platform domain *and* tracking through a shared host is the configuration most likely to cost inbox placement, and click data isn't worth that trade.

<Note>
  If something already lives at `click.yourdomain.com`, Crevio leaves it alone rather than overwriting it — the record is surfaced in domain settings instead, and tracking stays off until it's resolved.
</Note>

<Tip>
  Let Crevio write the copy: ask it to draft the broadcast, review the draft, then send. See [Tasks](/docs/developer/guides/tasks).
</Tip>

### Sending limits

Broadcasts draw on a per-account daily send limit set by your plan. Exceeding it returns `429` with `quota_exceeded`; a *scheduled* broadcast that hits the cap stays scheduled and is retried after the limit resets at UTC midnight, rather than failing outright.

## Part 2 — The email client

Crevio also operates as a real mailbox built around **inboxes → threads → messages**, with drafts, search, labels, and attachments.

Every account has a built-in inbox at `{slug}@ai.crevio.app` that works immediately — listing inboxes creates it if it doesn't exist yet. Mail sent from it goes out on Crevio's verified sending domain with replies routed back to that address. Verify a domain of your own to add named inboxes like `support@yourbrand.com` and send from your own address.

### Inboxes, threads, and messages

```bash theme={null}
# List inboxes
curl https://api.crevio.co/v1/email/inboxes \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# List threads in an inbox
curl https://api.crevio.co/v1/email/threads \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Read the messages in a thread
curl https://api.crevio.co/v1/email/threads/thread_abc123/messages \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

### Worked example: reply to an inbound message

Reading a thread gives you its messages; reply directly to one.

```bash theme={null}
curl -X POST https://api.crevio.co/v1/email/messages/msg_abc123/reply \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi Jordan — yes, the group coaching calls are included. Want me to add you to the next mastermind cohort?"
  }'
```

The client also supports `reply-all` and `forward` on a message, the same way.

### Drafts, search, and labels

```bash theme={null}
# Create a draft
curl -X POST https://api.crevio.co/v1/email/drafts \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "to": ["jordan@example.com"], "subject": "Mastermind options", "body": "..." }'

# Search across mail
curl "https://api.crevio.co/v1/email/search?query=refund" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

# Label a thread
curl -X POST https://api.crevio.co/v1/email/threads/thread_abc123/labels \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "label": "support" }'
```

Attachments are available on messages, and drafts can be sent once finalized.

### Sending domains

To send from your own domain (rather than the platform default), enable the `email_sending` capability on the domain. This is what makes broadcasts arrive from `hello@yourbrand.com`.

```bash theme={null}
# Enable sending on a domain (creates it if not yet connected)
curl -X POST https://api.crevio.co/v1/domains \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "mastermind.coach", "capabilities": { "email_sending": true } }'

# Re-check DNS whenever you like
curl -X POST "https://api.crevio.co/v1/domains/dom_abc123/verify?capability=email_sending" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

<Note>
  The response's `records[]` lists the DNS records to add at your registrar, each with its own status. For domains purchased through Crevio they're written automatically. See [Domains](/docs/developer/guides/domains).
</Note>

## Email webhooks

Both halves are event-driven. Subscribe instead of polling:

* **Inbox** — `email_message.received`, `email_message.sent`, `email_message.delivered`, `email_message.bounced`, `email_message.complained`, `email_message.failed`.
* **Broadcasts** — `broadcast.sent` (every recipient handed to the provider), `broadcast.canceled`, `broadcast.failed`.

A common pattern: listen for `email_message.received`, run a [Task](/docs/developer/guides/tasks) to draft a reply, and send it. See [Webhooks](/docs/developer/guides/webhooks).

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="bell" href="/docs/developer/guides/webhooks">
    React to inbound mail with `email_message.*` events.
  </Card>

  <Card title="Tasks" icon="robot" href="/docs/developer/guides/tasks">
    Let an agent triage and reply to your inbox.
  </Card>

  <Card title="Customers" icon="users" href="/docs/developer/api-reference/introduction">
    Segment who your broadcasts reach.
  </Card>

  <Card title="Domains" icon="globe" href="/docs/developer/guides/domains">
    Manage DNS for your sending domain.
  </Card>
</CardGroup>
