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:
-
The Router validates your payload and routes it according to the currently active state in the relevant flow.
-
Depending on your flow and active state, messages can be delivered to one or more applications.
-
The Router may add metadata like
targetApplicationInfowhen 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
POSTrequests withapplication/json. -
Allowlist the Router IP
34.34.64.134in 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) -
conversationMessagesorconversationEvents: 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:
| Field | Description | Required |
|---|---|---|
| id | A hash created from the combination of client id, host id and channel | Yes |
| sessionId | The id for the current session | No |
| accountId | The id of logical account | Yes |
| channel | The medium being used for communication, e.g. WhatsApp or CXWebConversations | Yes |
| conversationClientId | The client id from which messages are sent | Yes |
| conversationHostId | The host id to which messages are sent | Yes |
| conversationClientName | The display name of the client on the channel | No |
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.
| Field | Description |
|---|---|
| applicationId | The 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:
| Field | Description | Required |
|---|---|---|
| $type | The type of the message, see Event types | Yes |
| id | The id of the event | No |
| createdOn | The timestamp (UTC) at which the event was created within the Conversational Router | No |
| channelNativeReference | A channelNativeId of a previous message, to which is replied | No |
NOTE: we do not support setting
channelNativeIdon events, so you can only refer to messages usingchannelNativeReference.
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" : {}
}
]
}
| Field | Description | Required |
|---|---|---|
| Reason | Reason for the session initialization | Yes |
| Context | Dictionary containing extra information passed along with the initialization | No |
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" : {}
}
]
}
| Field | Description | Required |
|---|---|---|
| Reason | Reason for the session termination | Yes |
| Context | Dictionary containing extra information passed along with the termination | No |
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" : {}
}
]
}
| Field | Description | Required |
|---|---|---|
| Context | Dictionary containing extra information, like web store referrer for Agent Inbox | No |
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
channelNativeReferenceproperty 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
}
]
}
| Field | Description | Required |
|---|---|---|
| channelNativeReference | The channel-native ID of the message being responded to (e.g. the wamid from an inbound WhatsApp webhook). | No |
| timeout | How long (in seconds) the typing indicator should be shown. If omitted, the channel default applies | No |
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:
| Field | Description | Required |
|---|---|---|
| $type | The type of the message, see Message types | Yes |
| id | The id of the message | No |
| direction | The direction of the message, ClientOriginated is used for MO messages while ClientTerminated is used for MT | Yes |
| createdOn | The timestamp (UTC) at which the message was created within the Conversational Router | No |
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"
}
]
}
| Field | Description | Required |
|---|---|---|
| text | The text | Yes |
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"
}
]
}
| Field | Description | Required |
|---|---|---|
| media | An object describing the media | Yes |
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"
}
]
}
| Field | Description | Required |
|---|---|---|
| location | An object describing the location | Yes |
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"
}
]
}
| Field | Description | Required |
|---|---|---|
| contacts | An object describing the different contacts to send | Yes |
| contactAddresses | A list of contact addresses related to the contact | No |
| birthday | Birthday of the contact | No |
| emailAddresses | list of email addresses related to the contact | No |
| name | Name information of the contact, formattedName being required | Partly |
| organization | The organization the contact is affiliated with | No |
| phoneNumbers | Phone numbers related to the contact | No |
| urls | Relevant URLs | No |
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"
}]
}
| Field | Description | Required |
|---|---|---|
| merchantName | The merchant name | Yes |
| description | A description of the product or service being purchased | Yes |
| orderReference | A reference for the product or service being purchased | Yes |
| recipientEmail | The recipient's email address | Yes |
| currencyCode | 3 character currency code according to ISO 4217 | Yes |
| recipientCountryCode | 2 character country code according to ISO 3166-1 Alpha 2 | No |
| languageCountryCode | 2 character country code according to ISO 639-1 Code | No |
| billingAddressRequired | Whether the billing address is required for this product or service | Yes |
| shippingContactRequired | Whether shipping contact is required for this product or service | Yes |
| lineItems | A list of objects describing the line items | Yes |
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"
}]
}
| Field | Description | Required |
|---|---|---|
| header | Apple Business Chat: title, max of 40 charactersWhatsApp: header | Yes |
| body | Apple Business Chat: subtitle, max of 40 charactersWhatsApp: body | Yes |
| buttonTitle | Apple Business Chat: -WhatsApp: button title | No |
| footer | Apple Business Chat: -WhatsApp: footer | ABC: NoWA: Yes |
| mediaUri | The media URI | ABC: YesWA: No |
| buttons | A list of button definitions, maximum for WhatsApp of 10 buttons) | Yes |
| buttons.id | Apple Business Chat: media URIWhatsApp: id, max of 200 characters) | Yes |
| buttons.title | Button title; WhatsApp: max of 24 characters | Yes |
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"
}]
}
| Field | Description | Required |
|---|---|---|
| header | The header for the reply buttons, max of 20 characters | No |
| body | The body for the reply button, max of 1024 characters | Yes |
| footer | The footer for the reply button, max of 60 characters | No |
| buttons | A list of button definitions, max of 3 buttons | Yes |
| buttons.id | Button id; WhatsApp: max of 256 characters | Yes |
| buttons.title | Button title, max of 20 characters | Yes |
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"
}]
}
| Field | Description | Required |
|---|---|---|
| url | An object describing the URL | Yes |
| media | An object describing the media | Yes |
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"
}]
}
| Field | Description | Required |
|---|---|---|
| text | The message sent as a reply to the story | Yes |
| url | The link to the story being replied to | Yes |
| reference | An id of the story being replied to | Yes |
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",
}
]
}
| Field | Description | Required |
|---|---|---|
| reference | External reference identifier for the message | No |
| cc | List of CC recipient email addresses | No |
| bcc | List of BCC recipient email addresses. Inbound BCC is always empty (mail protocol limitation, BCC is stripped before delivery) | No |
| subject | Email subject line | No |
| textBody | Plain text version of the email body | No |
| htmlBody | HTML version of the email body | No |
| attachments | List of file attachments | No |
Attachment
| Field | Description | Required |
|---|---|---|
| name | Filename including extension, e.g. "invoice.pdf" | Yes |
| content | Base64-encoded raw file content | Yes |
| contentType | MIME type of the attachment, e.g. "application/pdf", "image/png" | Yes |
| contentId | Content-ID (CID) for inline image attachments referenced in htmlBody; null for regular attachments | No |
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" : {}
}
]
}
| Field | Description | Required |
|---|---|---|
| PassthroughMessageType | Type of the passthrough message, currently only WATemplate | Yes |
| JsonContent | The JSON content as belonging to the type | Yes |