Shared Types
Payload interfaces and enums used across outbound webhook events
Every outbound event's data is built from these payload types. Each event page links back here for the full definitions.
ChatPayload
interface ChatPayload {
id: string; // Wexio chat id
integrationId?: string; // Owning messaging integration (Telegram bot, WhatsApp number, etc.)
channel?: WebhookChannel; // "WHATSAPP" | "TELEGRAM" | "VIBER" | "INSTAGRAM" | "WEB"
externalChatId?: string; // Provider-side chat id (WhatsApp chat, Telegram chat)
whatsappId?: string; // Derived from the contact; present when channel=WHATSAPP
telegramId?: string; // Telegram user id as string
viberId?: string; // Viber user id
instagramId?: string; // Instagram user id
firstName?: string;
lastName?: string;
photoUrl?: string;
username?: string;
title?: string; // Group/channel title
createdAt?: string; // ISO-8601 UTC
updatedAt?: string; // ISO-8601 UTC
conversationId?: string; // The Conversation this thread belongs to
contactId?: string; // Merge-winner Contact id
threadKey?: string; // Semantic thread key; absent on 1:1 threads
status?: string; // Thread status, so chat.updated shows the change
assignedTo?: { kind: string; id: string }; // Assignee, id only
}threadKeynames a topic thread within a conversation. A web visitor's "billing" question is a different thread from their main one, under the same conversation and contact. It is absent on ordinary one-to-one threads, so its presence tells you a conversation has topic threads.assignedTois id-only:{ kind: "OPERATOR", id }, never the operator's record or email.kindis alwaysOPERATOR. Resolve the name on your side.conversationIdis the conversation this thread sits under. A chat is one channel thread inside a conversation, not the top-level unit.contactIdis the merge-winner contact id. It survives a merge, whichpeopleIddoes not, so key your own records on it.channelis resolved from the thread. A website-widget chat reportsWEB. Otherwise it comes from the contact's channel-specific id (whatsappId,telegramId,viberId,instagramId, in that order). It is absent only when the chat carries nothing identifying.
Slim variant - chat.deleted and message.*.deleted events carry only:
Pick<ChatPayload, "id" | "integrationId" | "channel">ContactPayload
interface ContactPayload {
peopleId: string; // Wexio People record id (always present)
phoneNumber?: string;
email?: string;
firstName?: string;
lastName?: string;
createdAt?: string; // ISO-8601 UTC
updatedAt?: string; // ISO-8601 UTC
contactId?: string; // Merge-winner Contact id this People row belongs to
merged?: { // Present only on a merge winner / retired loser
mergedInto?: string; // On a retired loser row: the winner it points at
loosersIds?: string[]; // On a winner: the People ids folded into it
};
}peopleId is not stable across a merge - a merge retires People rows. Key your records on contactId (the merge-winner id) if you want contact.merged to reconcile cleanly.
Slim variant - contact.deleted events carry only { peopleId: string }.
Merge variants - contact.merged and contact.split do not use this shape. They carry id lists instead: { from: string[], into: string } and { restored: string[], from: string } respectively.
ConversationPayload
The inbox row a contact occupies, grouping their channel threads. Delivered under data.conversation on conversation.created and conversation.updated.
interface ConversationPayload {
id: string; // Wexio conversation id
contactId?: string; // Merge-winner Contact id
createdAt?: string; // ISO-8601 UTC
updatedAt?: string; // ISO-8601 UTC
}The payload is lean: it identifies the conversation and nothing more. To see its threads, read the conversation back.
MessagePayload
interface MessagePayload {
id: string; // Wexio message id
chatId: string; // Wexio chat id
integrationId?: string;
direction: "INBOUND" | "OUTBOUND";
type: MessageType; // see enum below
text?: string; // Plain text, or media caption
media?: MessageMediaItem[]; // For media/media-group types
receivedAt?: string; // Present on inbound
sentAt?: string; // Present on outbound
deliveryStatus?: "pending" | "sent" | "delivered" | "read" | "failed"; // Outbound lifecycle
deliveredAt?: string; // Set once delivered or read
readAt?: string; // Set once read
failedAt?: string; // Set when deliveryStatus is failed
errorCode?: string; // Provider error code, when it can be parsed
errorMessage?: string;
raw?: { // Channel-specific provider IDs
providerMessageId?: string; // e.g. Telegram message_id, WhatsApp wamid
[k: string]: unknown;
};
conversationId?: string; // The Conversation this message belongs to
contactId?: string; // Merge-winner Contact id
peopleId?: string; // Per-channel People identity
channel?: WebhookChannel; // Channel this message rode
threadKey?: string; // Topic thread; absent on 1:1 threads
fromModel?: string; // "User"|"Bot"|"People"|"AIAssistant"|"Organisation"
author?: { kind: string; id?: string; name?: string }; // name for User and Organisation
apiKeyId?: string; // Public id of the API key that sent it
}
interface MessageMediaItem {
mediaId?: string; // Stable Wexio media id
scanStatus?: ScanStatus; // see enum below
url?: string; // Signed link; absent when INFECTED
type?: MediaType; // see enum below
mimeType?: string;
filename?: string;
sizeBytes?: number;
caption?: string;
}apiKeyIdis the public id of the API key that sent an outbound message, including when the key acted as an operator. It is absent on inbound messages and on anything sent by a member or the AI.deliveryStatusfollows an outbound message frompendingtoread, or tofailed.deliveredAt,readAtandfailedAtare set from the status.urlis a link valid for 1 hour for media Wexio stores, and it is regenerated on every delivery attempt. AnINFECTEDitem has nourl. UsemediaIdto deduplicate attachments.
Slim variant - message.*.deleted events carry only:
Pick<MessagePayload, "id" | "chatId" | "direction" | "type">Who Sent It
author and fromModel carry the same sender kind:
| Kind | Who |
|---|---|
User | A human operator. |
AIAssistant | The AI assistant. |
Bot | A flow or bot - not the AI assistant. |
People | The customer. |
Organisation | An API key with no acting operator. id is the organisation that owns the key, and name is its name. |
Organisation is new. If your handler has a strict allow-list of author.kind values, add it or those messages are dropped or mislabelled.
It appears whenever a message is sent with an API key and no actingUserId. For a tech-provider key acting on a managed client, the author is the provider, not the client.
Bot does not mean the AI. The AI assistant is AIAssistant, and Bot is a flow or bot sender. Swapping them mislabels every automated reply.
On an outbound message this is the only thing that tells a human reply from an AI one, because both arrive as message.outbound.created.
author pairs the kind with the sender's id, and carries a name for User and Organisation. The sender's record is never included, and an operator's email never appears.
Routing Without a Lookup
contactId, peopleId, channel and threadKey appear on the message as well as the chat, so you can route an event from data.message alone.
| Field | Use |
|---|---|
contactId | The person, stable across merges. Key your records on this. |
peopleId | The per-channel identity behind this particular message. |
channel | Which channel carried it. |
threadKey | Which topic thread, when the conversation has several. |
All four are optional. They are filled only when the emitting code had the chat and the person at hand, and the slim message.*.deleted payload has none of them. Never assume they are present.
CommentPayload
An Instagram post comment. Delivered under data.comment on the comment.* events.
interface CommentPayload {
id: string; // Wexio comment id
postId: string; // The SocialPost it belongs to
commenterId: string; // The Commenter record
rootCommentId?: string; // Set on replies; absent on top-level comments
externalCommentId?: string; // Instagram's own comment id
text?: string;
hidden?: boolean; // Hidden on Instagram
deleted?: boolean; // Soft-deleted in Wexio
createdAt?: string; // ISO-8601 UTC
updatedAt?: string; // ISO-8601 UTC
}The payload is flat: postId and commenterId are ids, not expanded objects. Resolve them yourself if you need the post or commenter.
PostPayload
An Instagram post. Delivered under data.post on post.created and post.deleted.
interface PostPayload {
id: string; // Wexio post id
integrationId: string; // The Instagram integration
channel: string; // "INSTAGRAM"
externalPostId: string; // Instagram's own media id
caption?: string;
username?: string; // Publishing account's channel username
isDeleted?: boolean;
publishedAt?: string; // When it went live on the channel
createdAt?: string; // When Wexio first recorded it
updatedAt?: string; // ISO-8601 UTC
}On the sync path of post.deleted, only id is meaningful. integrationId comes back as an empty string and every other field is absent, so treat everything but id as optional on that event.
Enums
type WebhookChannel = "WHATSAPP" | "TELEGRAM" | "VIBER" | "INSTAGRAM" | "WEB";
type MessageDirection = "INBOUND" | "OUTBOUND";
enum MessageType {
TEXT = "TEXT",
BUTTONS = "BUTTONS",
MEDIA = "MEDIA",
MEDIA_BUTTONS = "MEDIA_BUTTONS",
MEDIA_GROUP = "MEDIA_GROUP",
LOCATION = "LOCATION",
POOL = "POOL", // (sic - poll)
CONTACT = "CONTACT",
PIN = "PIN",
GAME = "GAME",
DICE = "DICE", // Telegram animated emoji
CHAT_UPDATE = "CHAT_UPDATE",
STORY = "STORY",
UNKNOWN = "UNKNOWN",
UNSUPPORTED = "UNSUPPORTED", // Sent by the contact but not deliverable in a supported form
// WhatsApp-specific
LIST = "LIST",
TEMPLATE = "TEMPLATE",
CALLBACK = "CALLBACK",
CTA_URL = "CTA_URL",
LOCATION_REQUEST = "LOCATION_REQUEST",
// Commerce
ORDER = "ORDER",
PRODUCT = "PRODUCT",
// System messages (flow started/completed, assignment changed, etc.)
SYSTEM = "SYSTEM",
// Internal to the inbox
INTERNAL_NOTE = "INTERNAL_NOTE",
INTERNAL_ACTIONS = "INTERNAL_ACTIONS",
}
type MessageDeliveryStatus = "pending" | "sent" | "delivered" | "read" | "failed";
enum ScanStatus {
PENDING = "PENDING", // Not scanned yet
CLEAN = "CLEAN",
INFECTED = "INFECTED", // No url is sent
SKIPPED = "SKIPPED",
ERROR = "ERROR", // The scan failed
}
enum MediaType {
PHOTO = "photo",
VIDEO = "video",
VIDEO_NOTE = "video_note",
DOCUMENT = "document",
AUDIO = "audio",
VOICE = "voice",
ANIMATION = "animation",
STICKER = "sticker",
}Related
- Envelope & Delivery - the envelope that carries these payloads
- Outbound Webhooks - the overview and the full event catalogue