MailLandDocs

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/catalog
Get event catalog
curl -X GET https://api.mail.land/v1/webhooks/catalog
Catalog response (abbreviated)
{
  "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:

Standard payload
{
  "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
  }
}
FieldTypeDescription
eventstringEvent type identifier
event_idstringUnique event ID for deduplication (format: evt_{32chars})
timestampdatetimeWhen the webhook was queued (UTC), not when the underlying event occurred. See data.timestamp for that.
occurred_atdatetimeWhen the underlying event occurred (UTC); for message events the same instant as data.timestamp
schema_versionintegerPayload schema version of this event type (1 today); mirrored in X-MLND-Event-Version. See Versioning
dataobjectEvent-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_version 0 while it is queued or sent. The step from queued to sent sends no event and does not change the version.
  • Several message.deferred events can share one status_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 failed message can then become bounced or delivered, a bounced message can become delivered, and an unknown message can become any final status.

A message never goes from delivered to failed. MailLand counts these corrections.

FieldTypeDescription
status_versionintegerThe version of the message's status this event describes.
statusstringThe message's status at that version: queued, sent, deferred, unknown, delivered, bounced or failed.
previous_statusstring or nullThe status before this change; null before the first change.
complaint_statusstringnone or complained.
reasonstring or nullThe 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_unknown and message.retried. Only then do the events you receive contain every status change.
  • Store status_version with the status. Apply an event only if its status_version is higher than the one you stored; a message.deferred with an equal version changes nothing.
  • Keep processing events after message.failed, message.bounced or message.delivered: a later event with a higher status_version replaces 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 at status_version 0 it can show sent with 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.

message.sent (illustrative)
{
  "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.

message.delivered
{
  "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.

message.deferred
{
  "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.

message.bounced
{
  "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:

ReasonWhat happened
dkim_revokedThe 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_bounceA MailLand operator removed the message from MailLand's delivery queue.
dkim_key_revokedThe sender domain is under an emergency DKIM hold, so MailLand refused the message before it entered the delivery queue.
inject_failedMailLand's delivery queue refused the message outright, so it was never queued for delivery.
expiredMailLand accepted the message but did not hand it to its delivery queue within 24 hours, so it ended it without sending it.
template_variable_missingThe 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 a message.delivered or message.bounced follows with a higher status_version.

A refusal by the receiving mail server is reported as message.bounced, never as message.failed.

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"
  }
}
FieldDescription
reasonWhy MailLand ended the message: one of the values above. Treat any other value as a new MailLand-side reason.
smtp_responseText 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.

message.outcome_unknown
{
  "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.

message.retried
{
  "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.

message.opened (illustrative)
{
  "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.

message.clicked (illustrative)
{
  "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).

message.unsubscribed
{
  "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).

message.complained
{
  "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.

suppression.added (illustrative)
{
  "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.

suppression.removed (illustrative)
{
  "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.

inbound.received
{
  "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.

account.webhook_paused
{
  "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.

account.domain_verified (illustrative)
{
  "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.

account.domain_failed (illustrative)
{
  "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.

account.quota_warning (illustrative)
{
  "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.

account.quota_exceeded (illustrative)
{
  "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.

EventAvailableProductsDomain-ScopedDescription
message.deliveredYestransactional, marketingYesSuccessfully delivered
message.deferredYestransactional, marketingYesTemporarily delayed
message.bouncedYestransactional, marketingYesHard or soft bounce
message.failedYestransactional, marketingYesEnded by MailLand on conclusive evidence, for a reason on MailLand's side; can be followed by a correction (see Status versions)
message.outcome_unknownYestransactional, marketingYesMailLand could not determine whether the message reached its delivery queue; not final
message.retriedYestransactional, marketingYesA bounced or failed message was retried and is queued again; not final
message.complainedYestransactional, marketingYesMarked as spam
inbound.receivedYesinboundYesInbound email received
message.sentNot yettransactional, marketingYesEmail accepted and queued
message.openedNot yettransactional, marketingYesTracking pixel loaded
message.clickedNot yettransactional, marketingYesLink clicked
message.unsubscribedYestransactional, marketingYesRecipient unsubscribed
suppression.addedNot yettransactional, marketingNoAdded to suppression list
suppression.removedNot yettransactional, marketingNoRemoved from suppression list
account.webhook_pausedNot yetsystemNoWebhook auto-paused
account.domain_verifiedNot yetsystemNoDomain verified
account.domain_failedNot yetsystemNoDomain verification failed
account.quota_warningNot yetsystemNoApproaching quota limit
account.quota_exceededNot yetsystemNoQuota 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

On this page