---
title: Messages Reference
path: reference/messages
status: published
---

# Messages Reference

Endpoints for querying and managing sent messages. For the guide-level walk-through, see [Messages and Events](../tutorials/messages-and-events).

**Base path:** `/v3/messages/`
**Required permission:** `mail.send` (for management actions); plain reads are available to any authenticated caller in the tenant.

## GET /v3/messages

List messages for the current tenant, filtered and paginated.

**Query parameters:**

| Parameter | Type | Notes |
|-----------|------|-------|
| `status` | string | One of `queued`, `processing`, `rendered`, `sending`, `sent`, `delivered`, `bounced`, `failed`, `cancelled`, `sandbox` |
| `to_email` | string | Exact match |
| `from_email` | string | Exact match |
| `subject` | string | Substring search |
| `batch_id` | string | Messages in this batch |
| `template_id` | string | Messages using this template |
| `category` | string | Messages tagged with this category |
| `start_date` | string | ISO-8601 or `YYYY-MM-DD` |
| `end_date` | string | ISO-8601 or `YYYY-MM-DD` |
| `page` | integer | 1-indexed (default 1) |
| `page_size` | integer | 1–100 (default 25) |

**Response (200):**

```json
{
  "messages": [
    {
      "id": "msg_01HXYZ",
      "status": "delivered",
      "from_email": "hello@mail.example.com",
      "from_name": "Acme",
      "subject": "Welcome to Acme",
      "to_emails": ["ada@example.com"],
      "template_id": "d-welcome",
      "batch_id": null,
      "categories": ["onboarding"],
      "created_at": "2026-04-23T10:00:00Z",
      "delivered_at": "2026-04-23T10:00:02Z"
    }
  ],
  "total": 4872,
  "page": 1,
  "page_size": 25,
  "total_pages": 195
}
```

## GET /v3/messages/{message_id}

Full message detail including headers, body, tracking settings, and event timeline.

**Response (200):**

```json
{
  "id": "msg_01HXYZ",
  "status": "delivered",
  "from_email": "hello@mail.example.com",
  "from_name": "Acme",
  "subject": "Welcome to Acme",
  "to_emails": ["ada@example.com"],
  "cc_emails": [],
  "bcc_emails": [],
  "reply_to": "support@example.com",
  "categories": ["onboarding"],
  "template_id": "d-welcome",
  "batch_id": null,
  "custom_args": {"user_id": "u_12345"},
  "custom_headers": {},
  "tracking_settings": {
    "open_tracking": true,
    "click_tracking": true,
    "subscription_tracking": false
  },
  "retry_count": 0,
  "next_retry_at": null,
  "html_content": "<html>...</html>",
  "plain_content": "...",
  "created_at": "2026-04-23T10:00:00Z",
  "queued_at": "2026-04-23T10:00:00Z",
  "sent_at": "2026-04-23T10:00:01Z",
  "delivered_at": "2026-04-23T10:00:02Z",
  "error_message": null,
  "events": [
    {
      "event_type": "processed",
      "recipient_email": "ada@example.com",
      "timestamp": "2026-04-23T10:00:00Z"
    },
    {
      "event_type": "delivered",
      "recipient_email": "ada@example.com",
      "timestamp": "2026-04-23T10:00:02Z",
      "smtp_response": "250 2.0.0 OK",
      "metadata": {"mx_host": "mx1.example.com", "tls_version": "TLSv1.3"}
    }
  ]
}
```

**Errors:** 404 if the message doesn't exist or belongs to another tenant.

## POST /v3/messages/{message_id}/cancel

Cancel a message that hasn't been sent yet. Works on `QUEUED` and `PROCESSING` states.

**Response (200):**

```json
{"message_id": "msg_01HXYZ", "status": "cancelled", "message": "Cancelled before delivery"}
```

If the message is already in `SENDING` or later, returns:

```json
{"message_id": "msg_01HXYZ", "status": "sending", "message": "Already in flight; cancellation ignored"}
```

**Permission:** `mail.cancel`.

**Errors:** 404 if not found.

## POST /v3/messages/{message_id}/retry

Force-requeue a message. Works on `PROCESSING`, `SENDING`, `FAILED`, and `BOUNCED` states.

**Response (200):**

```json
{"message_id": "msg_01HXYZ", "status": "queued", "message": "Re-queued for delivery"}
```

**Permission:** `mail.send`.

**Errors:** 400 if the message is in a state that can't be retried (e.g., `CANCELLED`, `SANDBOX`).

## POST /v3/messages/{message_id}/fail

Mark a stuck message as `FAILED` manually. Use this to clean up orphans the worker abandoned.

**Request body:**

```json
{"reason": "optional explanation"}
```

**Response (200):**

```json
{"message_id": "msg_01HXYZ", "status": "failed", "message": "Marked as failed"}
```

**Permission:** `mail.send`.

## Event shape on the timeline

Every event in `events[]` has at least `event_type`, `recipient_email`, and `timestamp`. Additional fields depend on the event type:

| Event | Extra fields |
|-------|--------------|
| `processed` | — |
| `deferred` | `smtp_response`, `metadata.attempt`, `metadata.next_retry_at` |
| `delivered` | `smtp_response`, `metadata.mx_host`, `metadata.tls_version` |
| `bounce` | `bounce_type`, `bounce_reason`, `smtp_response`, `metadata.diagnostic_code` |
| `blocked` | `smtp_response`, `metadata.reason` |
| `dropped` | `metadata.reason` |
| `open` | `user_agent`, `ip_address`, `metadata.is_first_open` |
| `click` | `url`, `user_agent`, `ip_address` |
| `spam_report` | `metadata.report_source`, `metadata.feedback_id` |
| `unsubscribe` | `metadata.source` |
| `group_unsubscribe` | `metadata.group_id`, `metadata.source` |
| `group_resubscribe` | `metadata.group_id` |

See [Events and Webhooks](../concepts/events-and-webhooks) for the full semantics.

## Related

- [Messages and Events](../tutorials/messages-and-events)
- [Events and Webhooks](../concepts/events-and-webhooks)
- [Statistics Reference](stats)
