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-DaemonorNDR Administrator, or the address is amailer-daemon@orpostmaster@address - The subject starts with
Undeliverable:and the message carries anAuto-Submittedheader (Exchange) - The message has a
message/delivery-statuspart - The message embeds the original message as
message/rfc822and the subject mentionsUndeliver,Failed,Returned,FailureorError - The subject matches a known bounce subject such as
Mail delivery failed,Delivery Status Notification,Returned mail,Failure Noticeorerror 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
| Field | Type | Required | Description |
|---|---|---|---|
serviceUrl | string or null | Yes | The configured EmailEngine service URL, null if not set |
account | string | Yes | Account ID that received the bounce message |
date | string | Yes | ISO 8601 timestamp when the webhook was generated |
event | string | Yes | Always messageBounce |
data | object | Yes | Bounce 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)
| Field | Type | Required | Description |
|---|---|---|---|
bounceMessage | string | Yes | EmailEngine message ID of the bounce notification email itself. Fetch it through the message API to read the full bounce |
recipient | string | Yes | Email address that bounced |
action | string | Yes | Bounce action. failed for a rejected delivery; a non-standard bounce that only reports a delay can carry delayed |
messageId | string | Yes | Message-ID header of the original message that bounced |
response | object | No | Details of the rejection, with classification (see below). Absent when the bounce carried no diagnostic text |
mta | string | No | Hostname of the server that reported the failure (Remote-MTA, or Reporting-MTA when there is no remote one), lowercased |
queueId | string | No | Queue ID from the sending MTA (X-Postfix-Queue-Id) |
messageHeaders | object or null | Yes | Headers of the original bounced message when the bounce included them, otherwise null (see below) |
id | string | No | Deprecated. 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:
| Field | Type | Description |
|---|---|---|
source | string | Type of the diagnostic code, typically smtp. Only for RFC 3464 notifications |
message | string | The error message from the receiving server |
status | string | Enhanced status code (for example 5.1.1), when one was found |
category | string | Classified bounce category (see Bounce Categories below) |
recommendedAction | string | Suggested action: remove, retry, review, fix_configuration, retry_different_ip or remove_content |
blocklist | object | Present when the message names a known blocklist. Contains name (string) and type (ip, domain or uri) |
retryAfter | number | Retry 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 Code | Meaning |
|---|---|
| 5.1.1 | Invalid mailbox / User unknown |
| 5.1.2 | Invalid domain |
| 5.2.1 | Mailbox disabled |
| 5.2.2 | Mailbox full (can also be temporary) |
| 5.4.1 | No answer from host |
| 5.7.1 | Delivery not authorized |
Soft Bounces (Temporary Failures)
Soft bounces are temporary and may succeed on retry. The enhanced status code starts with 4:
| Status Code | Meaning |
|---|---|
| 4.2.2 | Mailbox full (temporary) |
| 4.4.1 | Connection timeout |
| 4.4.2 | Connection dropped |
| 4.7.1 | Temporary 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:
| Category | Description | Recommended action |
|---|---|---|
user_unknown | Recipient email address does not exist | remove |
invalid_address | Bad email syntax or domain not found | remove |
mailbox_disabled | Account suspended or disabled | remove |
mailbox_full | Over quota, storage exceeded | retry |
greylisting | Temporary rejection, retry later | retry |
rate_limited | Too many connections or messages | retry |
server_error | Timeout or connection failed | retry |
ip_blacklisted | Sender IP on a blocklist (RBL) | retry_different_ip |
domain_blacklisted | Sender domain on a blocklist | fix_configuration |
auth_failure | DMARC, SPF, or DKIM failure | fix_configuration |
relay_denied | Relaying not permitted | fix_configuration |
spam_blocked | Message detected as spam | review |
policy_blocked | Local policy rejection | review |
virus_detected | Infected content detected | remove_content |
geo_blocked | Geographic or country-based rejection | retry_different_ip |
unknown | Unclassified bounce type | review |
Using Recommended Actions
| Action | Description |
|---|---|
remove | Permanently remove the address from mailing lists |
retry | Retry delivery after a delay |
review | Manual review required |
fix_configuration | Fix sender DNS, authentication, or relay settings |
retry_different_ip | Retry from a different sending IP address |
remove_content | Remove 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:
| Aspect | messageBounce | messageFailed |
|---|---|---|
| Trigger | Bounce email received in mailbox | EmailEngine gives up on a queued message |
| Source | Remote mail server's bounce notification | EmailEngine's delivery queue |
| Timing | Can be minutes to days after sending | Immediately after the final delivery attempt |
| Detection | Requires parsing the bounce email | Direct 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
- Track both events - Use both
messageBounceandmessageFailedfor complete deliverability monitoring - Deduplicate bounces - The same delivery failure may trigger both events; use
messageIdto correlate - Handle missing fields - Not all bounces include complete information; validate
responseand its fields before use - Distinguish bounce types - Hard bounces require different handling than soft bounces
- Update promptly - Remove hard-bounced addresses from mailing lists immediately
- Log for analysis - Store bounce data for deliverability trend analysis
- Monitor the MTA field - Track which receiving servers generate the most bounces
Related Events
- messageFailed - Triggered when EmailEngine gives up on a queued email
- messageDeliveryError - Triggered on each failed SMTP delivery attempt
- messageSent - Triggered when a message is accepted for delivery
- messageNew - The bounce notification also triggers this event
See Also
- Webhooks Overview - Delivery, retries, headers and signing
- Bounce handling - Detecting and acting on bounces end to end
- Sending Emails - How to send emails through EmailEngine
- Message API - Fetching the bounce message by
bounceMessage - Settings API - Configure webhook settings