---
title: How sending works
description: Why sends return 202, which checks run synchronously, and how a message moves through its nine-status lifecycle.
---

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

Sending is asynchronous. `POST /v1/emails` validates and persists your message, enqueues a send job, and returns before any email moves. To try it, swap `you@yourcompany.com` for an inbox you own:

```bash
curl -X POST https://api.mailfully.com/v1/emails \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "orders@mail.acme.com",
    "to": "you@yourcompany.com",
    "subject": "Your order shipped",
    "html": "<p>Your order is on its way.</p>"
  }'
```

```json
{ "id": "01J9ZC3AB8XQ4RW2N7VKT5EMHD" }
```

The response is `202 Accepted` with a `Location: /v1/emails/{id}` header. The `id` is a bare 26-character ULID with no prefix. Keep it; every status check keys off it.

## Why 202

A 202 means accepted, not delivered. By the time you receive it, Mailfully has validated the request, persisted a message row, and enqueued exactly one send job (or scheduled it when `scheduled_at` is set). A worker then hands the message to Amazon SES, and the delivery outcome arrives later as status transitions and events. A 202 says nothing about whether the recipient's mailbox will accept the message.

## What runs synchronously

Everything that can be checked before queueing is checked before the 202, in this order. The first failure ends the request:

1. **Authentication and scope**: bad credential → 401; key without `send` → `403 insufficient_scope`. See [Authentication](/concepts/authentication).
2. **Abuse pause check**: a paused account → `403 forbidden` with the message "This account is paused."
3. **Rate limit**: per-org token bucket → `429 rate_limit_exceeded`. See [Rate limits](/concepts/rate-limits).
4. **Idempotency**: replay, conflict, or claim when an `Idempotency-Key` is present. See [Idempotency](/concepts/idempotency).
5. **Schema validation**: violations → `422 validation_error` with `param` naming the field.
6. **Verified-domain gate**: a live send while your org has no verified domain → `403 unverified_domain_required`. See [Verify a domain](/guides/verify-a-domain).
7. **Quotas**: spend cap, then monthly allowance, then daily cap → 429 with `spend_cap_exceeded`, `monthly_quota_exceeded`, or `daily_quota_exceeded`.
8. **Payload preparation**: a `scheduled_at` more than 90 days out → `422 validation_error`; a `from` address on an unverified domain → `422 validation_error` with `param: "from"`; an unknown `template.id` → `404 not_found`, and template rendering failures → `422`. Suppressed recipients are silently removed from `to`, `cc`, and `bcc` before the message is persisted.

Because quotas run before payload preparation, a quota-exhausted request fails with a 429 even when its template id or `from` address is also bad. A denied request persists and enqueues nothing, with one caveat: a [batch](/guides/batch-sending) denied at the daily cap has already consumed daily-cap allowance for the units checked before the denial.

<Note>
If every `to` recipient is suppressed, the request still returns 202 with an id, but the message is persisted with status `canceled` and never enqueued. If you need to know a send will actually go out, check the message after accepting, or watch for the absence of subsequent events.
</Note>

## The lifecycle

A message moves through nine statuses:

| Status | Meaning | Terminal |
|---|---|---|
| `queued` | Accepted and waiting for a worker, or scheduled for later | No |
| `sending` | A worker claimed the message and is submitting it to SES | No |
| `sent` | SES accepted the message; delivery outcome pending | No |
| `delivered` | SES reported successful delivery | No |
| `bounced` | Permanent bounce — transient bounces do not change status | Yes |
| `complained` | The recipient marked the message as spam | Yes |
| `failed` | SES rejected the message, rendering failed, or every remaining recipient was suppressed before send | Yes |
| `canceled` | A scheduled send was canceled, all recipients were suppressed at accept, or the message was dropped before sending | Yes |
| `reconcile_review` | A stuck send parked for operator review after repeated re-enqueue attempts | No |

The happy path is `queued → sending → sent → delivered`. Only a `queued` message can be canceled. `delivered` is not terminal: a recipient can mark mail as spam after delivery, so a delivered message can still transition to `complained` — or to `bounced` when a permanent bounce report arrives late.

## Where events come from

Transitions after `sent` are driven by asynchronous event notifications from Amazon SES:

- `Delivery` → `delivered`
- `Bounce` with a permanent bounce type → `bounced` (transient bounces leave the status unchanged)
- `Complaint` → `complained`
- `Reject` and `RenderingFailure` → `failed`

`Send`, `Open`, `Click`, and `DeliveryDelay` events appear in the message's event timeline but never change the status.

## Tracking delivery

**Poll.** `GET /v1/emails/{id}` returns the message detail. Its `last_event` field mirrors the lifecycle, falling back to the status until the first SES event lands. `GET /v1/emails/{id}/events` returns the full timeline, oldest first.

```bash
curl https://api.mailfully.com/v1/emails/01J9ZC3AB8XQ4RW2N7VKT5EMHD \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
```

**Webhooks.** For anything beyond a handful of messages, register a webhook endpoint and let Mailfully push delivery, bounce, and complaint events to you instead. See [Webhooks](/guides/webhooks).

<Card title="Send an email" href="/api-reference/emails/send">
  The full request and response contract for POST /v1/emails.
</Card>
