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:
- Authentication and scope: bad credential → 401; key without
send→403 insufficient_scope. See Authentication. - Abuse pause check: a paused account →
403 forbiddenwith the message "This account is paused." - Rate limit: per-org token bucket →
429 rate_limit_exceeded. See Rate limits. - Idempotency: replay, conflict, or claim when an
Idempotency-Keyis present. See Idempotency. - Schema validation: violations →
422 validation_errorwithparamnaming the field. - Verified-domain gate: a live send while your org has no verified domain →
403 unverified_domain_required. See Verify a domain. - Quotas: spend cap, then monthly allowance, then daily cap → 429 with
spend_cap_exceeded,monthly_quota_exceeded, ordaily_quota_exceeded. - Payload preparation: a
scheduled_atmore than 90 days out →422 validation_error; afromaddress on an unverified domain →422 validation_errorwithparam: "from"; an unknowntemplate.id→404 not_found, and template rendering failures →422. Suppressed recipients are silently removed fromto,cc, andbccbefore 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:
| 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→deliveredBouncewith a permanent bounce type →bounced(transient bounces leave the status unchanged)Complaint→complainedRejectandRenderingFailure→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.
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.