Webhooks and APIOutbound

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
  };
}
FieldMeaning
idUnique event identifier - equivalent to meta.eventId. Stable across retries, so use it as your dedupe key.
typeEvent name literal (see the event catalogue).
timestampWhen the event occurred, ISO-8601 UTC.
dataPer-event payload - the shape is documented on each event's page.
meta.apiVersionPinned to "2026-04-01". Any breaking change to a data shape will bump this; additive changes (new optional field) will not.
meta.attempt1-indexed retry counter. Same (eventId, attempt) pair is never re-posted.
meta.externalIdPartner fan-out only. The partner's own identifier for the client org this event came from.
meta.partnerIdPartner 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.connectionIdPresent - the connection that matchedAbsent - there is no per-org connection behind it
meta.externalId / meta.partnerIdAbsentPresent
meta.organisationIdYour own orgThe client org the event came from
Auth to your endpointNone, Bearer token, Basic or HMAC signatureHMAC 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>
HeaderPurpose
X-Wexio-EventEvent name (same as type in the body)
X-Wexio-Event-Idevt_<uuid> - use to dedupe retries on your side
X-Wexio-Attempt1-indexed attempt number
X-Wexio-TimestampISO-8601 UTC - same as timestamp
Idempotency-KeySame as X-Wexio-Event-Id - use whichever convention fits your framework
AuthorizationPresent with Bearer token or Basic
X-Wexio-SignaturePresent with HMAC signature: t=<unix>,v1=<hex>

Auth Modes

You set the mode and credential per connection, in the Authentication and Credential fields.

AuthenticationHow the receiver verifies
NoneNo auth header. The receiver relies on HTTPS and a URL nobody can guess.
Bearer tokenAuthorization: Bearer <credential>. Compare to the shared token.
BasicAuthorization: Basic <base64(credential)>. The credential is username:password.
HMAC signatureX-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 eventId twice with different attempt values. Dedupe by id.
  • 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.occurredAt before 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 data shape, 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.

Was this page helpful?

On this page