Skip to main content

Overview

TwoWay is a JSON-based format the Router uses to exchange data with external services. It defines a consistent envelope and payloads for both messages and events, so your custom service can participate in routed conversations just like any built‑in application. With TwoWay, you use the same contract across channels (for example, WhatsApp, SMS, Web Conversations).

Integration

Prerequisites​

Before start, make sure you have created TwoWay application. For all the details on how to create and configure it, see section TwoWay.

Sending messages​

To send data into the Router, POST TwoWay-formatted JSON to your TwoWay application’s inbound URL. You can copy the exact endpoint from your application from the frontend (the Copy Inbound URL action) or construct it yourself based on the info in the messages you're receiving. Include authentication on every request (see Authentication). Payloads must conform to the TwoWay schema which is described in section Format.

What happens next:

  1. The Router validates your payload and routes it according to the currently active state in the relevant flow.

  2. Depending on your flow and active state, messages can be delivered to one or more applications.

  3. The Router may add metadata like targetApplicationInfo when delivering to downstream applications, providing the info for talking back

Example​

Below is an example for sending a simple text message through the SMS channel. To see all available message types, please see section Message types.

Endpoint: POST https://api.cm.com/router/twoway/v1/accounts/00000000-0000-0000-0000-000000000000/applications/00000000-0000-0000-0000-000000000000

Header: X-CM-PRODUCTTOKEN: 00000000-0000-0000-0000-000000000000

Request body:

{
"chat": {
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "SMS",
"conversationClientId": "+31600000000",
"conversationHostId": "+31611111111",
},
"conversationMessages": [
{
"$type": "text",
"text": "Hello",
}
]
}

Receiving messages​

To receive data from the Router, implement an HTTPS JSON webhook. The Router will POST TwoWay-formatted requests to your message (and optionally events) endpoint(s). Your service should:

  • Accept POST requests with application/json.

  • Allowlist the Router IP 34.34.64.134 in your firewall if necessary.

Format

TwoWay defines a single, consistent envelope for everything the Conversational Router exchanges with your service. Every payload has the same top level and then carries either messages or events. This makes it easy to parse and to build logic that works across connections.

There are two payload kinds:

  • conversationMessages: The payload contains one or more messages (text, media, location, etc.).

  • conversationEvents: The payload contains one or more events (for example, a session ended, a client left/rejoined, or an agent was assigned).

Base​

Every TwoWay payload includes three parts:

  • chat: identifiers and context for the conversation (who is involved, on which channel)

  • conversationMessages or conversationEvents: the content of the message/event.

  • targetApplicationInfo (added by the Router on delivery): metadata containing the ID of the application to which the router routed the message.

chat property:

FieldDescriptionRequired
idA hash created from the combination of client id, host id and channelYes
sessionIdThe id for the current sessionNo
accountIdThe id of logical accountYes
channelThe medium being used for communication, e.g. WhatsApp or CXWebConversationsYes
conversationClientIdThe client id from which messages are sentYes
conversationHostIdThe host id to which messages are sentYes
conversationClientNameThe display name of the client on the channelNo

conversationMessages or conversationEvents properties: described in sections Events and Messages.

targetApplicationInfo property: an object containing information about the application to which the conversationMessages/conversationEvents were sent. It is added automatically by the Conversational Router.

FieldDescription
applicationIdThe id of the target application

Events​

Events communicate context and lifecycle information around a conversation rather than message content. They help participants understand what’s happening (for example, a session ended, a client became inactive, or an agent joined).

Base​

All events share these generic fields:

FieldDescriptionRequired
$typeThe type of the message, see Event typesYes
idThe id of the eventNo
createdOnThe timestamp (UTC) at which the event was created within the Conversational RouterNo
channelNativeReferenceA channelNativeId of a previous message, to which is repliedNo

NOTE: we do not support setting channelNativeId on events, so you can only refer to messages using channelNativeReference.

Event types​

Router events​

