Webhooks

Webhook events

Learn what events Lettermint can send and how to handle them. Subscribe to the events you need when creating a webhook.

Common envelope

All webhook events share a consistent envelope:

JSONCode
{ "id": "54d7e8c9-1195-4ba0-9d3f-b9af92305add", "event": "message.delivered", "timestamp": "2025-08-08T20:14:00.000Z", "sandbox": false, "sandbox_result": null, "context": { "scope": "route", "team_id": "9f4e3d2c-1b0a-4987-9654-3210fedcba98", "project_id": "7d6c5b4a-3210-4987-9654-fedcba987654", "route_id": "1a2b3c4d-5678-4987-9654-abcdef123456" }, "data": { /* event-specific object */ } }

Fields:

  • id: Unique identifier (UUIDv4) for the specific delivery event. Useful for idempotency.
  • event: Event type.
  • timestamp: ISO timestamp when the event occurred.
  • context: Scope and source IDs for the event.
  • data: Event-specific fields (documented per event below).

Outbound message events also have these top-level fields:

  • sandbox: true for a Sandbox message. Use this field when your handler must prevent customer actions during a test.
  • sandbox_result: The resolved simulated result, or null for a Live message. On a recipient event, this is the recipient's own result, which can differ from the message-level result when the recipient used a test address. On message.created, it is the message-level result.

See Sandbox mode for webhook filters and simulated sequences.

The context object has these fields:

  • scope: Source scope. The value is team, project, or route.
  • team_id: Team ID for the event.
  • project_id: Project ID, or null for a team event.
  • route_id: Route ID, or null for a team or project event.

Use context when one webhook receives events from multiple projects or routes.

Common message data fields:

  • message_id: The unique identifier for the message.
  • subject: The email subject line.
  • metadata: Custom metadata attached to the message (if any).
  • headers: Accepted customer headers for outbound messages, as an array of {name, value} entries. Repeated values use separate entries.
  • tag: The tag assigned to categorize the message (e.g., "newsletter", "order-confirmation"). Will be null if no tag was assigned.
  • tags: The complete list of singular and structured tags. The reserved __lettermint_tag item is the singular-tag projection.

The tags field is additive. It does not replace tag. If your webhook data transfer objects reject unknown fields, update them to permit tags.

These fields apply to message.* events. Suppression events use the suppression-specific fields documented below.

Outbound message events include data.headers, including bounces and spam complaints. Header names retain their submitted case, except that Lettermint uses one canonical name for each list header. Compare header names without case sensitivity.

JSONCode
"headers": [ {"name": "X-Correlation-ID", "value": "correlation-id"}, {"name": "X-Custom", "value": "first"}, {"name": "X-Custom", "value": "second"} ]

Lettermint stores these headers when it accepts an SMTP or API message. It excludes credentials, private recipient headers such as Bcc, and Lettermint control headers such as X-LM-* and X-Lettermint-*. It also leaves out signature and relay trace headers such as DKIM-Signature, ARC-*, X-MS-* and X-Google-*. Lettermint keeps at most 50 entries and 8 KB of names and values; a header that does not fit is left out. Headers do not become metadata.

Messages accepted before header snapshots were available return headers: []. Lettermint does not read old message bodies to recover headers. Header snapshots follow message retention. A webhook retry uses its stored payload, which is kept for 7 days, so a retry of an older delivery can still have no headers field. Inbound webhooks keep their existing header and content rules.

Event payloads

message.created

Message accepted for processing.

Example payload:

