messageFailed
The messageFailed webhook event is triggered when EmailEngine gives up on a queued email. It is a terminal event: no further delivery attempts are made for this message.
When This Event is Triggered
The messageFailed event fires when the delivery job for a queued message ends without success, because:
- All delivery attempts were used up: the
deliveryAttemptsof the submission, or thedeliveryAttemptssetting, or 10 by default - An attempt failed permanently, so the remaining attempts were not spent. For SMTP submissions that is a
5xxserver reply other than503, or, without a server reply, anEAUTH,ENOAUTH,EOAUTH2,ETLS,EENVELOPE,EMESSAGEorEPROTOCOLerror - The gateway the submission named no longer exists (
GatewayNotFound). This is decided before any connection is made, so nomessageDeliveryErrorprecedes it
The event is sent for every submission type: SMTP, Gmail API and Microsoft Graph API. For SMTP submissions each failed attempt has already produced a messageDeliveryError event with the details of the error; for API submissions this event is the only report of the failure.
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 attempted to send the message |
date | string | Yes | ISO 8601 timestamp when the webhook was generated |
event | string | Yes | Always messageFailed |
data | object | Yes | Event 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.
Data Object Fields
| Field | Type | Required | Description |
|---|---|---|---|
messageId | string | Yes | Message-ID header of the email that failed to send |
queueId | string | Yes | EmailEngine's queue ID for this submission, the same value the Submit API returned |
error | string | Yes | First line of the stack trace recorded for the message's first failed attempt, so it starts with the error class name, as in Error: <message>. When a message fails the same way every time, which is the common case, that is also the final error |
networkRouting | object or null | No | Local address and proxy used for the last SMTP attempt, null when neither was configured. Present for SMTP submissions, absent for submissions handed to the Gmail API or the Microsoft Graph API (see below) |
There is no envelope on this event. Correlate by queueId or messageId with the messageSent or messageDeliveryError events, or with your own record of the submission.
Network Routing Object Structure
| Field | Type | Description |
|---|---|---|
localAddress | string | Local IP address used for the SMTP connection |
proxy | string | Proxy URL used for the connection |
name | string | Hostname used in the EHLO greeting |
requestedLocalAddress | string | The localAddress requested in the submission, when a different one was used |
Example Payload
Authentication Failure
{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T08:15:32.456Z",
"event": "messageFailed",
"data": {
"messageId": "<305eabf4-9538-2747-acec-dc32cb651a0e@example.com>",
"queueId": "183e4b18f0ffe977476",
"error": "Error: Invalid login: 535 5.7.8 Error: authentication failed",
"networkRouting": null
}
}
Connection Timeout
{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T09:30:15.123Z",
"event": "messageFailed",
"data": {
"messageId": "<abc123@example.com>",
"queueId": "184a5c29e1aaf988567",
"error": "Error: Connection timeout",
"networkRouting": null
}
}
Recipient Unknown
{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T10:45:00.000Z",
"event": "messageFailed",
"data": {
"messageId": "<msg-456@company.com>",
"queueId": "184b6d30f2bbg099678",
"error": "Error: Can't send mail - all recipients were rejected: 550 5.1.1 The email account that you tried to reach does not exist",
"networkRouting": null
}
}
With Network Routing Information
Every SMTP submission carries the field. It is null unless a local address or a proxy was used:
{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T11:22:33.789Z",
"event": "messageFailed",
"data": {
"messageId": "<campaign-789@marketing.com>",
"queueId": "184c7e41g3cch110789",
"error": "Error: connect ECONNREFUSED 203.0.113.25:587",
"networkRouting": {
"localAddress": "192.168.1.100",
"name": "mail.company.com"
}
}
}
Field Details
messageId
The Message-ID header from the original email. This is an RFC 5322 compliant identifier that can be used to:
- Correlate with the original submission request
- Match with any
messageDeliveryErrorevents that preceded this failure - Track the message through your email pipeline
Format: <unique-identifier@hostname.domain>
queueId
EmailEngine's queue identifier for this submission. Use this to:
- Correlate with the original Submit API response
- Match with
messageDeliveryErrorevents during retry attempts - Find the job in the Failed tab of Submission Queue under System > Queues
error
The first line of the stack trace of the first failed attempt, so it starts with the error class name. A message whose attempts failed for different reasons therefore reports the earliest one here; the messageDeliveryError events carry each attempt's own error, and the job's full stack traces are on the message's page in the queue browser (System > Queues > Outgoing messages > Failed).
Common patterns:
| Error Pattern | Description |
|---|---|
Error: Invalid login: 535... | SMTP authentication failure |
Error: Connection timeout | Unable to connect to the SMTP server within the timeout |
Error: Can't send mail - all recipients were rejected: 550 5.1.1 ... | Recipient mailbox does not exist |
Error: Can't send mail - all recipients were rejected: 550 5.7.1 ... | Sender rejected by the recipient server |
Error: connect ECONNREFUSED ... | SMTP server refused the connection |
Error: certificate has expired | TLS certificate validation failed |
Handling the Event
This is the terminal event for the submission, so the handler closes the record the Submit API's queueId opened and tells whoever submitted the message. For an SMTP submission the preceding messageDeliveryError events carry the error code and server reply; here error is a single line, so keep it as the reason rather than parsing it:
async function handleMessageFailed(event) {
const { account, data, date } = event;
const submission = await db.submissions.findUnique({
where: { queueId: data.queueId }
});
if (!submission) {
// Queued outside this application
return;
}
await db.submissions.update({
where: { queueId: data.queueId },
data: { status: 'failed', failedAt: new Date(date), error: data.error }
});
await notifySubmitter(submission, {
account,
messageId: data.messageId,
error: data.error
});
}
Retry Behavior
Before messageFailed is sent, the queue retries transient failures with an exponential backoff: 5 seconds after the first failure, doubling each time, with a random reduction of up to 20 percent, for up to 10 attempts by default. The schedule, the deliveryAttempts setting and the distinction between permanent and transient failures are described on the Outbox queue page.
The failed job stays in the submit queue for inspection: the last 500 failed jobs are kept for 7 days by default, see Keep Completed and Failed Jobs. The message itself is no longer queued and cannot be resumed; submit it again to retry.
Relationship to Other Events
The messageFailed event is the terminal failure state in the email delivery lifecycle:
| Event | Description |
|---|---|
messageSent | Successful handoff to the provider |
messageDeliveryError | A failed SMTP attempt, retried or not |
messageFailed | EmailEngine has given up (this event) |
messageBounce | Bounce notification received after a successful handoff |
Difference from messageDeliveryError
| Aspect | messageDeliveryError | messageFailed |
|---|---|---|
| When triggered | Each failed SMTP attempt | Once, when the job ends without success |
| Submission types | SMTP only | SMTP, Gmail API, MS Graph |
| Retries remaining | Usually | None |
| Action required | Monitor, may self-resolve | Intervention needed |
| Payload detail | Error code, server response, envelope, retry info | Message and queue IDs, one error line, network routing |
Related Events
- messageSent - Successful handoff to the provider
- messageDeliveryError - A failed SMTP attempt, with the error details
- messageBounce - Bounce message received after delivery
See Also
- Webhooks Overview - Delivery, retries, headers and signing
- Outbox queue - Retry schedule, permanent versus transient failures, and what the queue keeps
- Submit API - Send emails via EmailEngine
- Queue management - Inspecting failed jobs in the queue browser
- Sending Emails - Email sending guide