Emitted to inform you about Router-specific lifecycle changes (e.g., RouterSessionEnded). These are not accepted as inbound payloads to TwoWay; you receive them on your configured events webhook in the TwoWay application.

RouterSessionStarted​

Notifies you of the fact that the router session has been started for a certain chat

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationEvents": [
{
"$type": "routerSessionStarted",
"Reason": "reason",
"Context" : {}
}
]
}
FieldDescriptionRequired
ReasonReason for the session initializationYes
ContextDictionary containing extra information passed along with the initializationNo
RouterSessionEnded​

Notifies you of the fact that the router session has been reset for a certain chat

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationEvents": [
{
"$type": "routerSessionEnded",
"Reason": "reason",
"Context" : {}
}
]
}
FieldDescriptionRequired
ReasonReason for the session terminationYes
ContextDictionary containing extra information passed along with the terminationNo
ApplicationRoutingActivated​

Notifies the application receiving this event that messages and/or events will be sent its way from then on (according to the current state)

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationEvents": [
{
"$type": "applicationRoutingActivated",
"Context" : {}
}
]
}
FieldDescriptionRequired
ContextDictionary containing extra information, like web store referrer for Agent InboxNo
ApplicationRoutingDeactivated​

Notifies the application receiving this event that messages and/or events will not be sent its way anymore from then on (according to the current state)

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationEvents": [
{
"$type": "applicationRoutingDeactivated",
}
]
}

Exernal events​

Used by external parties to communicate intent (e.g., ClientLeft, ClientRejoined, AgentAssigned). These can be sent to TwoWay like messages to inform other participants in the flow.

ClientLeft​

Signifies the client has left the conversation. This can also be considered an inactive state

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents": [
{
"$type": "clientLeft"
}
]
}
ClientRejoined​

Signifies the client rejoined the conversation.

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents": [
{
"$type": "clientRejoined"
}
]
}
AgentAssigned​

Signifies an agent assigned to the conversation.

{
"chat":{
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents":[
{
"$type": "agentAssigned",
"AssignedAgent":{
"firstName": "Geertruida",
"lastName": "Flodder",
"alias": "Mevrouw Flodder",
"avatarUrl": "https://..."
}
}
]
}
FeedbackRequested​

Signifies feedback on the conversation or a specific message is requested.

{
"chat":{
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents":[
{
"$type": "feedbackRequested"
}
]
}
FeedbackSubmitted​

Signifies feedback on the conversation or a specific message has been submitted.

{
"chat":{
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents":[
{
"$type": "feedbackSubmitted",
"rating": {
"min": 1,
"max": 5,
"submitted": 5
},
"comment": "Helpful AI responses, thanks!",
"channelNativeReference": "a_reference_to_a_previous_message"
}
]
}

📘 Linking feedback to a specific message

You can optionally use the channelNativeReference property to link the feedback to a specific message.

TypingStarted​

Signals that the reply is being composed to a message.

📘 Supported channels

Currently only supported on WhatsApp. The event is accepted on all channels but silently dropped for unsupported ones.

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents": [
{
"$type": "typingStarted",
"channelNativeReference": "wamid.HBgLMzE2MTkxNzkxNDUVAgASGCA5NzYxNUE3QjlCQkM3",
"timeout": 15
}
]
}
FieldDescriptionRequired
channelNativeReferenceThe channel-native ID of the message being responded to (e.g. the wamid from an inbound WhatsApp webhook).No
timeoutHow long (in seconds) the typing indicator should be shown. If omitted, the channel default appliesNo
TypingStopped​

Signals that the reply has stopped being composed to a message.

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678"
},
"conversationEvents": [
{
"$type": "typingStopped"
}
]
}

Messages​

Messages contain the actual content exchanged in the conversation (for example, text, media, locations, structured buttons).

Base​

All messages share generic fields:

FieldDescriptionRequired
$typeThe type of the message, see Message typesYes
idThe id of the messageNo
directionThe direction of the message, ClientOriginated is used for MO messages while ClientTerminated is used for MTYes
createdOnThe timestamp (UTC) at which the message was created within the Conversational RouterNo

Message types​

Each message type defines its own required fields in addition to the generic ones.

Text​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "SMS",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [
{
"$type": "text",
"text": "Hi",
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}
]
}
FieldDescriptionRequired
textThe textYes

Media​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [
{
"$type": "media",
"media": {
"name": "Image name",
"uri": "http://example.com/my-image.png",
"mimeType": "image/jpeg"
},
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}
]
}
FieldDescriptionRequired
mediaAn object describing the mediaYes

Location​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [
{
"$type": "location",
"location": {
"latitude": 51.6035675548752,
"longitude": 4.77079096460324,
"label": "CM.com",
"searchQuery": "Konijnenberg 30, Breda, Noord-Brabant 4825 BD"
},
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}
]
}
FieldDescriptionRequired
locationAn object describing the locationYes

Contact​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [
{
"$type": "contact",
"contacts": [
{
"contactAddresses": [
{
"city": "Breda",
"country": "Netherlands",
"countryCode": "NL",
"state": null,
"street": "Konijnenberg 30",
"type": "WORK",
"zipCode": "4825 BD"
}
],
"birthday": null,
"emailAddresses": [],
"name": {
"firstName": null,
"lastName": "Your last name",
"middleName": null,
"namePrefix": null,
"nameSuffix": null,
"formattedName": "CM Developer"
},
"organization": null,
"phoneNumbers": [],
"urls": []
}
],
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}
]
}
FieldDescriptionRequired
contactsAn object describing the different contacts to sendYes
contactAddressesA list of contact addresses related to the contactNo
birthdayBirthday of the contactNo
emailAddresseslist of email addresses related to the contactNo
nameName information of the contact, formattedName being requiredPartly
organizationThe organization the contact is affiliated withNo
phoneNumbersPhone numbers related to the contactNo
urlsRelevant URLsNo

Apple Pay​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [{
"$type": "applePay",
"merchantName": "Merchant",
"description": "Test payment",
"orderReference": "00000000-0000-0000-0000-000000000000",
"recipientEmail": "[email protected]",
"currencyCode": "eur",
"recipientCountryCode": "nl",
"languageCountryCode": null,
"billingAddressRequired": true,
"shippingContactRequired": false,
"lineItems": [{
"label": "My product",
"type": "My type",
"amount": 1.10
}],
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}]
}
FieldDescriptionRequired
merchantNameThe merchant nameYes
descriptionA description of the product or service being purchasedYes
orderReferenceA reference for the product or service being purchasedYes
recipientEmailThe recipient's email addressYes
currencyCode3 character currency code according to ISO 4217Yes
recipientCountryCode2 character country code according to ISO 3166-1 Alpha 2No
languageCountryCode2 character country code according to ISO 639-1 CodeNo
billingAddressRequiredWhether the billing address is required for this product or serviceYes
shippingContactRequiredWhether shipping contact is required for this product or serviceYes
lineItemsA list of objects describing the line itemsYes

Listpicker​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [{
"$type": "listPicker",
"header": "My title",
"body": "Select an option",
"buttonTitle": "Options",
"footer": "My footer",
"mediaUri": "https://example.com/media.jpg",
"buttons": [
{
"id": "1",
"title": "list option 1"
},
{
"id": "2",
"title": "list option 2"
}
],
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}]
}
FieldDescriptionRequired
headerApple Business Chat: title, max of 40 charactersWhatsApp: headerYes
bodyApple Business Chat: subtitle, max of 40 charactersWhatsApp: bodyYes
buttonTitleApple Business Chat: -WhatsApp: button titleNo
footerApple Business Chat: -WhatsApp: footerABC: NoWA: Yes
mediaUriThe media URIABC: YesWA: No
buttonsA list of button definitions, maximum for WhatsApp of 10 buttons)Yes
buttons.idApple Business Chat: media URIWhatsApp: id, max of 200 characters)Yes
buttons.titleButton title; WhatsApp: max of 24 charactersYes