JSONCode
{ "id": "54d7e8c9-1195-4ba0-9d3f-b9af92305add", "event": "message.created", "timestamp": "2025-08-08T20:14:00.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "from": { "email": "updates@acme.com", "name": "Acme Updates" }, "to": ["user@example.com"], "cc": ["cc@example.com"], "bcc": ["bcc@example.com"], "reply_to": ["help@acme.com"], "subject": "Welcome to Acme", "metadata": { "X-Campaign-ID": "welcome-2025" }, "tag": "welcome", "tags": [ {"name": "__lettermint_tag", "value": "welcome"}, {"name": "campaign", "value": "welcome-v2"} ] } }

message.sent

Message sent to recipient server.

JSONCode
{ "id": "7f9c8e2a-1b3d-4f6e-b7d2-5c9f3a7e8b0c", "event": "message.sent", "timestamp": "2025-08-08T20:15:00.000Z", "data": { "message_id": "123e4567-e89b-12d3-a456-426614174000", "subject": "Your weekly newsletter", "recipient": "user@example.com", "metadata": { "X-User-ID": "user-123" }, "tag": "newsletter", "tags": [ {"name": "__lettermint_tag", "value": "newsletter"}, {"name": "environment", "value": "production"} ] } }

message.delivered

Message successfully delivered.

JSONCode
{ "id": "9b0c4a4e-4e29-4d8b-8b3a-3f0f3e6d2f9b", "event": "message.delivered", "timestamp": "2025-08-08T20:15:12.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Your order has shipped", "recipient": "user@example.com", "response": { "status_code": 250, "enhanced_status_code": "2.0.0", "content": "OK 1640705112 qp1355551phe.1 - gsmtp" }, "metadata": { "X-Transaction-ID": "txn-456" }, "tag": "order-confirmation", "tags": [ {"name": "__lettermint_tag", "value": "order-confirmation"}, {"name": "campaign", "value": "orders"} ] } }

message.auto_replied

Recipient mail server sent an automatic reply such as an out-of-office or vacation response. Auto replies are informational and do not change the message delivery status.

JSONCode
{ "id": "4c1a6e5b-7a2f-4f8a-9d3e-6f7b8c9d0e1f", "event": "message.auto_replied", "timestamp": "2025-08-08T20:16:10.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Your order has shipped", "metadata": { "X-Order-ID": "order-12345" }, "tag": "shipping", "tags": [ {"name": "__lettermint_tag", "value": "shipping"}, {"name": "environment", "value": "production"} ], "auto_reply": { "sender": "user@example.com", "recipient": "feedback-f47ac10b-58cc-4372-a567-0e02b2c3d479@lm-bounces.example.com", "subject": "Automatic reply: out of office", "body": { "text": "I am currently out of office and will reply when I return.", "html": "<p>I am currently out of office and will reply when I return.</p>" } } } }

Fields:

  • auto_reply.sender: Address that sent the automatic reply, when available
  • auto_reply.recipient: Bounce/feedback recipient address that received the automatic reply
  • auto_reply.subject: Subject of the automatic reply, when available
  • auto_reply.body.text: Plain-text automatic reply body, when available
  • auto_reply.body.html: HTML automatic reply body, when available

message.hard_bounced

Permanent delivery failure (e.g., user does not exist).

JSONCode
{ "id": "123e4567-e89b-12d3-a456-426614174000", "event": "message.hard_bounced", "timestamp": "2025-08-08T20:15:30.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Summer sale - 50% off!", "recipient": "user@example.com", "response": { "status_code": 250, "enhanced_status_code": "2.0.0", "content": "OK 1640705112 qp1355551phe.1 - gsmtp" }, "metadata": { "X-Campaign-ID": "summer-sale" }, "tag": "marketing", "tags": [ {"name": "__lettermint_tag", "value": "marketing"}, {"name": "campaign", "value": "summer-sale"} ] } }

message.soft_bounced

Temporary delivery failure (e.g., mailbox full, transient error).

JSONCode
{ "id": "9b0c4a4e-4e29-4d8b-8b3a-3f0f3e6d2f9b", "event": "message.soft_bounced", "timestamp": "2025-08-08T20:15:30.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Your order #789 update", "recipient": "user@example.com", "response": { "status_code": 250, "enhanced_status_code": "2.0.0", "content": "OK 1640705112 qp1355551phe.1 - gsmtp" }, "metadata": { "X-Order-ID": "order-789" }, "tag": "order-notification", "tags": [ {"name": "__lettermint_tag", "value": "order-notification"}, {"name": "environment", "value": "production"} ] } }

message.spam_complaint

Recipient reported the message as spam.

JSONCode
{ "id": "123e4567-e89b-12d3-a456-426614174000", "event": "message.spam_complaint", "timestamp": "2025-08-08T20:16:00.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Weekly digest - August edition", "recipient": "user@example.com", "metadata": { "X-Newsletter-ID": "weekly-digest" }, "tag": "newsletter", "tags": [ {"name": "__lettermint_tag", "value": "newsletter"}, {"name": "campaign", "value": "weekly-digest"} ] } }

message.failed

The message could not be delivered due to a terminal processing or delivery failure.

