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.

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.

Common Use Cases

  • Deliverability monitoring - Track bounce rates across your email campaigns
  • List hygiene - Automatically remove or flag invalid email addresses
  • Reputation management - Identify and address delivery issues before they impact sender reputation
  • Customer notification - Alert users when their messages fail to deliver
  • Analytics - Build dashboards showing delivery success rates
  • Retry logic - Implement custom retry strategies for soft bounces

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
actionstringYesBounce action. failed for a rejected delivery; a non-standard bounce that only reports a delay can carry 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)
idstringNoDeprecated. EmailEngine message ID of the original message, looked up through the Document Store, so present only on IMAP accounts with the Document Store enabled. The Document Store is removed from releases starting 2026-10-01, so do not rely on this field

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"]
}
}
}

Bounces on Later Events

On IMAP accounts EmailEngine records every reported bounce against the Message-ID of the original message. A later messageNew event for a message with that Message-ID, for example the copy in the Sent folder being re-indexed, carries the recorded bounces in data.bounces. Gmail API and MS Graph accounts keep no such record. The stored record holds recipient, action, response.message, response.status, the bounce message ID and the time it was recorded.

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

Basic Handler

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

console.log(`Bounce detected for ${account}:`);
console.log(` Recipient: ${data.recipient}`);
console.log(` Action: ${data.action}`);
console.log(` Original Message ID: ${data.messageId}`);

if (data.response) {
console.log(` Status: ${data.response.status}`);
console.log(` Message: ${data.response.message}`);
console.log(` Category: ${data.response.category}`);
console.log(` Recommended Action: ${data.response.recommendedAction}`);

if (data.response.blocklist) {
console.log(` Blocklist: ${data.response.blocklist.name} (${data.response.blocklist.type})`);
}
}

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

switch (action) {
case 'remove':
await removeFromMailingList(data.recipient);
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);
}
}

Updating Email Lists

async function handleHardBounce(bounceData) {
const { recipient, response } = bounceData;

await db.contacts.update(
{ email: recipient },
{
$set: {
emailValid: false,
bounceReason: response?.message,
bounceCode: response?.status,
bounceCategory: response?.category,
recommendedAction: response?.recommendedAction,
bouncedAt: new Date()
}
}
);
}

Tracking Bounce Metrics

async function trackBounceMetrics(event) {
const { account, data } = event;

const category = data.response?.category || 'unknown';
const recommendedAction = data.response?.recommendedAction || 'review';
const isHardBounce = ['remove', 'remove_content'].includes(recommendedAction);

await metrics.increment('email.bounces', {
account,
type: isHardBounce ? 'hard' : 'soft',
category,
recommendedAction,
mta: data.mta || 'unknown',
blocklisted: data.response?.blocklist ? 'yes' : 'no'
});

if (data.response?.blocklist) {
await metrics.increment('email.blocklist_bounces', {
account,
blocklistName: data.response.blocklist.name,
blocklistType: data.response.blocklist.type
});
}
}

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

Best Practices

  1. Track both events - Use both messageBounce and messageFailed for complete deliverability monitoring
  2. Deduplicate bounces - The same delivery failure may trigger both events; use messageId to correlate
  3. Handle missing fields - Not all bounces include complete information; validate response and its fields before use
  4. Distinguish bounce types - Hard bounces require different handling than soft bounces
  5. Update promptly - Remove hard-bounced addresses from mailing lists immediately
  6. Log for analysis - Store bounce data for deliverability trend analysis
  7. Monitor the MTA field - Track which receiving servers generate the most bounces

See Also