Skip to main content

messageBounce

The messageBounce webhook event is triggered when EmailEngine detects a bounce response email in a monitored mailbox. Bounce emails are generated by mail servers when they cannot deliver a message to the intended recipient. This event helps you track email deliverability by identifying messages that failed to reach their recipients.

When This Event is Triggered​

The messageBounce event fires when a message arriving in the Inbox or the Junk folder looks like a bounce, and EmailEngine can extract all three of the failed recipient, the bounce action and the Message-ID of the original message from it. A bounce that is missing any of these produces no event. The check runs on every account type: IMAP, Gmail API and MS Graph.

A message is checked for bounce content when one of these holds:

  • The sender name is Mail Delivery System, Mail Delivery Subsystem, Internet Mail Delivery, Mailer-Daemon or NDR Administrator, or the address is a mailer-daemon@ or postmaster@ address
  • The subject starts with Undeliverable: and the message carries an Auto-Submitted header (Exchange)
  • The message has a message/delivery-status part
  • The message embeds the original message as message/rfc822 and the subject mentions Undeliver, Failed, Returned, Failure or Error
  • The subject matches a known bounce subject such as Mail delivery failed, Delivery Status Notification, Returned mail, Failure Notice or error sending your mail

A message EmailEngine has already recognized as a delivery report is not examined again as a bounce.

EmailEngine then parses the message for bounce information from these formats:

  • Standard RFC 3464 delivery status notifications
  • Amazon WorkMail bounce notifications
  • Gmail bounce messages
  • Microsoft Exchange bounce reports
  • Postfix mailer-daemon responses
  • Zoho Mail bounce notifications
  • Exim and generic SMTP server bounces

A delivery status notification that reports a successful delivery or a delay is not a bounce. It is reported on the messageNew event as deliveryReport instead.

The bounce message itself also produces a messageNew event, sent before this one, with isBounce: true and relatedMessageId set to the Message-ID of the bounced message. On IMAP accounts neither event is sent for messages dated before the account's notifyFrom.

Payload Schema​

Top-Level Fields​

FieldTypeRequiredDescription
serviceUrlstring or nullYesThe configured EmailEngine service URL, null if not set
accountstringYesAccount ID that received the bounce message
datestringYesISO 8601 timestamp when the webhook was generated
eventstringYesAlways messageBounce
dataobjectYesBounce data object (see below)

The event carries no path or specialUse. The unique event identifier is sent as the HTTP header X-EE-Wh-Event-Id, not in the JSON payload.

Bounce Data Fields (data object)​

FieldTypeRequiredDescription
bounceMessagestringYesEmailEngine message ID of the bounce notification email itself. Fetch it through the message API to read the full bounce
recipientstringYesEmail address that bounced
actionstringYesAlways failed. Since EmailEngine 2.82.0 the event is sent only for a report whose action is failed; earlier releases also sent it for a non-standard report carrying delayed
messageIdstringYesMessage-ID header of the original message that bounced
responseobjectNoDetails of the rejection, with classification (see below). Absent when the bounce carried no diagnostic text
mtastringNoHostname of the server that reported the failure (Remote-MTA, or Reporting-MTA when there is no remote one), lowercased
queueIdstringNoQueue ID from the sending MTA (X-Postfix-Queue-Id)
messageHeadersobject or nullYesHeaders of the original bounced message when the bounce included them, otherwise null (see below)
idstringNoRemoved in v2.82.0 together with the Document Store. Releases before that set it, on IMAP accounts with the Document Store enabled, to the EmailEngine message ID of the original message

Response Object Structure​

The response object describes the rejection. source, message and status come from the bounce; the remaining fields are added by the bounce classifier (v2.60.0 and later) when it can classify message:

FieldTypeDescription
sourcestringType of the diagnostic code, typically smtp. Only for RFC 3464 notifications
messagestringThe error message from the receiving server
statusstringEnhanced status code (for example 5.1.1), when one was found
categorystringClassified bounce category (see Bounce Categories below)
recommendedActionstringSuggested action: remove, retry, review, fix_configuration, retry_different_ip or remove_content
blocklistobjectPresent when the message names a known blocklist. Contains name (string) and type (ip, domain or uri)
retryAfternumberRetry delay in seconds, read out of the message text. Only present when the message states one and it falls between 1 second and 24 hours

Message Headers Object Structure​

When the bounce embeds the original message or its headers, messageHeaders carries every header of that message, keyed by lowercase header name. Each value is an array of strings, one entry per header line, in the order they appeared:

{
"messageHeaders": {
"return-path": ["<sender@example.com>"],
"from": ["Sender Name <sender@example.com>"],
"to": ["Recipient <missing@example.com>"],
"subject": ["Your original message subject"],
"message-id": ["<305eabf4-9538-2747-acec-dc32cb651a0e@example.com>"],
"date": ["Mon, 17 Oct 2025 09:46:25 +0300"],
"mime-version": ["1.0"]
}
}

The set of headers depends on what the bouncing server included. A text/rfc822-headers part carries the full header block, an embedded message/rfc822 part carries the whole original message, and some servers quote only a few headers in the text body.

Example Payload​

