---
title: Mail Send Reference
path: reference/mail-send
status: published
---

# Mail Send Reference

The `/v3/mail/send` endpoint and related batch endpoints. For the guide-level walk-through, see [Sending Mail](../tutorials/sending-mail).

**Base path:** `/v3/mail/`
**Required permission:** `mail.send` (send), `mail.schedule` (batch endpoints, `send_at`), `mail.cancel` (cancel scheduled).

## POST /v3/mail/send

Queue one or more messages for delivery.

**Request body:**

Top-level fields:

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `personalizations` | array | Yes | 1–1000 items; each creates one message |
| `from` | object | Yes | `{email, name?}`; must use verified domain (live only) |
| `reply_to` | object | No | `{email, name?}` |
| `reply_to_list` | array | No | Alternative to single `reply_to` |
| `subject` | string | Conditional | Required unless each personalization has one, or template supplies it |
| `content` | array | Conditional | Required unless `template_id` is set |
| `template_id` | string | Conditional | Must start with `d-`; mutually exclusive with `content` |
| `attachments` | array | No | Max 10 items; total body ≤ 20 MB |
| `headers` | object | No | String → string |
| `categories` | array | No | Max 10 items, each ≤ 255 chars |
| `custom_args` | object | No | Arbitrary metadata |
| `send_at` | integer | No | Unix timestamp; future schedule. Requires `mail.schedule` |
| `batch_id` | string | No | From `POST /v3/mail/batch` |
| `asm` | object | No | `{group_id, groups_to_display?}` |
| `ip_pool_name` | string | No | Named outbound IP pool |
| `mail_settings` | object | No | See below |
| `tracking_settings` | object | No | See below |

**Personalization fields:**

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `to` | array of `{email, name?}` | Yes | 1–1000 recipients |
| `cc` | array of `{email, name?}` | No | |
| `bcc` | array of `{email, name?}` | No | |
| `subject` | string | No | Overrides top-level |
| `headers` | object | No | Overrides top-level |
| `substitutions` | object | No | Legacy SendGrid substitutions |
| `dynamic_template_data` | object | No | Template rendering context |
| `custom_args` | object | No | Overrides top-level |
| `send_at` | integer | No | Per-personalization schedule |

**Content fields:**

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `type` | string | Yes | `text/plain` or `text/html` |
| `value` | string | Yes | Body content |

**Attachment fields:**

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `content` | string (base64) | Yes | File bytes |
| `filename` | string | Yes | 1–255 chars |
| `type` | string | No | MIME type |
| `disposition` | string | No | `attachment` (default) or `inline` |
| `content_id` | string | Conditional | Required for `inline` |

**mail_settings fields:**

| Field | Type | Notes |
|-------|------|-------|
| `sandbox_mode.enable` | bool | Force sandbox for this request |
| `bypass_list_management.enable` | bool | Ignore all suppression lists |
| `bypass_bounce_management.enable` | bool | Ignore bounce suppressions |
| `bypass_spam_management.enable` | bool | Ignore spam-report suppressions |
| `bypass_unsubscribe_management.enable` | bool | Ignore unsubscribe suppressions |
| `footer.enable` | bool | Add a footer |
| `footer.text` | string | Plain-text footer |
| `footer.html` | string | HTML footer |

**tracking_settings fields:**

| Field | Type | Notes |
|-------|------|-------|
| `click_tracking.enable` | bool | |
| `click_tracking.enable_text` | bool | Also rewrite plain-text URLs |
| `open_tracking.enable` | bool | |
| `open_tracking.substitution_tag` | string | Custom pixel placement |
| `subscription_tracking.enable` | bool | |
| `subscription_tracking.text` | string | Plain-text unsubscribe footer |
| `subscription_tracking.html` | string | HTML unsubscribe footer |
| `subscription_tracking.substitution_tag` | string | Custom placement |
| `ganalytics.enable` | bool | |
| `ganalytics.utm_source` | string | |
| `ganalytics.utm_medium` | string | |
| `ganalytics.utm_campaign` | string | |
| `ganalytics.utm_term` | string | |
| `ganalytics.utm_content` | string | |

**Response (202 Accepted):**

Single personalization:
```json
{"message_id": "msg_01HXYZ"}
```

Multiple personalizations:
```json
{"message_ids": ["msg_01HXYZ", "msg_01HXYZ2", "msg_01HXYZ3"]}
```

**Error responses:**

| Status | Shape | When |
|--------|-------|------|
| 400 | `{"errors": [...]}` | Validation — missing required field, conflict between `content` and `template_id`, invalid `from`, etc. |
| 401 | `{"detail": "..."}` | Missing or invalid credentials |
| 403 | `{"detail": "Missing required scope: mail.send"}` | Key lacks scope |
| 403 | `{"detail": "Sender domain not verified"}` | `from.email` domain unverified (live only) |
| 413 | `{"detail": "Request body exceeds maximum size of 20 MB"}` | Body too large |
| 422 | `{"detail": [...]}` | Pydantic schema error |
| 429 | `{"detail": "Rate limit exceeded"}` | Honor `Retry-After` header |

## POST /v3/mail/batch

Create a new batch ID.

**Request:** empty body.

**Response (201):**

```json
{"batch_id": "bat_01HXYZ"}
```

**Permission:** `mail.send` (creating IDs isn't restricted to `mail.schedule`).

## GET /v3/mail/batch/{batch_id}

Aggregate status for every message with this `batch_id`.

**Response (200):**

```json
{
  "batch_id": "bat_01HXYZ",
  "total_messages": 4872,
  "status_counts": {
    "queued": 12,
    "processing": 8,
    "sent": 3200,
    "delivered": 1600,
    "bounced": 50,
    "failed": 2,
    "cancelled": 0,
    "sandbox": 0
  },
  "created_at": "2026-04-23T09:00:00Z",
  "completed_at": null,
  "is_complete": false
}
```

`is_complete` flips to `true` when every message has reached a terminal state.

**Errors:** 404 if batch_id has no messages or belongs to a different tenant.

## Related

- [Sending Mail](../tutorials/sending-mail)
- [Scheduling and Batches](../tutorials/scheduling-and-batches)
- [Messages Reference](messages)