JSONCode
{ "id": "9b0c4a4e-4e29-4d8b-8b3a-3f0f3e6d2f9b", "event": "message.failed", "timestamp": "2025-08-08T20:14:12.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Password reset request", "recipient": "user@example.com", "reason": "5.7.10 Encryption needed: recipient did not offer STARTTLS", "reason_code": "enforced_tls_failed", "response": { "status_code": 550 }, "metadata": { "X-Session-ID": "sess-abc123" }, "tag": null, "tags": [ {"name": "environment", "value": "production"} ] } }

When an enforced TLS policy cannot be satisfied, reason_code is enforced_tls_failed and response.status_code is 550. Temporary connection and TLS errors continue through normal retries and are reported as message.soft_bounced while retryable.

message.suppressed

Lettermint skipped one recipient because a suppression matched the message.

JSONCode
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "event": "message.suppressed", "timestamp": "2025-08-08T20:14:05.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Your account statement", "recipient": "user@example.com", "reason": "hard_bounce", "scope": "team", "suppression_type": "email", "matched_value": "user@example.com", "suppression_id": "1d74d4c9-8492-4f5f-ae3f-f6fd671399ea", "matching_suppression_ids": [ "1d74d4c9-8492-4f5f-ae3f-f6fd671399ea" ], "matching_reasons": ["hard_bounce"], "metadata": { "X-Account-ID": "acc-xyz789" }, "tag": "transactional", "tags": [ {"name": "__lettermint_tag", "value": "transactional"}, {"name": "environment", "value": "production"} ] } }

Lettermint sends one message.suppressed event for each blocked recipient. The reason and suppression_id fields identify the primary match. The matching_suppression_ids and matching_reasons arrays contain all matching records and reasons.

For disposable recipient suppression, reason is disposable_email and suppression_id is null. Lettermint does not store a suppression record for this route setting.

message.policy_rejected

Message rejected by policy (spam or content filter). This occurs when a message's spam score exceeds the configured threshold before delivery.

JSONCode
{ "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef", "event": "message.policy_rejected", "timestamp": "2025-08-08T20:14:03.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Limited time offer!!!", "reason": "Spam score threshold exceeded", "score": 7.5, "spam_symbols": [ { "name": "BAYES_SPAM", "score": 3.5, "options": [], "description": "Bayes spam probability is very high" }, { "name": "SUBJ_ALL_CAPS", "score": 1.5, "options": [], "description": "Subject is all capitals" } ], "metadata": { "X-Campaign-ID": "promo-123" }, "tag": "marketing", "tags": [ {"name": "__lettermint_tag", "value": "marketing"}, {"name": "campaign", "value": "limited-offer"} ] } }

Fields:

  • reason: Human-readable rejection reason (e.g., "spam content", "policy violation")
  • score: The spam score that triggered the rejection
  • spam_symbols: Array of spam rule matches with name, score, options, and description

message.unsubscribed

Recipient unsubscribed from the mailing list.

JSONCode
{ "id": "b2c3d4e5-f6a7-8901-bcde-f01234567891", "event": "message.unsubscribed", "timestamp": "2025-08-08T20:16:30.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "August newsletter highlights", "recipient": "user@example.com", "unsubscribed_at": "2025-08-08T20:16:30.000Z", "metadata": { "X-Campaign-ID": "newsletter-august" }, "tag": "newsletter", "tags": [ {"name": "__lettermint_tag", "value": "newsletter"}, {"name": "campaign", "value": "weekly-digest"} ] } }

message.opened

Recipient opened the email. Requires open tracking to be enabled on the route.

This event is only available for routes with open tracking enabled. By default, webhooks receive human-countable opens and supported Apple or Proton privacy opens. Security scanners, previews, and generic bots require Include machine events.

JSONCode
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "event": "message.opened", "timestamp": "2025-08-08T20:17:00.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Your order has shipped", "metadata": { "X-Order-ID": "order-12345" }, "tag": "shipping", "tags": [ {"name": "__lettermint_tag", "value": "shipping"}, {"name": "environment", "value": "production"} ], "recipient": "user@example.com", "opened_at": "2025-08-08T20:17:00+00:00", "tracking_event_id": "0198a5bd-6008-7d91-a532-635dc95f4371", "classification_revision": 1, "first_open": true, "device_type": "desktop", "client_type": "browser", "client_name": "Chrome", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "bot": { "detected": false, "probability": 0, "classification": "genuine", "proxy_type": null, "reason_codes": [], "machine": false, "counts_for_metrics": true }, "engagement": { "event_id": "0198a5bd-6008-7d91-a532-635dc95f4371", "revision": 1, "request_actor": "human", "engagement_state": "high_confidence_human", "time_basis": "direct_observation", "human_open_time_known": true, "observed_open": true, "human_open": true, "privacy_open": false, "inferred": false } } }

