Webhooks

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.

EventFires on
message.outbound.updatedEdits to the message content.
message.outbound.statusDelivery-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:

FieldMeaning
deliveryStatuspending, sent, delivered, read or failed.
deliveredAtSet when the status is delivered or read.
readAtSet when the status is read.
failedAtSet when the status is failed.
errorCodeThe provider's own error code, when it can be parsed out. Absent otherwise.
errorMessageThe 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 receiptsNo event.
Out-of-order provider receiptsNo event - receipts only move forward.
Internal notesNever 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 withWhat it meansSafe to resend?
outcome unknownThe message may have reached the contact.No - you risk a duplicate.
send timed out in queueIt never reached the provider.Yes.
Anything elseA 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.

On this page