Delivery and status

What happened to one email: the status field, the event timeline, and how to read a bounce.

A send returns an id. GET /v1/emails/:id turns that id into the answer to "did it arrive", without a support ticket and without a log grep.

GET /v1/emails/:id

Status and delivery events for one email.

Ask about one email
curl https://emails.sh/v1/emails/em_01J9X8Q2K7Y4RN3M \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"
200 OK
{
  "id": "em_01J9X8Q2K7Y4RN3M",
  "status": "delivered",
  "from": "Acme <hello@acme.com>",
  "to": ["ada@example.com"],
  "subject": "Your receipt from Acme",
  "tags": { "campaign": "receipt" },
  "test_mode": false,
  "created_at": "2026-07-28T09:14:01.882Z",
  "events": [
    { "type": "queued", "at": "2026-07-28T09:14:01.882Z" },
    { "type": "sent", "at": "2026-07-28T09:14:02.117Z" },
    { "type": "delivered", "at": "2026-07-28T09:14:04.903Z" }
  ]
}

test_mode is on every response. It is true when the email went to one of the reserved test addresses and therefore never left our servers, and false otherwise. It is never absent, because "did a person receive this" cannot be answered by a field that is sometimes missing. See /docs/sending.

Statuses

StatusWhat it means
queuedAccepted and waiting to go out. Normal for a second or two, and for as long as you asked if you set send_at.
scheduledBooked for a send_at in the future. Cancellable until it leaves.
sentHanded to the receiving mail server and accepted by it. This is the last thing SMTP tells us synchronously.
deliveredThe receiving server confirmed it took the message. As close to "it landed" as email gets: it does not mean anybody read it, and it does not rule out a spam folder.
bouncedRefused. The event carries the remote server's reason and whether it was permanent.
complainedThe recipient pressed the spam button. The address is suppressed automatically.
canceledA scheduled email was cancelled before it went.

Reading a bounce

A permanent bounce
{
  "type": "bounced",
  "at": "2026-07-28T09:14:06.220Z",
  "bounce_type": "permanent",
  "bounce_subtype": "no_such_mailbox",
  "diagnostic": "smtp; 550 5.1.1 <ada@example.com>: Recipient address rejected: User unknown"
}
permanent
The address does not exist or will never accept mail. It is suppressed on the workspace, and retrying is what gets a sender blocked. Fix the address, do not loop.
transient
A full mailbox, a greylist, or a server having a bad afternoon. We retry these for you. Nothing to do.
complaint
A spam report, which arrives hours or days later through a feedback loop. The address is suppressed and should stay that way.

Do not poll

Polling this endpoint in a loop is a way to spend your 600 requests a minute on nothing. Delivery takes seconds to minutes and complaints take days, so subscribe to webhooks and let the events come to you. GET /v1/emails/:id is for the moment somebody asks about one specific message.

emails.get

Status and delivery events for one email: when it was accepted, when the receiving server took it, and the bounce or complaint if there was one.

Arguments

id string required
The id a send returned.

Returns{ id, status, to, subject, created_at, events[] }