# 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:
```bash
curl https://emails.sh/v1/emails/em_01J9X8Q2K7Y4RN3M \
  -H "Authorization: Bearer $EMAILSSH_API_KEY"
```

200 OK:
```json
{
  "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

A permanent bounce:
```json
{
  "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`

`{ id: string }`

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.

| Parameter | Type | Required |
| --- | --- | --- |
| id | `string` | yes |

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

Message bodies are retained for 30 days and the event timeline for 12 months. After that the id still resolves and the body does not.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
