Envelope & Delivery
The JSON envelope, HTTP headers, signing scheme, and retry policy for outbound webhooks
Every outbound event - regardless of type - is delivered as a single POST with the same envelope shape, the same headers, and the same retry semantics. This page is the one-stop reference.
Envelope
interface OutboundWebhookEnvelope<TData> {
id: string; // "evt_<uuid>" - same as meta.eventId; unique across retries
type: OutboundWebhookEventName; // e.g. "chat.created", "message.inbound.created", "flow.<suffix>"
timestamp: string; // ISO-8601 UTC - same as meta.occurredAt
data: TData; // per-event payload - see the individual event pages
meta: {
eventId: string;
event: OutboundWebhookEventName;
occurredAt: string; // ISO-8601 UTC
organisationId: string;
connectionId: string;
apiVersion: "2026-04-01";
attempt: number; // 1-indexed retry number. Same (eventId, attempt) is never re-posted.
externalId?: string; // Partner deliveries only - the partner's own id for the child org
partnerId?: string; // Partner deliveries only - the receiving partner organisation
};
}| Field | Meaning |
|---|---|
id | Unique event identifier - equivalent to meta.eventId. Stable across retries, so use it as your dedupe key. |
type | Event name literal (see the event catalogue). |
timestamp | When the event occurred, ISO-8601 UTC. |
data | Per-event payload - the shape is documented on each event's page. |
meta.apiVersion | Pinned to "2026-04-01". Any breaking change to a data shape will bump this; additive changes (new optional field) will not. |
meta.attempt | 1-indexed retry counter. Same (eventId, attempt) pair is never re-posted. |
meta.externalId | Partner fan-out only. The partner's own identifier for the client org this event came from. |
meta.partnerId | Partner fan-out only. The receiving partner organisation. |
Partner Fan-Out Deliveries
A tech provider registers one webhook that receives events for every client org it manages. The envelope is the same, with two differences in meta:
| Per-org connection (this page) | Partner fan-out | |
|---|---|---|
meta.connectionId | Present - the connection that matched | Absent - there is no per-org connection behind it |
meta.externalId / meta.partnerId | Absent | Present |
meta.organisationId | Your own org | The client org the event came from |
| Auth to your endpoint | None, Bearer token, Basic or HMAC signature | HMAC signature only |
Everything else - the event catalogue, the signing scheme, the retry policy, deduplication - is identical. Route a fan-out delivery on meta.organisationId or meta.externalId.
See Partner webhooks for the partner-side setup.
HTTP Request
POST <outboundUrl>
Content-Type: application/json
X-Wexio-Event: <event name>
X-Wexio-Event-Id: evt_<uuid>
X-Wexio-Attempt: 1
X-Wexio-Timestamp: 2026-04-24T12:00:00.000Z
Idempotency-Key: evt_<uuid>
[Authorization: ...] # Bearer token / Basic only
[X-Wexio-Signature: t=…,v1=…] # HMAC signature only
<envelope JSON body>| Header | Purpose |
|---|---|
X-Wexio-Event | Event name (same as type in the body) |
X-Wexio-Event-Id | evt_<uuid> - use to dedupe retries on your side |
X-Wexio-Attempt | 1-indexed attempt number |
X-Wexio-Timestamp | ISO-8601 UTC - same as timestamp |
Idempotency-Key | Same as X-Wexio-Event-Id - use whichever convention fits your framework |
Authorization | Present with Bearer token or Basic |
X-Wexio-Signature | Present with HMAC signature: t=<unix>,v1=<hex> |
Auth Modes
You set the mode and credential per connection, in the Authentication and Credential fields.
| Authentication | How the receiver verifies |
|---|---|
| None | No auth header. The receiver relies on HTTPS and a URL nobody can guess. |
| Bearer token | Authorization: Bearer <credential>. Compare to the shared token. |
| Basic | Authorization: Basic <base64(credential)>. The credential is username:password. |
| HMAC signature | X-Wexio-Signature: t=<unix>,v1=<hex> over ${t}.${rawBody}, SHA-256. |
HMAC verification
const [tPart, v1Part] = header.split(',');
const t = tPart.slice(2); // "t=1714..."
const v1 = v1Part.slice(3); // "v1=<hex>"
const expected = createHmac('sha256', sharedSecret)
.update(`${t}.${rawBody}`)
.digest('hex');
if (!timingSafeEqual(expected, v1)) throw new Error('bad signature');
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) throw new Error('stale');The t value is a unix timestamp; reject anything outside a ±5 minute tolerance to block replays.
Retry Policy
- Up to 6 attempts per event.
- Exponential backoff starting at 2 seconds.
- Wexio waits 15 seconds for a response.
- Retries fire on 5xx responses and on network or timeout failures.
- Any 4xx is final. Fix the connection, because Wexio does not retry it.
- A 3xx redirect is not followed and not retried. Use the final URL.
Each attempt increments meta.attempt and writes a new row to the delivery log. The (eventId, attempt) pair is unique. The same combination is never re-posted.
Partner fan-out deliveries follow a stricter policy: a 5 second timeout, 408, 425 and 429 also retry, Retry-After is honoured up to 5 minutes, and an event still undelivered 24 hours after it occurred is dropped. A rotated partner secret signs with both the new and previous secret for 24 hours. See Envelope & Delivery for partners.
Processing Guarantees
- At-least-once. Retries after 5xx/timeout mean a consumer can see the same
eventIdtwice with differentattemptvalues. Dedupe byid. - Best-effort ordering. Events are enqueued in order but processed in parallel by the delivery worker. Consumers that need strict ordering per chat should sort by
meta.occurredAtbefore applying. - Fire-and-forget. Wexio's domain services never wait on outbound delivery - if your endpoint is down, events queue up for retry, then dead-letter after the last attempt. Downtime on your side does not affect Wexio operation.
- Audit trail. Every attempt (success or failure) is persisted in the delivery log, queryable via Webhook History. Retention 7 days.
API Version
meta.apiVersion is currently "2026-04-01". Treat it as a breaking-change marker:
- Additive changes (new optional field on an existing
datashape, new event type) - version stays the same. - Breaking changes (removed or renamed field, changed field type) - version is bumped, and old connections continue receiving the old version until migrated.
Related
- Outbound Webhooks - overview, auth config, delivery log
- Envelope & Delivery for partners - the partner fan-out contract
- Shared Types -
ChatPayload,ContactPayload,MessagePayload, enums - Webhook History - browsing delivery attempts and the live subscription