Reply buttons​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [{
"$type": "replyButtons",
"header": "My title",
"body": "Select an option",
"footer": "My footer",
"buttons": [
{
"id": "1",
"title": "option 1"
},
{
"id": "2",
"title": "option 2"
}
],
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}]
}
FieldDescriptionRequired
headerThe header for the reply buttons, max of 20 charactersNo
bodyThe body for the reply button, max of 1024 charactersYes
footerThe footer for the reply button, max of 60 charactersNo
buttonsA list of button definitions, max of 3 buttonsYes
buttons.idButton id; WhatsApp: max of 256 charactersYes
buttons.titleButton title, max of 20 charactersYes

Text with URL​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [{
"$type": "textWithUrl",
"text": "Example",
"url": {
"uri": "https://example.com/1",
"label": "test"
},
"media": {
"name": "test",
"uri": "https://example.com/media.jpg",
"mimeType": "image/jpg"
},
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-07-12T06:59:59.2158043+00:00"
}]
}
FieldDescriptionRequired
urlAn object describing the URLYes
mediaAn object describing the mediaYes

StoryReply​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "Instagram",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": "Bob"
},
"conversationMessages": [{
"$type": "storyReply",
"text": "That's a great looking apple pie!",
"url": "https://some_url",
"reference": "some_reference",
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2022-06-24T06:59:59.2158043+00:00"
}]
}
FieldDescriptionRequired
textThe message sent as a reply to the storyYes
urlThe link to the story being replied toYes
referenceAn id of the story being replied toYes

Email​

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "Email",
"conversationClientId": "[email protected]",
"conversationHostId": "[email protected]",
"conversationClientName": null
},
"conversationMessages": [
{
"$type": "email",
"reference": "unique-reference-string",
"cc": ["[email protected]"],
"bcc": ["[email protected]"],
"subject": "Question about my order",
"textBody": "Hi, I have a question about my order #12345.",
"htmlBody": "<p>Hi, I have a question about my order <strong>#12345</strong>.</p>",
"attachments": [
{
"name": "invoice.pdf",
"content": "base64encodedcontent==",
"contentType": "application/pdf",
"contentId": null
}
],
"id": "00000000-0000-0000-0000-000000000000",
"direction": "ClientOriginated",
"createdOn": "2021-01-26T18:52:02+00:00",
}
]
}
FieldDescriptionRequired
referenceExternal reference identifier for the messageNo
ccList of CC recipient email addressesNo
bccList of BCC recipient email addresses. Inbound BCC is always empty (mail protocol limitation, BCC is stripped before delivery)No
subjectEmail subject lineNo
textBodyPlain text version of the email bodyNo
htmlBodyHTML version of the email bodyNo
attachmentsList of file attachmentsNo

Attachment

FieldDescriptionRequired
nameFilename including extension, e.g. "invoice.pdf"Yes
contentBase64-encoded raw file contentYes
contentTypeMIME type of the attachment, e.g. "application/pdf", "image/png"Yes
contentIdContent-ID (CID) for inline image attachments referenced in htmlBody; null for regular attachmentsNo

Passthrough​

Passes through the message without any processing done on it.

{
"chat": {
"id": "00000000-0000-0000-0000-000000000000",
"sessionId": "20210126185202",
"accountId": "00000000-0000-0000-0000-000000000000",
"channel": "WhatsApp",
"conversationClientId": "+316012345678",
"conversationHostId": "00316012345678",
"conversationClientName": null
},
"conversationMessages": [
{
"$type": "passthrough",
"PassthroughMessageType": "WATemplate",
"JsonContent" : {}
}
]
}
FieldDescriptionRequired
PassthroughMessageTypeType of the passthrough message, currently only WATemplateYes
JsonContentThe JSON content as belonging to the typeYes