Shared Types
The payload shapes that event data objects are built from
Every event's data is assembled from these shapes. Fields without ? are always present; ? fields are omitted when the underlying value is unset.
All IDs are 24-character hex strings. All timestamps are ISO-8601 UTC strings.
The Identity Model
Four levels, narrowing from the person to a single message:
Contact the person (contactId) - survives merges
└── People one channel identity (peopleId)
Conversation the inbox row (conversationId)
└── Chat one thread (chatId, threadKey)
└── Message (id)A Contact is the person. Each channel they arrive on is a People row, and a merge consolidates several People under one Contact. A Conversation groups the person's Chat threads, and a thread with a threadKey is one topic among several on the same channel.
Which field answers which question, all from data.message:
| Question | Field |
|---|---|
| Who is this person? | contactId - key your own records on it |
| Which channel identity wrote? | peopleId |
| Which channel? | channel |
| Which inbox row? | conversationId |
| Which thread do I reply into? | chatId |
| Which topic, if several? | threadKey |
| Who sent this message? | author.kind |
chatId is the one you pass to sendMessage - replies go to a thread, not to a contact or a conversation.
ChatPayload
One channel thread. Delivered under data.chat.
interface ChatPayload {
id: string;
integrationId?: string;
channel?: "WHATSAPP" | "TELEGRAM" | "VIBER" | "INSTAGRAM" | "WEB";
externalChatId?: string;
whatsappId?: string;
telegramId?: string;
viberId?: string;
instagramId?: string;
firstName?: string;
lastName?: string;
photoUrl?: string;
username?: string;
title?: string;
createdAt?: string;
updatedAt?: string;
archivedAt?: string | null;
conversationId?: string;
contactId?: string;
threadKey?: string;
status?: string;
assignedTo?: { kind: string; id: string };
}| Field | Notes |
|---|---|
id | The chat ID. This is the chatId you pass to sendMessage and getMessages. |
channel | The channel this thread is on, including WEB for website-widget chats. Absent only when the thread carries nothing identifying. |
externalChatId, whatsappId, telegramId, viberId, instagramId | Channel-native identifiers. Only the one for this chat's channel is populated. |
conversationId | The conversation this thread belongs to. |
contactId | The merge-winner contact ID. Survives merges - see contact.merged. |
threadKey | Names a topic thread inside the conversation. Absent on ordinary one-to-one threads, so its presence is what tells you the conversation has topic threads. |
status | The thread's status, so a chat.updated shows what changed. |
assignedTo | The assignee as { kind, id } - kind is always "OPERATOR", and it is id only, never the operator's record or email. |
Identity fields (firstName, lastName, username, photoUrl) are read from the merge-aware contact record, so they can change on a chat you have already mirrored. Treat them as mutable.
MessagePayload
One message. Delivered under data.message.
interface MessagePayload {
id: string;
chatId: string;
integrationId?: string;
direction: "INBOUND" | "OUTBOUND";
type: MessageType;
text?: string;
media?: MessageMediaItem[];
receivedAt?: string;
sentAt?: string;
deliveredAt?: string;
errorMessage?: string;
raw?: { providerMessageId?: string };
conversationId?: string;
contactId?: string;
peopleId?: string;
channel?: "WHATSAPP" | "TELEGRAM" | "VIBER" | "INSTAGRAM" | "WEB";
threadKey?: string;
fromModel?: string;
author?: { kind: string; id?: string };
}| Field | Notes |
|---|---|
text | Falls back to the message's caption when there is no text body. |
receivedAt | Set on INBOUND messages only. |
sentAt | Set on OUTBOUND messages only. |
deliveredAt | When the delivery status last changed. |
raw.providerMessageId | Channel-native message ID - WhatsApp wamid, Telegram message_id. Use it to correlate with provider-side data. Omitted when the channel gave none. |
conversationId | The conversation this message belongs to. |
contactId | Merge-winner contact ID. Key your records on this - it survives merges, peopleId does not. |
peopleId | The per-channel identity behind this particular message. |
channel | Which channel carried the message. |
threadKey | Which topic thread, when the conversation has several. Absent on 1:1 threads. |
fromModel | Sender kind - one of User, Bot, People, AIAssistant, Organisation. See who sent it. |
author | The sender as { kind, id, name? } - never the sender's record. name is present for User and Organisation; an operator's email is never included. |
Who sent it
author and fromModel carry the same four-way 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. |
author.kind is how you tell a human reply from an AI reply. Both arrive as message.outbound.created; nothing else in the payload distinguishes them.
contactId, peopleId, channel and threadKey are repeated on the message, not only on the chat - so you can route an event from data.message alone, without joining to data.chat.
All four are optional. They are populated only when the emitting code had both the chat and the person in hand, and the slim message.*.deleted payload carries none of them. Treat their absence as "not supplied here", never as "this message has no contact".
interface MessageMediaItem {
url: string;
type?: MediaType;
mimeType?: string;
filename?: string;
sizeBytes?: number;
caption?: string;
}url is resolvable - fetch media asynchronously, after you have acknowledged the webhook.
ContactPayload
A person. Delivered under data.contact.
interface ContactPayload {
peopleId: string;
phoneNumber?: string;
email?: string;
firstName?: string;
lastName?: string;
tags?: string[];
systemFields?: Record<string, string | number | boolean | null>;
customFields?: Record<string, string | number | boolean | null>;
createdAt?: string;
updatedAt?: string;
contactId?: string;
merged?: { mergedInto?: string; loosersIds?: string[] };
}| Field | Notes |
|---|---|
peopleId | The People record ID. |
contactId | The merge-winner contact ID this People row belongs to. Key your own records on this, not on peopleId, if you want merges to reconcile cleanly. |
merged.loosersIds | On a merge winner: the People IDs folded into it. |
merged.mergedInto | On a retired loser row: the winner it points at. |
tags | The contact's tags. |
systemFields / customFields | Field values as flat maps - Wexio's built-in fields and the ones your org defined. |
Identity fields can be null when the client org's privacy settings mask them.
ConversationPayload
An inbox row spanning a contact's threads. Delivered under data.conversation.
interface ConversationPayload {
id: string;
contactId?: string;
channels?: string[];
primaryThreadChatId?: string;
createdAt?: string;
updatedAt?: string;
}channels and primaryThreadChatId are not stored on the conversation record, so they appear only when the emitting code already had them cheaply at hand. Do not rely on either being present - read the threads via searchConversations when you need them for certain.
CommentPayload
An Instagram post comment. Delivered under data.comment.
interface CommentPayload {
id: string;
postId: string;
commenterId: string;
rootCommentId?: string;
externalCommentId?: string;
text?: string;
hidden?: boolean;
deleted?: boolean;
createdAt?: string;
updatedAt?: string;
}Only fields stored on the comment itself are included - the commenter and the post are not expanded. rootCommentId is set on replies and absent on top-level comments.
PostPayload
An Instagram post. Delivered under data.post.
interface PostPayload {
id: string;
integrationId: string;
channel: string;
externalPostId: string;
caption?: string;
username?: string;
isDeleted?: boolean;
publishedAt?: string;
createdAt?: string;
updatedAt?: string;
}username is the publishing account's channel username. publishedAt is when the post went live on the channel; createdAt is when Wexio first recorded it.
Enum Values
| Enum | Values |
|---|---|
direction | INBOUND, OUTBOUND |
channel | WHATSAPP, TELEGRAM, VIBER, INSTAGRAM, WEB |
type (message) | TEXT, MEDIA, BUTTONS, and further channel-specific types. UNKNOWN when a channel sends something Wexio does not model. |
Treat every enum as open. A new channel or message type can appear without an apiVersion bump, so default unknown values to a passthrough branch instead of throwing.