Webhook Events
Complete reference of all webhook event types and their payloads
MailLand's event catalog defines 21 event types across 6 categories (2 of the 21 are platform notices you cannot subscribe to). Each event is delivered as a signed HTTP POST to your webhook endpoint.
Not every catalogued event fires yet. Nine events are live and subscribable today:
message.delivered, message.deferred, message.bounced, message.failed,
message.complained, message.outcome_unknown, message.retried, message.unsubscribed and inbound.received. The rest are documented, reserved, and not currently accepted in a
subscription: the API returns 400 if you include one. The catalog endpoint reports this per event
with availableNow and customerSelectable, so read those flags rather than hard-coding a list.
Event Catalog
Retrieve the full event catalog programmatically:
GET https://api.mail.land/v1/webhooks/catalogcurl -X GET https://api.mail.land/v1/webhooks/catalog{
"version": 1,
"products": [
{ "id": "transactional", "name": "Transactional Email", "isActive": true, "comingSoon": false },
{ "id": "marketing", "name": "Marketing Email", "isActive": true, "comingSoon": false },
{ "id": "inbound", "name": "Inbound Email", "isActive": true, "comingSoon": false },
{ "id": "system", "name": "System Events", "isActive": true, "comingSoon": false },
{ "id": "validation", "name": "Email Validation", "isActive": false, "comingSoon": true }
],
"categories": [
{
"id": "message",
"name": "Message Events",
"events": [
{ "id": "message.sent", "name": "Message Sent", "products": ["transactional", "marketing"] },
{ "id": "message.delivered", "name": "Message Delivered", "products": ["transactional", "marketing"] }
]
}
]
}This endpoint is public (no authentication required) and always returns the latest catalog version.
Standard Payload Structure
Every webhook delivery includes a consistent outer structure:
{
"event": "message.delivered",
"event_id": "evt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
"timestamp": "2026-02-17T12:34:56.789Z",
"occurred_at": "2026-02-17T12:34:56.120Z",
"schema_version": 1,
"data": {
// Event-specific fields
}
}| Field | Type | Description |
|---|---|---|
event | string | Event type identifier |
event_id | string | Unique event ID for deduplication (format: evt_{32chars}) |
timestamp | datetime | When the webhook was queued (UTC), not when the underlying event occurred. See data.timestamp for that. |
occurred_at | datetime | When the underlying event occurred (UTC); for message events the same instant as data.timestamp |
schema_version | integer | Payload schema version of this event type (1 today); mirrored in X-MLND-Event-Version. See Versioning |
data | object | Event-specific payload |
Every message event's data also carries status_version, status, previous_status,
complaint_status and reason, see Status versions.
Status versions
Every message event carries data.status_version, data.status, data.previous_status,
data.complaint_status and data.reason. status_version increases by one each time MailLand sends an
event for a change in the message's status, its reason or its complaint status.
Two exceptions:
- A message starts at
status_version0 while it isqueuedorsent. The step fromqueuedtosentsends no event and does not change the version. - Several
message.deferredevents can share onestatus_version: a repeated delay does not change the status.
Webhooks can arrive out of order and can be retried, so apply them by status_version, never by
arrival order.
A status can change after it was final. MailLand reports what it knows when it knows it. When it
learns more later, it sends the newer status with a higher status_version:
- a delivered message can later bounce, when the recipient's server rejects it afterwards (an out-of-band bounce);
- after an internal failure, or after MailLand's mail server recovers, a delivery or a refusal can be
reported late. A
failedmessage can then becomebouncedordelivered, abouncedmessage can becomedelivered, and anunknownmessage can become any final status.
A message never goes from delivered to failed. MailLand counts these corrections.
| Field | Type | Description |
|---|---|---|
status_version | integer | The version of the message's status this event describes. |
status | string | The message's status at that version: queued, sent, deferred, unknown, delivered, bounced or failed. |
previous_status | string or null | The status before this change; null before the first change. |
complaint_status | string | none or complained. |
reason | string or null | The bounce or failure reason for bounced, failed and unknown; otherwise null. |
Retry deadlines
MailLand retries a deferred message until its retry deadline: about 5 hours after it was first
handed to the delivery queue for transactional mail, and about 25 hours for marketing mail. After the
deadline MailLand does not queue the message for any new delivery attempt. A copy that was already
waiting for a connection at the deadline can still be attempted after it. These are retry deadlines,
not promises about when you receive the final event. You receive each event once the outcome
reaches MailLand; that delay is not yet measured, and it can be longer during an internal outage. A
message MailLand could not hand to its delivery queue within 24 hours is ended as expired.
If you keep a message's current status from webhooks (action required)
This is a behaviour change for existing integrations. An integration that treats message.failed or
message.delivered as permanent, and ignores later events, can keep a wrong status after a
correction. Event names that already exist now have new possible sequences.
- Subscribe to every message event:
message.delivered,message.deferred,message.bounced,message.failed,message.complained,message.outcome_unknownandmessage.retried. Only then do the events you receive contain every status change. - Store
status_versionwith the status. Apply an event only if itsstatus_versionis higher than the one you stored; amessage.deferredwith an equal version changes nothing. - Keep processing events after
message.failed,message.bouncedormessage.delivered: a later event with a higherstatus_versionreplaces the earlier status. - Once you have received every event up to the API's
status_version, your status equals the API's. The API can be ahead of the webhooks you have received so far, and atstatus_version0 it can showsentwith no event.
If you subscribe to only some message events, the highest status_version you received is
not necessarily the current status: an event type you did not subscribe to may have changed it.
Read the current status from the API, for example for every message whose last event you received
is message.failed, message.bounced or message.outcome_unknown, and reconcile.
If you cannot change your integration, poll the API for the status of messages you care about. Integrations that only count events, or that only show a history, need no change.
Message Events
message.sent
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "message.sent",
"event_id": "evt_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
"timestamp": "2026-02-17T12:34:56.789Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T12:34:56.789Z"
}
}message.delivered
Fired when the email is successfully delivered to the recipient's mail server.
This event can also arrive as a correction after an earlier final status for the same message. See Status versions.
{
"event": "message.delivered",
"event_id": "evt_b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"timestamp": "2026-02-17T12:34:58.123Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T12:34:58.123Z",
"smtp_response": "OK",
"status_version": 1,
"status": "delivered",
"previous_status": "sent",
"complaint_status": "none",
"reason": null
}
}message.deferred
Fired when delivery is temporarily delayed. MailLand will retry automatically.
A message can be deferred several times. The first deferral increases status_version; repeated
deferrals carry the same status_version.
{
"event": "message.deferred",
"event_id": "evt_c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7",
"timestamp": "2026-02-17T12:35:00.000Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T12:35:00.000Z",
"smtp_response": "Try again later",
"bounce_reason": "TransientFailure"
}
}message.bounced
Fired when an email bounces (hard or soft).
This event can also arrive as a correction after an earlier final status for the same message. See Status versions.
{
"event": "message.bounced",
"event_id": "evt_d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8",
"timestamp": "2026-02-17T12:35:01.000Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T12:35:01.000Z",
"bounce_type": "hard",
"bounce_reason": "InvalidRecipient",
"smtp_response": "The email account that you tried to reach does not exist.",
"status_version": 1,
"status": "bounced",
"previous_status": "sent",
"complaint_status": "none",
"reason": "InvalidRecipient"
}
}bounce_reason is the classification token MailLand assigns to the bounce (e.g. InvalidRecipient,
QuotaIssues, SpamBlock), not the raw SMTP text, use smtp_response for that. bounce_type is
one of hard, soft, blocked or unknown.
smtp_response is the receiving server's reply text, without its numeric reply and enhanced status
codes. bounce_type says what MailLand did about the address: hard, suppressed for all mail;
soft, suppressed for marketing mail for 30 days; blocked, the refusal was about the sending, not
the address (for example spam filtering, sender authentication, reputation or a receiver's policy), so
nothing is suppressed; unknown, no classification, handled like soft.
message.failed
Fired when MailLand ends a message on conclusive evidence that it was not delivered, for a reason on
MailLand's side rather than the recipient's. In the cases described under
Status versions, a later message.delivered or message.bounced can follow with a
higher status_version. The message's status becomes Failed, not Bounced, and reason names the
case:
| Reason | What happened |
|---|---|
dkim_revoked | The message was waiting in MailLand's delivery queue, signed with a DKIM key that MailLand then revoked in an emergency for the sender domain (for example after a suspected key compromise). MailLand never delivers or re-signs such a message: it expires at its next retry, within about 30 minutes for transactional mail and 4 hours for marketing mail. |
admin_bounce | A MailLand operator removed the message from MailLand's delivery queue. |
dkim_key_revoked | The sender domain is under an emergency DKIM hold, so MailLand refused the message before it entered the delivery queue. |
inject_failed | MailLand's delivery queue refused the message outright, so it was never queued for delivery. |
expired | MailLand accepted the message but did not hand it to its delivery queue within 24 hours, so it ended it without sending it. |
template_variable_missing | The message used a {{name}} placeholder that had no value for this recipient, so MailLand did not send it with the literal placeholder in it. |
- The recipient is not suppressed and the failure does not count toward your bounce rate.
- A message produces at most one
message.failed. It is the last event for that message unless MailLand later receives a delivery or a refusal; then amessage.deliveredormessage.bouncedfollows with a higherstatus_version.
A refusal by the receiving mail server is reported as message.bounced, never as message.failed.
{
"event": "message.failed",
"event_id": "evt_e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
"timestamp": "2026-09-28T02:00:19.1047332Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-09-28T02:00:18.2522997Z",
"reason": "dkim_revoked",
"smtp_response": "Next delivery time PT3.028008S > PT1S",
"status_version": 1,
"status": "failed",
"previous_status": "sent",
"complaint_status": "none"
}
}| Field | Description |
|---|---|
reason | Why MailLand ended the message: one of the values above. Treat any other value as a new MailLand-side reason. |
smtp_response | Text from MailLand's own delivery queue where there is some (dkim_revoked, admin_bounce, inject_failed), otherwise null. Informational only: it is never a reply from the recipient's server. |
message.outcome_unknown
Fired when MailLand cannot determine whether a message reached its delivery queue, for example because
the hand-off was interrupted and could not be established afterwards. It is sent some time after the
message's retry deadline, currently 7 days, if no evidence has arrived by then. This is not a final
status. The status becomes unknown. If MailLand later learns the outcome, you receive
message.delivered, message.bounced or message.failed with a higher status_version.
{
"event": "message.outcome_unknown",
"event_id": "evt_0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d",
"timestamp": "2026-10-08T17:05:00.000Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-10-08T17:05:00.000Z",
"status_version": 1,
"status": "unknown",
"previous_status": "queued",
"complaint_status": "none",
"reason": "handoff_undetermined"
}
}message.retried
Fired when you retry a bounced or failed message and MailLand queues it again. The status becomes
queued and status_version increases; the retried copy's own events follow with higher versions. This
is not a final status.
{
"event": "message.retried",
"event_id": "evt_1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e",
"timestamp": "2026-10-01T18:00:00.000Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-10-01T18:00:00.000Z",
"attempt": 1,
"status_version": 2,
"status": "queued",
"previous_status": "bounced",
"complaint_status": "none",
"reason": null
}
}Engagement Events
message.opened and message.clicked are catalogued but not customer-selectable and not emitted today. You cannot subscribe to them: attempting to select them on create/update is rejected with 400 "One or more invalid events selected". The examples below are illustrative only.
message.opened
Illustrative: not available.
{
"event": "message.opened",
"event_id": "evt_f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"timestamp": "2026-02-17T13:15:00.000Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T13:15:00.000Z"
}
}message.clicked
Illustrative: not available.
{
"event": "message.clicked",
"event_id": "evt_g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1",
"timestamp": "2026-02-17T13:16:00.000Z",
"data": {
"message_id": "t_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T13:16:00.000Z"
}
}message.unsubscribed
Fired when a recipient unsubscribes from a message you sent: through its List-Unsubscribe header or
mailto: address, its one-click POST, its unsubscribe link, or the preference page. It is not sent for
unsubscribes you make yourself through the API or the dashboard, and not for spam complaints
(message.complained). It is sent once per message, in the same moment the recipient is opted out, to every
endpoint subscribed to it. Like every message event, data also carries the message's current status fields
(see Status versions).
{
"event": "message.unsubscribed",
"event_id": "evt_h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2",
"timestamp": "2026-02-17T14:00:00.000Z",
"data": {
"message_id": "m_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T14:00:00.000Z",
"status_version": 3,
"status": "delivered",
"previous_status": "queued",
"complaint_status": "none",
"reason": null
}
}Suppression Events
message.complained
Fired when a recipient marks the email as spam (ISP feedback loop).
{
"event": "message.complained",
"event_id": "evt_i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3",
"timestamp": "2026-02-17T15:00:00.000Z",
"data": {
"message_id": "m_a1b2c3d4e5f6",
"recipient": "[email protected]",
"timestamp": "2026-02-17T15:00:00.000Z",
"feedback_type": "abuse"
}
}Complaint rates above 0.1% can damage your sender reputation. Monitor message.complained events closely and ensure you only send to opted-in recipients.
suppression.added
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "suppression.added",
"event_id": "evt_j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4",
"timestamp": "2026-02-17T15:00:01.000Z",
"data": {
"email": "[email protected]",
"type": "complaint",
"scope": "global",
"reason": "Spam complaint via ISP feedback loop"
}
}suppression.removed
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "suppression.removed",
"event_id": "evt_k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5",
"timestamp": "2026-02-17T16:00:00.000Z",
"data": {
"email": "[email protected]",
"type": "manual",
"scope": "global"
}
}Inbound Events
inbound.received
Fired when an inbound email is received on one of your domains.
This event uses the standard envelope, like every other event on this page: event_id equals
X-MLND-Event-Id and is stable across retries, occurred_at is when the email was received, and the
inbound fields are under data. Each attachment's id is stable across retries; its download_url
is new on every attempt. Field reference: Inbound Webhooks.
{
"event": "inbound.received",
"event_id": "evt_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"timestamp": "2026-02-17T12:00:00.000Z",
"occurred_at": "2026-02-17T12:00:00.000Z",
"schema_version": 1,
"data": {
"idempotency_key": "r7n2k5x9m1p4w6_s5m3x8k1p7n4w2",
"email": {
"id": "r7n2k5x9m1p4w6",
"received_at": "2026-02-17T12:00:00.000Z",
"size_bytes": 4521,
"envelope": {
"from": "[email protected]",
"to": ["[email protected]"]
},
"headers": {
"from": { "address": "[email protected]", "name": "Jane Doe" },
"to": [{ "address": "[email protected]" }],
"subject": "Help with my order",
"message_id": "<[email protected]>",
"date": "2026-02-17T11:59:50.000Z"
},
"body": {
"text_preview": "Hi, I need help with...",
"has_text": true,
"has_html": true
},
"attachments": [],
"authentication": {
"spf": { "result": "pass" },
"dkim": { "result": "pass" },
"dmarc": { "result": "pass" }
},
"spam": {
"score": 0.1,
"is_spam": false,
"reason": null
},
"source": {
"ip": "203.0.113.10",
"hostname": "mail.external.com"
}
},
"matched_route": {
"id": "s5m3x8k1p7n4w2",
"name": "Support inbox"
}
}
}Account Events
account.webhook_paused
Not emitted. Auto-pause is real and happens as described in Retry Logic,
but it is surfaced through the webhook's own pausedAt / pausedReason fields and an in-app
notification, not as a webhook event. Sending "your endpoint stopped working" to a webhook endpoint
is unreliable by construction, and no subscription can currently contain this event anyway.
Fired when a webhook is automatically paused due to consecutive failures.
{
"event": "account.webhook_paused",
"event_id": "evt_m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7",
"timestamp": "2026-02-17T18:00:00.000Z",
"data": {
"webhook_id": "z8m3p1k6x4w2n7",
"webhook_name": "Production Events",
"consecutive_failures": 10,
"last_error": "Connection refused"
}
}account.domain_verified
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "account.domain_verified",
"event_id": "evt_n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8",
"timestamp": "2026-02-17T10:15:00.000Z",
"data": {
"domain_id": "q3z8v1k6m9p2x5",
"domain": "yourdomain.com"
}
}account.domain_failed
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "account.domain_failed",
"event_id": "evt_o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9",
"timestamp": "2026-02-17T10:15:00.000Z",
"data": {
"domain_id": "q3z8v1k6m9p2x5",
"domain": "yourdomain.com",
"reason": "DKIM record not found for selector mlnd"
}
}account.quota_warning
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "account.quota_warning",
"event_id": "evt_p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0",
"timestamp": "2026-02-17T20:00:00.000Z",
"data": {
"used": 45000,
"limit": 50000,
"percentage": 90
}
}account.quota_exceeded
Illustrative: not emitted yet. This event is catalogued but has no producer today.
{
"event": "account.quota_exceeded",
"event_id": "evt_q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1",
"timestamp": "2026-02-17T22:00:00.000Z",
"data": {
"used": 50000,
"limit": 50000,
"percentage": 100
}
}Event Summary
"Available" is the catalog's availableNow flag: whether anything emits the event today. Only
available events may be included in a subscription.
| Event | Available | Products | Domain-Scoped | Description |
|---|---|---|---|---|
message.delivered | Yes | transactional, marketing | Yes | Successfully delivered |
message.deferred | Yes | transactional, marketing | Yes | Temporarily delayed |
message.bounced | Yes | transactional, marketing | Yes | Hard or soft bounce |
message.failed | Yes | transactional, marketing | Yes | Ended by MailLand on conclusive evidence, for a reason on MailLand's side; can be followed by a correction (see Status versions) |
message.outcome_unknown | Yes | transactional, marketing | Yes | MailLand could not determine whether the message reached its delivery queue; not final |
message.retried | Yes | transactional, marketing | Yes | A bounced or failed message was retried and is queued again; not final |
message.complained | Yes | transactional, marketing | Yes | Marked as spam |
inbound.received | Yes | inbound | Yes | Inbound email received |
message.sent | Not yet | transactional, marketing | Yes | Email accepted and queued |
message.opened | Not yet | transactional, marketing | Yes | Tracking pixel loaded |
message.clicked | Not yet | transactional, marketing | Yes | Link clicked |
message.unsubscribed | Yes | transactional, marketing | Yes | Recipient unsubscribed |
suppression.added | Not yet | transactional, marketing | No | Added to suppression list |
suppression.removed | Not yet | transactional, marketing | No | Removed from suppression list |
account.webhook_paused | Not yet | system | No | Webhook auto-paused |
account.domain_verified | Not yet | system | No | Domain verified |
account.domain_failed | Not yet | system | No | Domain verification failed |
account.quota_warning | Not yet | system | No | Approaching quota limit |
account.quota_exceeded | Not yet | system | No | Quota exhausted |
Future Events
When includeFutureEvents is true (the default), your webhook automatically receives new event types as they are added to your subscribed products. Set it to false to lock your subscription to the current event snapshot.
The events marked "Not yet" above become subscribable when their producers ship; the event names and payload shapes documented here will not change when that happens.
Next Steps
- Signature Verification: verify that events are from MailLand
- Retry Logic: understand retry behavior
- Webhooks Overview: create and manage webhooks