How sending works

Why sends return 202, which checks run synchronously, and how a message moves through its nine-status lifecycle.

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:

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>"
  }'
{ "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 send403 insufficient_scope. See 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.
  4. Idempotency: replay, conflict, or claim when an Idempotency-Key is present. See 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.
  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.id404 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 denied at the daily cap has already consumed daily-cap allowance for the units checked before the denial.

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.

The lifecycle

A message moves through nine statuses:

StatusMeaningTerminal
queuedAccepted and waiting for a worker, or scheduled for laterNo
sendingA worker claimed the message and is submitting it to SESNo
sentSES accepted the message; delivery outcome pendingNo
deliveredSES reported successful deliveryNo
bouncedPermanent bounce — transient bounces do not change statusYes
complainedThe recipient marked the message as spamYes
failedSES rejected the message, rendering failed, or every remaining recipient was suppressed before sendYes
canceledA scheduled send was canceled, all recipients were suppressed at accept, or the message was dropped before sendingYes
reconcile_reviewA stuck send parked for operator review after repeated re-enqueue attemptsNo

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:

  • Deliverydelivered
  • Bounce with a permanent bounce type → bounced (transient bounces leave the status unchanged)
  • Complaintcomplained
  • Reject and RenderingFailurefailed

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.

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.

Send an email

The full request and response contract for POST /v1/emails.