Webhooks

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:

QuestionField
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 };
}
FieldNotes
idThe chat ID. This is the chatId you pass to sendMessage and getMessages.
channelThe channel this thread is on, including WEB for website-widget chats. Absent only when the thread carries nothing identifying.
externalChatId, whatsappId, telegramId, viberId, instagramIdChannel-native identifiers. Only the one for this chat's channel is populated.
conversationIdThe conversation this thread belongs to.
contactIdThe merge-winner contact ID. Survives merges - see contact.merged.
threadKeyNames 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.
statusThe thread's status, so a chat.updated shows what changed.
assignedToThe 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 };
}
FieldNotes
textFalls back to the message's caption when there is no text body.
receivedAtSet on INBOUND messages only.
sentAtSet on OUTBOUND messages only.
deliveredAtWhen the delivery status last changed.
raw.providerMessageIdChannel-native message ID - WhatsApp wamid, Telegram message_id. Use it to correlate with provider-side data. Omitted when the channel gave none.
conversationIdThe conversation this message belongs to.
contactIdMerge-winner contact ID. Key your records on this - it survives merges, peopleId does not.
peopleIdThe per-channel identity behind this particular message.
channelWhich channel carried the message.
threadKeyWhich topic thread, when the conversation has several. Absent on 1:1 threads.
fromModelSender kind - one of User, Bot, People, AIAssistant, Organisation. See who sent it.
authorThe 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:

KindWho
UserA human operator.
AIAssistantThe AI assistant.
BotA flow or bot - not the AI assistant.
PeopleThe customer.
OrganisationAn 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[] };
}
FieldNotes
peopleIdThe People record ID.
contactIdThe 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.loosersIdsOn a merge winner: the People IDs folded into it.
merged.mergedIntoOn a retired loser row: the winner it points at.
tagsThe contact's tags.
systemFields / customFieldsField 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

EnumValues
directionINBOUND, OUTBOUND
channelWHATSAPP, 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.

On this page