# Tracking Delivery

Source: https://molted.email/docs/tracking-delivery

Delivery statuses, delivery events, and how to monitor email delivery from the CLI, API, MCP and portal.

Every email sent through Molted Email is tracked through its full delivery lifecycle. The delivery provider reports events (accepted, delivered, bounced, ...); each one is recorded against the send's `requestId` and moves the send's delivery status forward.

## Status lifecycle

Emails progress through the following statuses:

```
queued → accepted → sent → delivered
                        ↘ deferred
                        ↘ bounced
                        ↘ complained
                        ↘ failed
```

| Status | Description |
| - | - |
| `queued` | The send request passed policy checks and is waiting to be sent. |
| `accepted` | The email provider accepted the message for delivery. |
| `sent` | The message was handed off to the recipient's mail server. |
| `delivered` | The recipient's server confirmed receipt. |
| `deferred` | Delivery was temporarily delayed. The system will retry automatically. |
| `bounced` | The recipient's server rejected the message. Most bounces are permanent (e.g., an invalid address); see [Automatic suppression](#automatic-suppression) for soft bounces. |
| `complained` | The recipient marked the email as spam. |
| `failed` | Delivery failed after all retry attempts. |

A send that never reaches the provider has one of these instead: `scheduled` (waiting for its `scheduledAt`), `pending_approval` (held for a human), `cancelled` (a scheduled send you cancelled), or none at all (blocked by policy, or a held send that was rejected or expired).

`opened` and `clicked` are recorded as events too, but they are engagement signals: they never change the delivery status.

## Status only moves forward

The status only ever advances: an event that ranks at or below the current status is recorded but leaves the status as it is. The ranking is `queued`, then `accepted`, then `sent` and `deferred` (equal), then `delivered`, then `bounced`, `complained` and `failed` (equal).

So `delivered` is not final: a later bounce or spam complaint still moves a delivered email to `bounced` or `complained`. Once an email reaches `bounced`, `complained` or `failed`, it will not change status again.

## Reading delivery status

| Where | What you get |
| - | - |
| `molted timeline <requestId>` / `GET /v1/dashboard/request/:requestId/timeline` | Every delivery event for the send, oldest first: `event_type`, `provider_name`, `occurred_at`, `metadata`. `404 request_not_found` for an unknown id. |
| `molted trace <requestId>` / `GET /v1/ops/trace/:requestId` | The send request (its `status` and `delivery_status`), the policy decision, the delivery events and the audit events. |
| `molted events get <messageId>` / `GET /v1/agent/messages/:id/events` | `{ "events": [...] }`, oldest first: each has `id`, `message_id`, `tenant_id`, `event_type`, `payload` and `created_at`. Pass the send's `requestId`. For delivery events the `payload` holds `provider_name`, `provider_event_id`, `occurred_at` and `metadata`; an `unsubscribed` event is added when the recipient clicks the unsubscribe link. An unknown id returns an empty list, not a 404. No MCP tool. |
| `GET /v1/agent/threads/:id` (`molted threads get`, MCP `get_thread`) | Each outbound message's `deliveryStatus` in its thread. |
| [Webhooks](https://molted.email/docs/webhooks.md) | `delivery.sent`, `delivery.delivered`, `delivery.bounced`, `delivery.complained`, and `suppression.created` when an address is suppressed. |
| [Event stream](https://molted.email/docs/events.md) (`molted listen`) | `delivery.*` for every event type above, plus `send.queued`, `policy.blocked` and the approval events. |

```bash title="CLI"
molted timeline REQUEST_ID
# → [ { "event_type": "accepted", ... }, { "event_type": "delivered", "provider_name": "resend", "occurred_at": "..." } ]
```

Use the `requestId` from the send response. Reading a send's timeline needs `read` access to the send's mailbox.

`timeline` and `events get` both come from the provider's reports, in two shapes: `timeline` keeps the provider's fields at the top level, `events get` wraps them in `payload` and also lists the `unsubscribed` event.

## Workspace-wide views

Four more routes summarise delivery across the workspace instead of one send. They are API-only: there is no CLI command or MCP tool for them. For a contact's history use [`molted contacts context`](https://molted.email/docs/cli/contacts.md) or the MCP `get_context` tool, which return the same timeline as `GET /v1/ops/timeline` together with the contact's suppression status and the workspace's open incidents.

| Route | What you get |
| - | - |
| `GET /v1/dashboard/summary` | `requestStatusCounts` and `attemptStatusCounts` (each a list of `{ status, count }`), plus a `deliverability` block. Needs a key with access to every mailbox: a mailbox-scoped key gets `403 mailbox_scope_denied`. |
| `GET /v1/dashboard/requests?limit=` | The most recent sends as `{ items, totalCount }`. Each item has `id`, `recipientEmail`, `templateId`, `status`, `deliveryStatus`, `policyReason`, `subject`, `preview` (the first 160 characters), `createdAt`, and `threadId` and `threadMessageCount` when the send is in a thread. `limit` defaults to 25 and is capped at 100. Sends in trashed or archived threads are left out. Add `mailboxId` to narrow it to one mailbox. |
| `GET /v1/dashboard/delivery-summary` | A list of `{ status, count }` with the count of sends in each delivery status. Sends with no delivery status yet are not counted. |
| `GET /v1/ops/timeline?email=` | One contact's latest 50 sends (`id`, `template_id`, `status`, `delivery_status`, `created_at`), latest 50 journey runs and latest 50 inbound messages. |

A mailbox-scoped key sees only the mailboxes it can read in `requests`, `delivery-summary` and `timeline`. See [API keys](https://molted.email/docs/api-keys.md).

> **Note:** For bounce and complaint numbers read `delivery-summary` or **Analytics**, not the `deliverability` block of `/v1/dashboard/summary`. That block counts records in a separate table that provider events do not write to, so its `bounceCount` and `complaintCount` read 0 for most workspaces.

## In the portal

Open a mailbox (press **⌘K** and type its name) to see its activity, and open a thread to see each message's delivery status under it (for example `Sent by your agent · delivered · opened 14:20`). **Analytics** shows sends, replies and the bounce rate over time, for all mailboxes or one.

### Automatic suppression

When a spam complaint or a hard bounce arrives, the recipient is added to your [suppression list](https://molted.email/docs/suppressions.md) straight away (reason `complaint` or `hard_bounce`), and a `suppression.created` event is published. Future sends to that address are blocked with reason `suppressed`. See [Errors](https://molted.email/docs/errors.md) for details.

A soft (temporary) bounce is retried instead, after 1 hour, 4 hours and 24 hours. Once an address has bounced 3 times within 30 days, it is suppressed with reason `soft_bounce` for 90 days. Only providers that report the bounce type (SES, Postmark) produce soft bounces; a bounce from Resend is always treated as a hard bounce. Each bounce and complaint also counts toward the mailbox's reputation, which can pause the mailbox, and toward the account's daily negative-signal budget (see [Check remaining quotas](https://molted.email/docs/policy-simulation.md#check-remaining-quotas)).
