MOLTED EMAIL

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
StatusDescription
queuedThe send request passed policy checks and is waiting to be sent.
acceptedThe email provider accepted the message for delivery.
sentThe message was handed off to the recipient's mail server.
deliveredThe recipient's server confirmed receipt.
deferredDelivery was temporarily delayed. The system will retry automatically.
bouncedThe recipient's server rejected the message. Most bounces are permanent (e.g., an invalid address); see Automatic suppression for soft bounces.
complainedThe recipient marked the email as spam.
failedDelivery 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

WhereWhat you get
molted timeline <requestId> / GET /v1/dashboard/request/:requestId/timelineEvery 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/:requestIdThe 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.
Webhooksdelivery.sent, delivery.delivered, delivery.bounced, delivery.complained, and suppression.created when an address is suppressed.
Event stream (molted listen)delivery.* for every event type above, plus send.queued, policy.blocked and the approval events.
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 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.

RouteWhat you get
GET /v1/dashboard/summaryrequestStatusCounts 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-summaryA 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.

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 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 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).