Messaging

Send, start conversations, read threads and search

Every tool takes an optional orgId - tech providers only, naming the managed client to act on. An id outside the target org behaves exactly like a missing one.

Messaging - 5

ToolScopeDoes
list_conversationsMESSAGES_READThe inbox. Filter by rules or a stored collection, sort, page. Expensive with rules or a collection.
get_messagesMESSAGES_READOne thread, or a whole conversation with its threads merged.
search_messagesMESSAGES_READSearch. Needs a query, rules or a scope. Expensive.
send_messageMESSAGES_SENDSend into an existing thread.
trigger_conversationMESSAGES_SENDStart or continue a conversation by phone or channel id.

trigger_conversation

Starts or continues a conversation with a recipient named directly.

ArgumentTypeRequiredDescription
channel"WHATSAPP" | "TELEGRAM" | "VIBER" | "WEB" | "INSTAGRAM"YesMust have a connected integration.
toPhonestringOne of the twoE.164 phone number. The WhatsApp path.
toExternalChatIdstringOne of the twoChannel-native chat ID, for an existing conversation.
textstringConditionalFree-form body.
templateNamestringConditionalAn approved template's name.
templateVariablesstring[]NoPositional values for the template body.
externalIdstringNoYour id for this recipient. Binds the conversation to the contact carrying that externalId, creating it if there is none.
integrationIdstringConditionalWhich connected number or bot to start a new chat on. Required only when the org has several connected for that channel.
namestringNoDisplay name for a new WhatsApp contact. Trimmed, up to 200 characters.

Without name, a new contact is named by their phone number. Pass the name you already hold and the inbox reads properly from the first message instead of showing a number to your operators.

name replaces an existing name when one is set.

Pass externalId and you never have to reconcile ids afterwards: the conversation attaches to the right contact in the same call, and a later upsert_contact with the same externalId updates that same person. See keying contacts on your own id.

Output - the whole identity graph it just created, not only the message:

{
  chatId: string | null;          // the thread - pass this to send_message
  conversationId: string | null;  // the inbox row
  contactId: string | null;       // the person
  peopleId: string | null;        // the channel identity
  message: MessageDto | null;     // the message that went out
}

A cold start on WhatsApp creates the full chain - channel identity, contact, conversation, thread - and hands you every id at once, so the agent can carry the conversation on without a follow-up lookup.

Only WhatsApp can create a chat from a phone number, and outside the 24-hour window (or with no prior inbound) templateName is required. Telegram, Viber, Web and Instagram cannot initiate - the contact must message first.

Which Number the Message Goes Out On

An organisation can have several WhatsApp numbers, Telegram bots or Viber bots, so "which channel" is now answered explicitly rather than by picking the first one.

CaseWhich integration is used
An existing chatThe chat's own channel. The template and the 24-hour window are checked against that number, not the org's first one.
A new WhatsApp contact, one number connectedThat number, automatically.
A new WhatsApp contact, several numbers connectedYou must pass integrationId. The call errors and asks for it rather than guessing.

A foreign or not-connected integrationId is rejected. Get the ids from list_channels or the GraphQL channels query.

This changes behaviour for organisations with more than one WhatsApp number. A call that worked before, because the first number was picked for you, now errors until it passes integrationId. Single-number organisations are unaffected.

The existing-chat rule is the one that quietly fixes bugs: a reply in a chat belonging to your second number used to be window-checked against the first, which could reject a perfectly open conversation or let a closed one through. It now checks the number that actually owns the chat.

One trigger_conversation produces four webhook deliveries, not one: contact.created, conversation.created, chat.created and message.outbound.created.

Two consequences:

  • Mirroring: they arrive right after this call returns. Deduplicate against the ids the call gave you, or you will store the same thread several times.
  • Volume: a bulk trigger run fills your delivery log roughly 4× faster than the number of conversations you started. Size your endpoint's throughput and your log retention for that, not for one event per conversation.

MessageDto

The trimmed message shape the three message-returning tools use. It is deliberately not the GraphQL Message type - internal IDs, raw provider payloads and full user records are stripped.

{
  id: string;
  type: string;            // TEXT, MEDIA, …
  direction: "INBOUND" | "OUTBOUND";
  text?: string;
  caption?: string;
  createdAt?: string;
  deliveryStatus?: string;
  media: Array<{ id?: string; url?: string; type?: string }>;
  chatId?: string;
  replyToId?: string;
  from: { kind?: string; id?: string; name?: string } | null;   // see below
  externalMessageId: string | null;   // the provider's own id, e.g. a WhatsApp wamid
  errorCode: string | null;           // parsed provider error code, when there is one
  errorMessage: string | null;        // why a send failed
}

externalMessageId correlates a message with the provider's own records. errorCode and errorMessage say why a send failed without a second lookup - check them whenever deliveryStatus is failed.

Who sent it

from carries the sender kind, and whether you get it at all depends on the kind and on PII_READ:

SenderWithout PII_READWith PII_READ
Organisation (an API key with no acting operator)Returned, with nameReturned, with name
User (a human operator)null{ kind, id, name }
AIAssistant, Bot, Peoplenull{ kind, id }

Organisation is the exception: it is your own organisation, not a person, so it is not treated as personal data and comes back regardless of scope. Everything identifying a person sits behind PII_READ, and an operator's email is never included either way.

A provider-side send failure is not a tool error. send_message and trigger_conversation succeed with deliveryStatus: "FAILED". Have the agent check it rather than treating a non-error result as delivered.

On this page