Fields:

  • opened_at: Timestamp when the open was detected
  • first_open: Whether this is the first time this recipient opened the email
  • first_observed_open: Whether this is the first human or supported privacy open observation
  • device_type: Device category: desktop, mobile, or tablet
  • client_type: Client category: browser, email_client, etc.
  • client_name: Specific client name: Chrome, Safari, Outlook, etc.
  • user_agent: The user agent string from the request
  • bot.detected: Whether the open appears to be from an automated source (bot, proxy, scanner)
  • bot.probability: Confidence score (0-100) that this is a bot interaction
  • bot.classification: Source classification such as genuine, privacy_proxy, security_scanner, or generic_bot
  • bot.proxy_type: Known proxy family when applicable, otherwise null
  • bot.reason_codes: Broad categories behind the classification. See Reason code guidance
  • bot.machine: Whether this open was classified as machine, privacy, or security activity
  • bot.counts_for_metrics: Whether this open contributes to human-open metrics
  • bot.counts_for_status: Whether this event can change the message status
  • bot.webhook_eligible: Whether this event is eligible for a default webhook
  • tracking_event_id: Stable ID for this tracking observation. Use it with classification_revision to process later updates.
  • classification_revision: Revision number for this tracking event. A higher number replaces a lower number for the same tracking_event_id.
  • engagement: Optional engagement conclusion. It separates observed, human, privacy, and inferred engagement. It can also contain privacy_fetch_at, engagement_confirmed_at, and human_open_time_known.

See Bot Detection Field Values for the stable classification contract and guidance for open diagnostic strings.

message.clicked

Recipient clicked a link in the email. Requires click tracking to be enabled on the route.

This event is only available for routes with click tracking enabled. By default, webhooks receive only human-countable clicks; machine, link preview, and security scanner clicks require Include machine events.

