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.
/v1/emails/:idStatus and delivery events for one email.
curl https://emails.sh/v1/emails/em_01J9X8Q2K7Y4RN3M \
-H "Authorization: Bearer $EMAILSSH_API_KEY"{
"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
| Status | What it means |
|---|---|
queued | Accepted and waiting to go out. Normal for a second or two, and for as long as you asked if you set send_at. |
scheduled | Booked for a send_at in the future. Cancellable until it leaves. |
sent | Handed to the receiving mail server and accepted by it. This is the last thing SMTP tells us synchronously. |
delivered | The 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. |
bounced | Refused. The event carries the remote server's reason and whether it was permanent. |
complained | The recipient pressed the spam button. The address is suppressed automatically. |
canceled | A scheduled email was cancelled before it went. |
Reading a 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
idstringrequired- The id a send returned.
Returns{ id, status, to, subject, created_at, events[] }