{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T06:46:29.436Z",
"event": "messageBounce",
"data": {
"bounceMessage": "AAAAAQAABy8",
"recipient": "missing@example.com",
"action": "failed",
"response": {
"source": "smtp",
"message": "550 5.1.1 The email account that you tried to reach does not exist",
"status": "5.1.1",
"category": "user_unknown",
"recommendedAction": "remove"
},
"mta": "mx.example.com",
"queueId": "9441D8220E",
"messageId": "<305eabf4-9538-2747-acec-dc32cb651a0e@example.com>",
"messageHeaders": {
"return-path": ["<sender@example.com>"],
"from": ["Sender Name <sender@example.com>"],
"to": ["Recipient <missing@example.com>"],
"subject": ["Your original message subject"],
"message-id": ["<305eabf4-9538-2747-acec-dc32cb651a0e@example.com>"],
"date": ["Mon, 17 Oct 2025 09:46:25 +0300"],
"mime-version": ["1.0"]
}
}
}

No Bounce Record is Kept​

EmailEngine does not store reported bounces. Releases before v2.81.2 recorded each bounce against the Message-ID of the original message on IMAP accounts and listed them in a bounces array on later messageNew events and in message API responses; that store and the field were removed in v2.81.2. Keep your own record of sent messages and correlate this event with it by messageId.

Understanding Bounce Types​

Hard Bounces (Permanent Failures)​

Hard bounces indicate permanent delivery failures. The action field is failed and the enhanced status code starts with 5:

Status CodeMeaning
5.1.1Invalid mailbox / User unknown
5.1.2Invalid domain
5.2.1Mailbox disabled
5.2.2Mailbox full (can also be temporary)
5.4.1No answer from host
5.7.1Delivery not authorized

Soft Bounces (Temporary Failures)​

Soft bounces are temporary and may succeed on retry. The enhanced status code starts with 4:

Status CodeMeaning
4.2.2Mailbox full (temporary)
4.4.1Connection timeout
4.4.2Connection dropped
4.7.1Temporary authentication failure

A server that intends to retry normally sends a delayed delivery status notification rather than a bounce; see deliveryReport on messageNew.

Bounce Categories​

Since v2.60.0 EmailEngine classifies the rejection text with a machine learning model (@postalsys/bounce-classifier). The category field in the response object provides a detailed classification beyond the basic hard/soft distinction, and recommendedAction maps it to a next step:

CategoryDescriptionRecommended action
user_unknownRecipient email address does not existremove
invalid_addressBad email syntax or domain not foundremove
mailbox_disabledAccount suspended or disabledremove
mailbox_fullOver quota, storage exceededretry
greylistingTemporary rejection, retry laterretry
rate_limitedToo many connections or messagesretry
server_errorTimeout or connection failedretry
ip_blacklistedSender IP on a blocklist (RBL)retry_different_ip
domain_blacklistedSender domain on a blocklistfix_configuration
auth_failureDMARC, SPF, or DKIM failurefix_configuration
relay_deniedRelaying not permittedfix_configuration
spam_blockedMessage detected as spamreview
policy_blockedLocal policy rejectionreview
virus_detectedInfected content detectedremove_content
geo_blockedGeographic or country-based rejectionretry_different_ip
unknownUnclassified bounce typereview
ActionDescription
removePermanently remove the address from mailing lists
retryRetry delivery after a delay
reviewManual review required
fix_configurationFix sender DNS, authentication, or relay settings
retry_different_ipRetry from a different sending IP address
remove_contentRemove problematic content and resend

Blocklist Detection​

When the rejection names a known blocklist, the blocklist object identifies it:

{
"response": {
"message": "550 blocked using zen.spamhaus.org",
"category": "ip_blacklisted",
"recommendedAction": "retry_different_ip",
"blocklist": {
"name": "Spamhaus ZEN",
"type": "ip"
}
}
}

Retry Timing​

When the rejection states a delay (for example "try again in 5 minutes"), retryAfter carries it in seconds:

{
"response": {
"message": "450 Greylisted, try again in 5 minutes",
"category": "greylisting",
"recommendedAction": "retry",
"retryAfter": 300
}
}

Handling the Event​

response and the classifier fields inside it are absent when the bounce carried no diagnostic text, so fall back to action when there is no recommendedAction. retryAfter is only present when the rejection stated a delay:

async function handleMessageBounce(event) {
const { data } = event;

const action = data.response?.recommendedAction ||
(data.action === 'failed' ? 'remove' : 'review');

switch (action) {
case 'remove':
await removeFromMailingList(data.recipient, {
reason: data.response?.category || 'bounce',
messageId: data.messageId
});
break;
case 'retry': {
const delay = data.response?.retryAfter || 3600;
await scheduleRetry(data.recipient, delay);
break;
}
case 'fix_configuration':
await alertAdminConfigIssue(data);
break;
case 'retry_different_ip':
await retryWithDifferentIP(data);
break;
default:
await flagForReview(data);
}
}

Key Differences from messageFailed​

The messageBounce and messageFailed events serve different purposes:

AspectmessageBouncemessageFailed
TriggerBounce email received in mailboxEmailEngine gives up on a queued message
SourceRemote mail server's bounce notificationEmailEngine's delivery queue
TimingCan be minutes to days after sendingImmediately after the final delivery attempt
DetectionRequires parsing the bounce emailDirect SMTP or API error response

Use messageBounce when you need to:

  • Track bounces from emails sent through other systems
  • Get detailed bounce information from the receiving server's perspective
  • Monitor mailboxes that receive bounce notifications

Use messageFailed when you need to:

  • Track delivery failures for emails sent through EmailEngine's queue
  • Get immediate feedback on delivery attempts
  • Handle failures before bounce notifications arrive

See Also​