JSONCode
{ "id": "b2c3d4e5-f6a7-8901-bcde-f01234567892", "event": "message.clicked", "timestamp": "2025-08-08T20:18:30.000Z", "data": { "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "subject": "Your weekly recommendations", "metadata": { "X-Campaign-ID": "weekly-recs" }, "tag": "recommendations", "tags": [ {"name": "__lettermint_tag", "value": "recommendations"}, {"name": "variant", "value": "B"} ], "recipient": "user@example.com", "clicked_at": "2025-08-08T20:18:30+00:00", "tracking_event_id": "0198a5be-cfa8-78e4-8339-144ab5a92fb1", "classification_revision": 1, "destination_url": "https://example.com/product/123", "link_index": 0, "anchor_text": "View Product", "first_click": true, "device_type": "mobile", "client_type": "browser", "client_name": "Safari", "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15", "bot": { "detected": false, "probability": 0, "classification": "genuine", "proxy_type": null, "reason_codes": [], "machine": false, "counts_for_metrics": true } } }

Fields:

  • clicked_at: Timestamp when the click occurred
  • destination_url: The original destination URL that was clicked
  • link_index: Zero-based index of the link in the email (order of appearance)
  • anchor_text: The visible text of the clicked link
  • first_click: Whether this is the first time this recipient clicked any link in this email
  • device_type: Device category: desktop, mobile, or tablet
  • client_type: Client category: browser, email_client, etc.
  • client_name: Specific client name: Chrome, Safari, Outlook, etc.
  • user_agent: The user agent string from the request
  • bot.detected: Whether the click appears to be from an automated source (security scanner, link preview bot)
  • bot.probability: Confidence score (0-100) that this is a bot interaction
  • bot.classification: Source classification such as genuine, privacy_proxy, security_scanner, or generic_bot
  • bot.proxy_type: Known proxy family when applicable, otherwise null
  • bot.reason_codes: Broad categories behind the classification. See Reason code guidance
  • bot.machine: Whether this click was classified as machine, preview, or security activity
  • bot.counts_for_metrics: Whether this click contributes to human-click metrics
  • bot.counts_for_status: Whether this event can change the message status
  • bot.webhook_eligible: Whether this event is eligible for a default webhook
  • tracking_event_id: Stable ID for this click observation. The click has an ID that is different from the ID of an earlier privacy open.
  • classification_revision: Revision number for this click. A new click starts at revision 1.
  • engagement: Optional engagement conclusion and evidence times. It is present when the click confirms an earlier privacy open.

Privacy inference revisions

A supported privacy fetch first creates a message.opened event at revision 1. If a later high-confidence click confirms human engagement, Lettermint sends revision 2 of that open event. The revised open keeps the same tracking_event_id and the same privacy-fetch time. Its engagement.engagement_confirmed_at value contains the click time. Its engagement.human_open_time_known value is false.

The related message.clicked event has its own tracking_event_id and starts at revision 1. Store the highest classification_revision for each tracking_event_id.

See Bot Detection Field Values for the stable classification contract and guidance for open diagnostic strings.

Suppression events

Suppression events are emitted once per non-global suppression entry. Lettermint sends them to enabled, subscribed team, project, and route webhooks that cover the affected transactional and broadcast routes.

Global-scope suppressions do not emit webhook events. Inbound routes do not support suppression events.

Delivery fans out according to the suppression scope:

Suppression scopeWebhook recipients
routeWebhooks attached to that route
projectWebhooks attached to routes in that project
teamWebhooks attached to routes in that team

Duplicate additions do not emit another event. A removal event is emitted only when the suppression is actually deleted, so a removal that remains pending for manual review does not emit suppression.removed.

suppression.added

A suppression entry affecting the webhook's route was added. The envelope timestamp is the suppression's creation time.

JSONCode
{ "id": "6f9475b4-487e-4a6e-b79f-983fd7b02ed8", "event": "suppression.added", "timestamp": "2026-07-29T08:15:30.000Z", "context": { "scope": "team", "team_id": "9f4e3d2c-1b0a-4987-9654-3210fedcba98", "project_id": null, "route_id": null }, "data": { "suppression_id": "1d74d4c9-8492-4f5f-ae3f-f6fd671399ea", "type": "email", "value": "suppressed@example.com", "reason": "hard_bounce", "applies_to": "all" } }

suppression.removed

A suppression entry affecting the webhook's route was deleted. The original suppression fields remain in the payload. The envelope timestamp is the removal time.

JSONCode
{ "id": "9a61ff8c-42fe-4e02-a01e-6cb742ab8d87", "event": "suppression.removed", "timestamp": "2026-07-29T09:45:00.000Z", "context": { "scope": "team", "team_id": "9f4e3d2c-1b0a-4987-9654-3210fedcba98", "project_id": null, "route_id": null }, "data": { "suppression_id": "1d74d4c9-8492-4f5f-ae3f-f6fd671399ea", "type": "email", "value": "suppressed@example.com", "reason": "hard_bounce", "applies_to": "all" } }

Suppression event data fields:

  • suppression_id: Unique identifier for the suppression entry.
  • type: Suppressed value type: email, domain, or extension.
  • value: The suppressed email address, domain, or extension.
  • reason: Why the entry was created: manual, hard_bounce, spam_complaint, or unsubscribe.
  • applies_to: all for transactional and broadcast mail, or broadcast for broadcast mail only.

The webhook context identifies the team, project, or route for the suppression.

Inbound events

message.inbound

Sent after an inbound route receives, parses, and queues an email. The event contains SMTP envelope data, parsed message headers and bodies, attachment references, spam diagnostics, and the original raw message.

If the spam score is equal to or higher than the route threshold, Lettermint quarantines the message. It does not send this event. An authorized user can process the message from the dashboard or Team API. Lettermint then sends the event to all current webhook targets that match the original route. The event keeps the original receive time, spam result, and attachment delivery form.

This event is only available for messages from inbound routes. A team or project webhook receives the event when its scope includes the inbound route. See Inbound Mail to create a route, or Process inbound email for handling guidance.

A route webhook can select inbound and outbound routes together. In this case, it can subscribe to inbound and outbound event groups. Each selected route sends only the events that its route type supports.

Example payload:

JSONCode
{ "id": "e3d4f5a6-b7c8-9012-d3e4-f5a6b7c89012", "event": "message.inbound", "timestamp": "2025-10-02T14:30:00.000Z", "data": { "route": "support-inbox", "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "envelope": { "remote_ip": "203.0.113.42", "remote_hostname": "mail.example.com", "helo": "smtp.example.com", "mail_from": "customer@example.com" }, "from": { "email": "customer@example.com", "name": "John Doe", "subaddress": null }, "to": [ { "email": "support@acme.com", "name": null, "subaddress": null } ], "cc": [], "recipient": "support@acme.com", "subaddress": null, "reply_to": "customer@example.com", "subject": "Question about my order", "date": "2025-10-02T14:30:00.000Z", "body": { "text": "Hi, I have a question about my recent order...", "html": "<p>Hi, I have a question about my recent order...</p>" }, "tag": null, "tags": [], "headers": [ { "name": "Message-ID", "value": "<abc123@mail.example.com>" }, { "name": "X-Mailer", "value": "Apple Mail (2.3445.104.11)" } ], "authentication_results": { "authserv_id": "lettermint.mx", "header": "lettermint.mx; dkim=pass header.d=example.com header.s=mail; dmarc=pass header.from=example.com policy=none; spf=pass smtp.mailfrom=example.com", "dkim": { "result": "pass", "domain": "example.com", "selector": "mail" }, "dmarc": { "result": "pass", "domain": "example.com", "policy": "none" }, "spf": { "result": "pass", "domain": "example.com" } }, "attachments": [ { "filename": "receipt.pdf", "content_type": "application/pdf", "size": 45678, "content_id": null, "content": "JVBERi0xLjQKJeLjz9MK..." } ], "raw": { "url": "https://storage.lettermint.co/inbound/raw/f47ac10b-58cc-4372-a567-0e02b2c3d479?expires=..&signature=..", "expires_at": "2025-10-30T14:30:00+00:00" }, "is_spam": false, "spam_score": 1.2, "spam_symbols": [ { "name": "DKIM_VALID", "score": -0.1, "options": [], "description": "Message has valid DKIM signature" }, { "name": "SPF_PASS", "score": -0.1, "options": [], "description": "SPF check passed" }, { "name": "BAYES_HAM", "score": -3.0, "options": [], "description": "Bayesian classifier identified message as non-spam" } ] } }

Event envelope

FieldTypeDescription
idstringWebhook delivery ID. Lettermint reuses it when retrying the same delivery.
event"message.inbound"Event type.
timestampstringISO 8601 time when Lettermint created the event.
dataobjectParsed inbound message.

The top-level id identifies this webhook delivery. data.message_id identifies the inbound message itself.

Message data

FieldTypeDescription
routestringInbound route slug.
message_idstringStable UUID for the inbound message.
envelopeobjectSMTP connection and envelope data. This is separate from visible message headers.
fromobjectParsed visible From address.
toarrayParsed visible To addresses. The array can be empty.
ccarrayParsed visible Cc addresses. The array can be empty.
recipientstring | nullPrimary SMTP envelope recipient. Use this field for application routing.
subaddressstring | nullPlus tag from recipient, without the +.
reply_tostring | nullParsed Reply-To address.
subjectstring | nullParsed message subject.
datestringISO 8601 time when Lettermint received the message.
body.textstring | nullPlain-text body.
body.htmlstring | nullHTML body. Sanitize it before rendering.
tagstring | nullValid tag parsed from X-LM-Tag or X-Tag.
tagsarrayComplete singular and structured tag list. The array can be empty.
headersarrayParsed non-standard headers as name and value pairs. Standard address, subject, and date headers are represented by dedicated fields.
authentication_resultsobjectStructured DKIM, DMARC, and SPF results produced during inbound scanning.
attachmentsarrayAttachment objects in exactly one of the delivery shapes below. The array can be empty.
rawobjectAlways-present signed reference to the original .eml message.
is_spambooleanWhether the configured route threshold classified the message as spam.
spam_scorenumber | nullScore produced by inbound spam scanning.
spam_symbolsarray | nullDiagnostic scanner symbols. Use authentication_results for stable authentication decisions.

Each address in from, to, and cc contains email, nullable name, and nullable subaddress. Envelope fields remote_ip, remote_hostname, helo, and mail_from are strings or null when the SMTP session did not provide a usable value.

authentication_results always contains authserv_id, an RFC-style header string, and structured dkim, dmarc, and spf objects. Each method has a result. Related domain, selector, and policy fields can be null. A result can be pass, fail, softfail, neutral, temperror, permerror, none, or unknown.

Attachment delivery shapes

Every attachment contains nullable filename, a content_type string, nullable integer size, and nullable content_id. The remaining fields depend on the route's Attachments setting:

ModePresent fieldsAbsent fields
Inlinecontent with Base64-encoded bytesurl, expires_at
Signed URLurl, expires_atcontent

The raw object always contains url and expires_at. Attachment and raw-message signed URLs expire after 28 days. See Attachments and raw email for configuration and secure processing guidance.

Verify the webhook signature before trusting these fields. See Signed webhooks for complete examples.

inbound.rejected

Sent when Lettermint permanently rejects mail for one of your inbound routes during the SMTP conversation. The sending server gets a 5xx reply and returns the message to its sender, so your application never receives it as message.inbound. Use this event to tell your team or the sender what happened.

Lettermint does not send this event for an address that no inbound route accepts, or for a temporary 4xx rejection that the sending server retries. A rejection at the rcpt stage sends one event per recipient. A rejection at the data stage sends one event that lists all affected recipients of your route. A sender that tries again gets a new rejection_id.

Inbound route webhooks created in the dashboard subscribe to this event automatically. An existing webhook receives it after you add inbound.rejected to its events.

JSONCode
{ "id": "4b1d6f0e-2c7a-5e39-9a8b-1f2e3d4c5b6a", "event": "inbound.rejected", "timestamp": "2026-10-28T10:00:00.123000Z", "context": { "scope": "route", "team_id": "9f4e3d2c-1b0a-4987-9654-3210fedcba98", "project_id": "7d6c5b4a-3210-4987-9654-fedcba987654", "route_id": "1a2b3c4d-5678-4987-9654-abcdef123456" }, "data": { "rejection_id": "0f3c2b9e-5a4d-4c1b-9e8f-7a6b5c4d3e2f", "route": "support-inbox", "stage": "rcpt", "reason": "message_too_large", "permanent": true, "recipients": ["support@acme.com"], "smtp": { "code": 550, "enhanced_code": "5.3.4", "message": "Message size exceeds fixed maximum message size" }, "envelope": { "mail_from": "customer@example.com", "remote_ip": "203.0.113.42", "remote_hostname": "mail.example.com", "helo": "mail.example.com" }, "size": { "declared_bytes": 52428800, "observed_bytes": null, "limit_bytes": 26214400 } } }
FieldTypeDescription
timestampstringISO 8601 time when Lettermint rejected the message.
rejection_idstringStable UUID for the rejection. Retried deliveries of the event keep the same id and rejection_id.
routestringInbound route slug.
stagestringrcpt when Lettermint refused the message before receiving it, for example because the sender announced a size over the limit. data when Lettermint refused it after receiving it.
reasonstringWhy Lettermint rejected the message. See the table below.
permanentbooleanAlways true.
recipientsarrayEnvelope recipients of this route.
smtpobjectThe reply sent to the sending server. enhanced_code can be null.
envelopeobjectSMTP envelope data. Each field is a string, or null when the session did not provide a usable value. A null mail_from means the message had an empty sender (<>), as bounces do.
sizeobject | nullMessage size in bytes for message_too_large, and null for other reasons. declared_bytes is the size the sender announced and observed_bytes the size Lettermint received. Either can be null.

The event never contains the subject, headers, body, or attachments.

ReasonWhat happenedWhat to do
message_too_largeThe message exceeds 25 MB, including headers, MIME encoding, and attachments.Ask the sender to share a link or send a smaller file.
attachment_blockedThe message contains a blocked file type, such as an executable or script, or an archive that is encrypted or cannot be inspected.Ask the sender to share the file another way, for example as a download link.
mail_loop_detectedThe message has more than 100 Received headers, so it is probably forwarded in a loop.Check forwarding rules that send mail for this route back to the route.

Test events

webhook.test

Special event that can be triggered from the Dashboard for connectivity testing.

JSONCode
{ "id": "test-7f9c8e2a-1b3d-4f6e-b7d2-5c9f3a7e8b0c", "event": "webhook.test", "timestamp": "2025-08-08T20:14:12.000Z", "data": { "message": "This is a test webhook from Lettermint", "webhook_id": "9f9bf19c-4a2c-45f3-a6c7-bc937224ec5a", "timestamp": 1754921294 } }

Next steps