message.outbound.status
Delivery-status transitions for a message you sent - opt-in, one event per real change
type: message.outbound.status
Fired when an outbound message's delivery status actually changes: it was sent, delivered, read, or it failed.
This event is opt-in. It is never delivered to a subscription that did not list it explicitly. If an org has no subscriber for it, the payload is not even built.
Subscribe with the enum name MESSAGE_OUTBOUND_STATUS - see webhook subscriptions.
What Changed
message.outbound.updated used to carry both edits and delivery-status changes. It now fires on edits only, and status transitions come here instead.
| Event | Fires on |
|---|---|
message.outbound.updated | Edits to the message content. |
message.outbound.status | Delivery-status transitions. Opt-in. |
If you were reading deliveryStatus off message.outbound.updated, subscribe to this event - otherwise you will stop seeing status changes.
data shape
The same shape as message.outbound.updated:
type Data = {
chat: ChatPayload;
message: MessagePayload;
previous?: { deliveryStatus: string };
contact?: ContactPayload;
};The fields on message that carry the outcome:
| Field | Meaning |
|---|---|
deliveryStatus | pending, sent, delivered, read or failed. |
deliveredAt | Set when the status is delivered or read. |
readAt | Set when the status is read. |
failedAt | Set when the status is failed. |
errorCode | The provider's own error code, when it can be parsed out. Absent otherwise. |
errorMessage | The provider's error text. Read this - it carries the distinctions below. |
previous.deliveryStatus is the status it moved from, which is what lets you order transitions.
One Event per Real Change
| Repeated or redelivered provider receipts | No event. |
| Out-of-order provider receipts | No event - receipts only move forward. |
| Internal notes | Never fire this event. |
Status only advances: pending → sent → delivered → read. failed can happen at any point, and a successful retry moves failed back to sent.
Example
{
"id": "evt_01JV4R0A1B2C3D4E5F6G7H8J9K",
"type": "message.outbound.status",
"timestamp": "2026-10-06T18:00:04.000Z",
"data": {
"chat": { "id": "6804f4c2a6f9f35f6e66f1a1", "channel": "WHATSAPP" },
"message": {
"id": "6804f5011c34d0012ab8a077",
"chatId": "6804f4c2a6f9f35f6e66f1a1",
"direction": "OUTBOUND",
"type": "TEXT",
"deliveryStatus": "delivered",
"deliveredAt": "2026-10-06T18:00:04.000Z"
},
"previous": { "deliveryStatus": "sent" }
},
"meta": {
"eventId": "evt_01JV4R0A1B2C3D4E5F6G7H8J9K",
"event": "message.outbound.status",
"occurredAt": "2026-10-06T18:00:04.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}Ordering
For one message, Wexio emits message.outbound.created before any message.outbound.status.
Emission order is not delivery order. Deliveries are parallel and each retries independently, so the two can reach your endpoint swapped.
Order on your side by meta.occurredAt and previous.deliveryStatus, and deduplicate on the envelope id. Delivery is at-least-once.
Failures You Must Not Retry Blindly
A failed status does not always mean the message was not delivered. Read errorMessage:
errorMessage starts with | What it means | Safe to resend? |
|---|---|---|
outcome unknown | The message may have reached the contact. | No - you risk a duplicate. |
send timed out in queue | It never reached the provider. | Yes. |
| Anything else | A definite provider rejection. | Judge from the error. |
outcome unknown covers timeouts, dropped connections, gateway and other 5xx responses, a partially sent album, and a worker dying mid-call. None of these are retried automatically, and none of them tell you whether the contact received anything.
Surface these to a human rather than resending in code. A duplicate message to a customer is worse than a delayed one.
A send interrupted, it may have been delivered failure is the one case that can later flip to sent - a late success, arriving with previous.deliveryStatus: "failed".
Transient conditions are handled for you and do not surface as failures: our own per-channel throttle and a provider's rate-limit response are waited out, and a few retryable provider errors are retried up to four attempts.
Related
- Async send - the flow this event completes
- Message events -
message.outbound.updatedis now edits only - Envelope & delivery - ordering, deduplication, retries