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

# Sending email

> Every option on POST /v1/emails.

## Request parameters

<ParamField body="from" type="string" required>
  RFC 5322 address. Either `email@verified-domain.com` or `"Name" <email@verified-domain.com>`. The domain must be verified in your project.
</ParamField>

<ParamField body="to" type="string[]" required>
  Up to 50 recipients per message.
</ParamField>

<ParamField body="cc" type="string[]">
  Optional CC recipients.
</ParamField>

<ParamField body="bcc" type="string[]">
  Optional BCC recipients.
</ParamField>

<ParamField body="reply_to" type="string[]">
  Optional `Reply-To` override. Default: the `from` address.
</ParamField>

<ParamField body="subject" type="string" required>
  Subject line. Max 998 chars per RFC 5322.
</ParamField>

<ParamField body="html" type="string">
  HTML body. Either `html` or `text` (or both) is required.
</ParamField>

<ParamField body="text" type="string">
  Plain-text body. Strongly recommended for deliverability — many mail clients score text+html better than html-only.
</ParamField>

<ParamField body="headers" type="object">
  Custom RFC 5322 headers. Prefix with `X-` for safety.
</ParamField>

<ParamField body="tags" type="string[]">
  Free-form labels for dashboard filtering and analytics grouping. e.g. `["campaign:launch-q2", "category:transactional"]`.
</ParamField>

<ParamField body="track_opens" type="boolean" default="false">
  Inject a 1×1 tracking pixel. Fires `email.opened` webhooks.
</ParamField>

<ParamField body="track_clicks" type="boolean" default="false">
  Rewrite links through our click-tracking proxy. Fires `email.clicked` webhooks.
</ParamField>

<ParamField body="send_at" type="string (ISO 8601)">
  Schedule for a future time. Max lead: 30 days.
</ParamField>

## Response

```json theme={null}
{
  "id": "01H8XKQJ3Z...",
  "status": "queued"
}
```

The `status` transitions through `queued → sent → delivered` (or `bounced` / `complained` / `failed`), with webhooks at every transition.

## With tracking

```typescript theme={null}
await pm.emails.send({
  from: 'team@mail.shuttlers.ng',
  to: ['user@example.com'],
  subject: 'New update on your booking',
  html: '<p>See details <a href="https://shuttlers.ng/bookings/42">here</a>.</p>',
  text: 'See details: https://shuttlers.ng/bookings/42',
  track_opens: true,
  track_clicks: true,
  tags: ['category:transactional'],
});
```

## Batch send

`POST /v1/emails/batch` with up to 100 emails per call:

```typescript theme={null}
await pm.emails.batch([
  { from, to: ['a@example.com'], subject: 'A', html: '...' },
  { from, to: ['b@example.com'], subject: 'B', html: '...' },
]);
```

Batch returns `{ data: [{id, status}, ...] }` — one row per input, in the same order.

## Scheduled send & cancel

```typescript theme={null}
const email = await pm.emails.send({
  from, to, subject, html,
  send_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(),  // in 1h
});

// Before send_at fires:
await pm.emails.cancel(email.id);
```

After `send_at` elapses, cancel fails with `conflict`.

## Error cases

| Error                 | HTTP | Cause                                     |
| --------------------- | ---- | ----------------------------------------- |
| `domain_unverified`   | 400  | Sending domain has no verified DKIM/SPF   |
| `address_suppressed`  | 400  | A recipient is on your suppression list   |
| `validation_error`    | 400  | Malformed address, missing required field |
| `quota_exceeded`      | 402  | Monthly quota + wallet empty              |
| `rate_limit_exceeded` | 429  | Slow down                                 |
