Guides

Handling delivery events

Polling the event log versus receiving webhooks, and what each event means.

A 200 from POST /messages means we accepted the message. Everything after that is an event.

Two ways to find out

Poll the event log for one-off checks, reconciliation, and anything historical:

curl "$MAILYTE_BASE/api/v1/email-logs/messages/<message_id>" \
  -H "Authorization: Bearer $MAILYTE_API_KEY"

Receive webhooks for anything ongoing. They arrive sooner and cost you no requests.

Use both: webhooks for the live path, the log to reconcile after an outage. The log is the durable record; webhooks are a notification layer on top of it.

The lifecycle

Event Means Act on it?
email.accepted We have the message and have queued it No — it is a receipt
email.delivered The receiving server took it Mark sent
email.deferred Temporarily refused; we will retry No — retrying yourself doubles the mail (except an inbox-full refusal, below)
email.bounced Permanently refused Yes — stop sending to this address
delivery.complaint Marked as spam by the recipient Yes, urgently
email.dropped We refused to send it Yes — usually a suppression
tracking.open A tracking pixel loaded Cautiously — see below
tracking.click A tracked link was followed Cautiously

Deferred is not a bounce

A deferral is the receiving server saying "not now". Greylisting, a full mailbox, a rate limit at their end. We retry on a schedule designed not to make it worse.

The mistake is treating a deferral as a failure and sending again from your side. Now two copies are in flight, the receiving server sees a sender that ignores backpressure, and a temporary problem becomes a reputation one.

One exception: a full inbox refused before sending. When a marketing SMTP key sends to a contact whose inbox we found full, we refuse that recipient with a 452 4.2.2 that names a date. Mailyte never queued the message for that person, so we do not retry it. Your app still has it: send it again after the date in the reply.

Your SMTP library always sees the 452, so rely on that. The refusal is also logged as email.deferred, with inbox is full (checked before sending) in its text (data.detail in the webhook), but only when no other recipient of that message had been accepted yet. See Checking contacts added through the API.

Bounced means stop

A bounce is permanent: the mailbox does not exist, or the domain does not accept mail. The address is added to your suppression list automatically, and further sends to it are refused.

That refusal is a feature. Repeatedly mailing addresses that do not exist is one of the clearest signals of a list that was not collected honestly, and receiving servers weight it heavily.

Complaints matter more than they look

A complaint is someone pressing "this is spam". The volume needed to damage a sending reputation is much smaller than most people expect — a fraction of a percent sustained is enough.

Treat one complaint as a permanent, immediate opt-out from everything, not just the campaign it came from.

Opens and clicks are unreliable, by nature

Not a limitation of our implementation. A limitation of the technique.

Open tracking works by embedding a remote image. So:

  • Privacy proxies fetch it on the recipient's behalf, often immediately and sometimes for mail the person never opens. Apple Mail Privacy Protection does this by default for a large share of consumer mail.
  • Many clients block remote images entirely, so a genuine open records nothing.
  • Security scanners follow links before the recipient sees the message, producing clicks nobody made.

The result is that opens over-count in one direction and under-count in the other, and clicks can be entirely machine-generated. We have seen this concretely: a 30-day window here once contained 499 opens and 36 clicks that were, on inspection, all our own infrastructure and no human at all.

Use them as a weak signal about a population, never as evidence about a person. "Did Ada read my email" is not a question this data can answer. "Is engagement on this segment falling" is.

Reconciling

After an outage, do not guess at what you missed:

curl "$MAILYTE_BASE/api/v1/email-logs?since=2026-09-17T00:00:00Z&event=bounced" \
  -H "Authorization: Bearer $MAILYTE_API_KEY"

Walk the log from your last known good timestamp and apply anything you have not already recorded. If your webhook handler is idempotent — and it should be — replaying events you already have is harmless.

Next

Suppressions and compliance — what happens to the addresses that bounce and complain.