Mailfully logo

How sending works

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

Sending is asynchronous. POST /v1/emails validates and stores your message, queues it for sending, 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>"
  }'
{
  "object": "email",
  "id": "01J9ZC3AB8XQ4RW2N7VKT5EMHD",
  "status": "queued",
  "type": "transactional",
  "to": ["you@yourcompany.com"],
  "cc": [],
  "bcc": [],
  "from": "orders@mail.acme.com",
  "subject": "Your order shipped",
  "scheduled_at": null,
  "created_at": "2026-09-11T14:15:00.000Z"
}

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. The rest of the body is the message as accepted — what you sent, echoed back, so you can log the accept without a second request. Bodies (html, text) are never returned.

status is queued normally. If every to recipient is suppressed, the 202 body has status: "canceled" and nothing is sent. Check status in the response.

Why 202

A 202 means Mailfully stored the message and will try to deliver it. It says nothing yet about the recipient's server. The message is queued once, or scheduled when scheduled_at is set, and the delivery outcome arrives later as status changes and events.

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.
  2. Pause check: a paused account → 403 forbidden with the message "This account is paused."
  3. Rate limit: per-org request limit → 429 rate_limit_exceeded. See Rate limits.
  4. Idempotency: replay, conflict, or in-flight request when an Idempotency-Key is present. See Idempotency.
  5. Schema validation: violations → 422 validation_error with param naming the field. For attachments, param names the offending entry by index, as in attachments.0.content.
  6. Account gates: a deleted account → 403 account_deleted, in both environments. Live only: a rejected account → 403 account_rejected; an account whose setup was never completed → 403 account_suspended.
  7. Verified-domain gate: a live send while your org has no verified domain → 403 unverified_domain_required. See Verify a domain.
  8. Marketing compliance: type: "marketing" sends only → 422 marketing_missing_postal_address, 422 marketing_single_recipient, 422 recipient_unsubscribed, or, live only, 429 marketing_quota_exceeded when the separate marketing allowance is spent.
  9. Quotas (live only): spend cap, then monthly allowance → 429 with spend_cap_exceeded or monthly_quota_exceeded.
  10. Daily limit: → 429 daily_quota_exceeded, 429 account_under_review for a new account awaiting review, or 429 volume_limit_reached when the account reached its daily sending volume limit. A live send from an account with a past-due payment → 402 payment_past_due.
  11. Payload preparation: a scheduled_at more than 90 days out → 422 validation_error; a live send whose from address is 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 stored.

Every quota above counts recipients, not messages: one message to five people consumes five units. An address repeated across to, cc and bcc is sent and counted once. Quotas run before payload preparation, so a quota-exhausted request fails with a 429 even when its template id or from address is also bad. A denied request stores and queues nothing, and a denied batch consumes none of the allowance.

A marketing send that breaks a compliance rule is refused with a 422 before it uses any daily allowance.

Each 429 above is recorded and readable from GET /v1/refusals, with the reason and the message and recipient counts. The per-second rate_limit_exceeded is not recorded: it refuses before these gates run.

Test mode skips approval, the setup-suspension check, the verified-domain check, the from-domain check, the past-due pause, and the monthly, spend and marketing allowances. A paused or deleted account is still refused, a marketing send still needs exactly one recipient and your postal address, and the daily limit still applies, with its own limit for test mode (see Rate limits).

The lifecycle

A message moves through ten statuses:

StatusMeaningTerminal
queuedAccepted, or scheduled for laterNo
heldWaiting for review before sending. It sends if released, and can be canceled.No
sendingBeing sent nowNo
sentOur sending servers accepted the message; delivery outcome pendingNo
deliveredThe recipient's mail server accepted the messageNo
bouncedPermanent bounce; transient bounces do not change statusYes
complainedThe recipient marked the message as spamYes
failedThe message was rejected at submission, rendering failed, or every remaining recipient was suppressed before sendYes
canceledA scheduled send was canceled, every to recipient was suppressed at accept, or the message was dropped before sendingYes
reconcile_reviewThe send outcome is unclear and needs a person to check it. Contact contact@mailfully.com with the message id.No

The happy path is queued → sending → sent → delivered. Only a queued or held 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

Changes after sent come from delivery events. GET /v1/emails/{id}/events returns these types in lowercase:

  • 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 delivery 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.