Webhook events

Every event Mailyte will send to your endpoint, what it means, and whether it needs you to do anything.

Registering an endpoint, verifying signatures and surviving retries are covered in Receiving webhooks. This page is the catalogue.

The envelope

Every delivery has the same outer shape. Payload fields are snake_case throughout.

Delivery
{
  "id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
  "event": "email.bounced",
  "created_at": "2026-09-17T10:04:11.000000Z",
  "account": {
    "organization_id": "01JBT8XQ2M9WYC3K4F6R7S8T9W"
  },
  "data": {
    "message_id": "<01JBT8XQ2M@yourdomain.com>",
    "recipient": "ada@example.com",
    "domain": "yourdomain.com",
    "dsn": "5.1.1",
    "reason": "Mailbox does not exist",
    "provider": "Gmail"
  }
}

id is stable across retries — key your idempotency on it. provider names the receiving platform where we can identify it, which makes "are we only bouncing at Microsoft" a question you can answer.

Events

  • email.acceptedinfo

    Fires when We have taken the message and queued it for delivery.

    What to do Nothing. It is a receipt, not an outcome.

  • email.deliveredinfo

    Fires when The receiving server accepted the message.

    What to do Mark as sent. Note this means accepted by the server, not read by a person, and not necessarily in the inbox rather than the spam folder.

  • email.deferredwarning

    Fires when Temporarily refused — greylisting, a full mailbox, rate limiting at their end. We will retry.

    What to do Nothing. Sending again yourself puts two copies in flight and makes a temporary problem a reputation one. One exception: if the text contains "inbox is full (checked before sending)", your marketing key sent to a contact whose inbox is full. Mailyte never queued that message, so it is yours: send it again after the date in the text.

  • email.bouncederror

    Fires when Permanently refused. No such mailbox, or the domain does not accept mail.

    What to do Stop sending to this address. It is added to your suppression list automatically.

  • email.rejectederror

    Fires when The receiving server refused the message outright, often for policy or content reasons. Also sent when we drop a marketing message before sending because none of its recipients can take it, for example an inbox that is full (checked before sending).

    What to do If `not_sent_reason` is set, we refused it ourselves and nothing left our servers: the address does not exist, the domain is misspelt (see `suggested_domain`), it is on your suppression list, and so on. Remove or fix the address — see Addresses we do not send to. Otherwise read the reason: repeated rejections from one provider point at a content or authentication problem rather than a bad address.

  • email.droppederror

    Fires when We refused to send it — the recipient is suppressed, or the address does not exist. `not_sent_reason` says which.

    What to do Check your suppression list, or remove an address that does not exist. This event exists so a refused send leaves a trace instead of vanishing.

  • delivery.complaintwarning

    Fires when The recipient marked the message as spam, and their provider sent us a feedback-loop report. This is the complaint event — there is no email.complained.

    What to do Treat as a permanent opt-out from everything, immediately. Complaints damage deliverability at volumes far below what most people expect.

  • spam.reportedwarning

    Fires when Our own filtering classified an inbound message as spam.

    What to do Only relevant if you are receiving mail through Mailyte.

  • email.inboundinfo

    Fires when A message arrived at one of your mailboxes.

    What to do Process it. This is the event to build an inbound pipeline on — it is distinct from email.delivered, which is about mail you sent.

  • tracking.openinfo

    Fires when A tracking pixel in an HTML message was loaded.

    What to do Treat with caution — see below.

  • tracking.clickinfo

    Fires when A tracked link was followed.

    What to do Treat with caution — see below.

  • tracking.unsubscribewarning

    Fires when The recipient used an unsubscribe link.

    What to do Honour it everywhere, not just in the campaign it came from. They are suppressed automatically.

  • storage.quota.warningwarning

    Fires when A mailbox is approaching its quota.

    What to do Raise the quota or prompt the holder to clear space, before mail starts bouncing.

  • storage.quota.exceedederror

    Fires when A mailbox is full and is refusing new mail.

    What to do Raise the quota. Incoming mail is being rejected right now.

Contacts and checks

Three events are about your account, not about a message. They carry no domain, sender or subject, so a webhook's filters never hold them back. They are sent whether or not your account's Bounce protection switch is on. The envelope is the usual one; the samples below are its data. The whole story is in Checking contacts added through the API.

  • contact.checkedinfo

    Fires when A contact's address was checked and the answer changed. Once per change. An answer the checker could not confirm yet is asked again later, so the same contact can arrive again.

    What to do Update your copy of the contact. invalid means the address does not exist; risky with reason inbox_full means the mailbox is full, and email_full_until says until when marketing to them is held.

  • checks.running_lowinfo

    Fires when The checks will last under a week at the pace of the last 7 days (trigger forecast), or 80% of this month's plan checks are used and none are bought (trigger plan_80). forecast_days is null when there is no recent use to forecast from. At most once per cycle: a new month, a purchase or checks we add start the next one.

    What to do Tell whoever buys checks. New contacts keep being checked until the checks run out.

  • checks.ran_outinfo

    Fires when A check could not run because no checks were left: a new contact was added as not_checked, or a one-address check (POST /verifications/check) answered 402. At most once per cycle.

    What to do Buy checks: buy_path is the dashboard page that buys the ones we suggest, or /dashboard/billing?from=api_warning when no pack is on sale or the amount needs a quote. Contacts added meanwhile can be checked later with POST /verifications/contacts.

contact.checked
{
  "contact_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
  "email": "ada@example.com",
  "email_status": "risky",
  "reason": "inbox_full",
  "suggestion": null,
  "checked_at": "2026-10-01T09:12:44+00:00",
  "email_full_until": "2026-10-08T09:12:40+00:00"
}
The data of each event

Inbound mail is its own event

Mail arriving at your mailboxes fires email.inbound. Mail you sent reaching its destination fires email.delivered.

They were once conflated, and inbound mail was emitted as email.delivered — which made a "received" count that sat at zero for ever while mail was plainly arriving. If you integrated against the old behaviour, this is the change to check.

Opens and clicks are unreliable

Not a limitation of our implementation — a limitation of the technique. Open tracking needs a remote image to load, and:

  • privacy proxies fetch it on the recipient's behalf, sometimes for mail they never open;
  • many clients block remote images, so a real open records nothing;
  • security scanners follow links, producing clicks nobody made.

We have measured this here: a 30-day window once contained 499 opens and 36 clicks that were, on inspection, entirely our own infrastructure and no human at all.

Use them as a weak signal about a population. Never as evidence about a person.