# Conversation API callbacks This page covers registering webhooks and listening to callbacks from the Sinch Conversation API. ## Testing your solution Certain webhook test services, like [https://webhook.site/](https://webhook.site/), Beeceptor, and [https://collect2.com/](https://collect2.com/), can be used when trialing a Conversation API solution. However, these services should only be used for testing purposes. Using the Conversation API can generate high volumes of callback traffic (including multiple delivery states per message, MO reply messages, and more), which can easily exceed the TPS limits of certain webhook services. These high volumes can result in queuing, latency, and rejections of callbacks, which can negatively impact the performance of your Conversation API solution. **Additionally, sensitive information may be included in the callback, including message content and contact information.** Ensure that you use a scalable and secure callback/webhook processor after your initial testing is complete. Note: When using [https://webhook.site/](https://webhook.site/), note that callback delivery retries will not be performed if the initial webhook dispatch is undeliverable and receives a response other than `200OK`. This constraint only exists for [https://webhook.site/](https://webhook.site/) at this time. ## Webhook Management The Conversation API delivers contact messages, delivery receipts for app messages and various notifications through callbacks. API clients can create fine-grained subscriptions for up-to 5 webhooks per conversation API app through the [Sinch Portal](https://dashboard.sinch.com/convapi/apps) or by using the `/webhooks` management endpoint. Each webhook represents a subscription for events defined by a list of triggers. The events are delivered by the Conversation API to the webhook target URL. The callbacks are signed with the webhook secret if such is provided. The signature can be used to verify the authenticity and integrity of the callbacks. ### Webhook Triggers Each webhook can subscribe to one or more of the following triggers: - `MESSAGE_INBOUND` - subscribe to inbound messages from end users on the underlying channels. - `EVENT_INBOUND` - subscribe to inbound events (for example, composing events) from end users on the underlying channels. - `MESSAGE_DELIVERY` - subscribe to [delivery receipts](#message-delivery-receipt) for app messages sent on Conversation API channels. - `MESSAGE_SUBMIT` - subscribe to [message submission notifications](#message-submit-notification) for app messages sent on Conversation API channels. - `EVENT_DELIVERY` - subscribe to [delivery receipts](#event-delivery-receipt) for events sent on Conversation API channels. - `CONVERSATION_START` - subscribe to the [conversation start](#conversation-start) events. - `CONVERSATION_STOP` - subscribe to [conversation stop](#conversation-stop) events. - `CONVERSATION_DELETE` - subscribe to conversation deleted events. - `CONTACT_CREATE` - subscribe to [contact create](#contact-create) events. - `CONTACT_DELETE` - subscribe to [contact delete](#contact-delete) events. - `CONTACT_MERGE` - subscribe to [contact merge](#contact-merge) events. - `CONTACT_UPDATE` - subscribe to [contact update](#contact-update) events. - `CAPABILITY` - this trigger is used to receive the results of asynchronous [capability checks](#capability-check). - `OPT_IN` - subscribe to [opt-in events](#opt-in). - `OPT_OUT` - subscribe to [opt-out events](#opt-out). - `CONTACT_IDENTITIES_DUPLICATION` - subscribe to [notifications about contact identity duplications](#contact-identities-duplication-notification) found during message or event processing. - `CHANNEL_EVENT` - subscribe to [channel event](#channel-event) notifications. - `RECORD_NOTIFICATION` - subscribe to record notifications. - `BATCH_STATUS_UPDATE` - subscribe to [batch status update](#batch-status-update) notifications. - `UNSUPPORTED` - subscribe to receive [channel callbacks that are not natively supported by the Conversation API](#unsupported-callback). - `UNSPECIFIED_TRIGGER`: using this value will cause errors. ### Smart Conversations Triggers In addition to the standard webhooks, you may also configure your solution to subscribe to the `SMART_CONVERSATION` or `MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION` webhooks. These triggers allows you to subscribe to payloads that: - Provide machine learning analyses of inbound messages from end users on the underlying channels (in the case of the `SMART_CONVERSATION` webhook) - Deliver redacted versions of inbound messages (in the case of the `MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION` webhook) #### Smart Conversation trigger The `SMART_CONVERSATION` webhook delivers the results of the machine learning analyses. For information on the structure of the callback, see [Smart Conversations callbacks](/docs/smart-conversations/callbacks/#smart-conversations-trigger). #### Message Inbound Smart Conversation Redaction trigger The `MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION` webhook delivers messages as the `MESSAGE_INBOUND` webhook does, but the content of the message has been analyzed and masked (or blocked, in some cases) based on the results of the analysis. For information on the structure of the callback, see [Message Inbound Smart Conversation Redaction callback](/docs/smart-conversations/callbacks/#message-inbound-smart-conversation-redaction-trigger) ### Creating webhooks using the API The snippet below demonstrates how to create a webhook using the management API. The webhook registers a POST capable endpoint with URL `{{WEBHOOK_URL}}` to receive inbound messages and delivery receipts for app with ID `{{APP_ID}}` and project with ID `{{PROJECT_ID}}`. ```shell curl -X POST \ 'https://eu.conversation.api.sinch.com/v1/projects/{{PROJECT_ID}}/webhooks' \ -H 'Content-Type: application/json' --header 'Authorization: Bearer {{ACCESS_TOKEN}}' \ -d '{ "app_id": "{{APP_ID}}", "target": "{{WEBHOOK_URL}}", "target_type": "HTTP", "triggers": ["MESSAGE_DELIVERY", "MESSAGE_INBOUND"] }' ``` ## Callback delivery and retries When a callback is successfully delivered, no retries are necessary. A callback is considered successfully delivered when your server responds with an HTTP status code in the **200–299** range. | Status Code Range | Description | | --- | --- | | 200–299 | Success – no retry needed | Any response outside this range may trigger retries. The Conversation API uses an exponential backoff retry mechanism to ensure reliable callback delivery. The system will retry a reasonable number of times, with each retry occurring at progressively longer intervals until the maximum retry period is reached. Retries are attempted when failures are considered temporary or potentially recoverable. ### When the system retries The following sections describe the secnarios in which the system will retry callbacks. #### On connection or delivery failures Retries are automatically triggered in the following situations: - Temporary **network issues** - **Timeouts** while connecting or waiting for a response - **Failures when fetching an OAuth2 access token** (only applies **if you've configured your webhook trigger to use OAuth2 authentication**; e.g., failures due to **invalid**, **expired**, or **misconfigured credentials**) These issues are considered **temporary** and are expected to succeed on retry. #### On specific HTTP status codes Retries also occur when the callback endpoint responds with: - **5xx (500–599)**, which indicate temporary server errors - **429 Too Many Requests**, which indicates rate limiting - **401 / 403**, but **only when attempting to fetch an OAuth2 token**. This is often a sign that credentials were recently updated and should be retried. ### When the system doesn't retry Callbacks are not retried in the following situations. #### 4xx status codes Callbacks are not retried upon receipt of **4xx status codes** (except **429**), including: - **400 Bad Request** - **403 Forbidden** (outside of OAuth2 flow) - **404 Not Found** - **401 Unauthorized**, unless it occurred during OAuth2 token fetch Note: Status code **429** does trigger retries. Status codes **401** and **403** may trigger retries under specific circumstances. See [On specific HTTP status codes](#on-specific-http-status-codes) for more information. #### Known test or sandbox domains Callbacks made to known test or sandbox domains are not retried. Such domains include: - webhook.site - collect2.com - ngrok.app - ngrok.dev - ngrok-free.app - ngrok-free.dev - ngrok.io ### Behavior matrix Below is a table displaying the behavior described in the previous sections: | Condition | Retry Attempted? | Notes | | --- | --- | --- | | HTTP **2xx** | ❌ | Treated as success, so no retries required | | Temporary network failure or timeout | ✅ | Connection or delivery issue | | OAuth2 token fetch failure | ✅ | Token might be refreshed | | HTTP **5xx** | ✅ | Temporary server issue | | HTTP **429** Too Many Requests | ✅ | Throttling or rate limit | | HTTP **401** / **403** during token fetch | ✅ | Retry allowed | | HTTP **401** / **403** outside token fetch | ❌ | Treated as permanent failure | | **HTTP 400 Bad Request** | ❌ | Treated as permanent failure | | Callback to test domains (e.g., ngrok.io) | ❌ | Retry skipped entirely | ### Behavior summary Below is a quick summary of the behavior described in the previous sections: - **2xx status codes** are treated as success, so no retries are needed - **5xx status codes**, **429 Too Many Requests**, and specific **401/403 errors during OAuth2 token fetch** are retried - Temporary network issues, timeouts, and OAuth2 token fetch failures (if configured) are also retried - Other **4xx** errors and callbacks to known test domains are **not retried** ## Idempotency, duplicate callbacks and callback ordering API implementations must be idempotent to handle potential duplicate callbacks. While Sinch employs best-effort deduplication, duplicates may still occur because of: - Network issues, such as lost connections or timeouts. - A registered webhook failing to respond correctly, which will trigger retries. - Unexpected failures in transmission paths beyond Sinch's control. ## Callback ordering Sinch attempts to preserve callback order; however, because of retries and network conditions, exact ordering is not guaranteed. If strict ordering is required, implementing a client-side state machine is recommended. This approach ensures that events are processed in the correct sequence, even if callbacks arrive out of order because of retries or network delays. ## Required Throughput Per Second (TPS) for registered webhooks The performance of a registered webhook must align with the expected number of incoming callbacks. The required ingestion rate is calculated as follows: ```Plaintext Required TPS = (Expected callback ingestion rate) × (Number of webhook triggers configured) ``` ### Example: `MESSAGE_SUBMIT` trigger If only the `MESSAGE_SUBMIT` event is configured, and the expected rate is **100 Message Submit notifications per second** with a single webhook trigger, the required TPS is: ```Plaintext 100 (which represents the expected ingestion rate) × 1 (which represents the single MESSAGE_SUBMIT webhook trigger) = 100 TPS ``` In this case, **your webhook processing server** must be capable of handling **100 TPS**. ### Example: Delivery Reports (DR) and the `MESSAGE_DELIVERY` trigger Delivery reports (which can have the `QUEUED_ON_CHANNEL`, `READ`, and either `SUCCESS` or `FAILURE` statuses) are generated per message. If this trigger is configured, each message typically results in **three** DR callbacks. Using the same assumptions (an expected message ingestion rate of **100 TPS** and a single webhook trigger) the required TPS for delivery reports is: ```Plaintext 100 (which represents the expected ingestion rate) × 3 (which represents the number of callbacks per DR) × 1 (which represents the single MESSAGE_DELIVERY webhook trigger) = 300 TPS ``` In this scenario, **your webhook processing server** must handle **300 TPS** to process all DR callbacks efficiently. ### Load Testing Your Server for Expected Throughput Per Second (TPS) To ensure that your webhook endpoint can handle the required throughput per second (TPS), it is recommended to perform a load test using Apache Benchmark (`ab`). This section outlines how to install `ab` on different operating systems and execute a load test to verify your server's performance. #### Installing Apache Benchmark (`ab`) on macOS ```shell brew install httpd ``` The `ab` command will be available under `/usr/local/bin/ab` or `/opt/homebrew/bin/ab` depending on your architecture. #### Installing Apache Benchmark (`ab`) on Debian/Ubuntu ```shell sudo apt update && sudo apt install apache2-utils -y ``` #### Installing Apache Benchmark (`ab`) On Windows (Using WSL) ```shell sudo apt update && sudo apt install apache2-utils -y ``` Alternatively, install the Windows Subsystem for Linux (WSL) and follow the Linux instructions. #### Running the Load Test To simulate the load, use the following command: ```shell ab -t 300 -c 100 -T 'application/json' -p payload.json https://your-webhook-endpoint.com/webhook ``` The various components of the command are described below: - `-t 300`: Total duration of the test in seconds (5 minutes) - `-c 100`: Number of concurrent requests - `-T 'application/json'`: Specifies the content type - `-p payload.json`: Specifies the file containing the test payload - `https://your-webhook-endpoint.com/webhook`: Replace with the actual webhook URL #### Example Payload (Message Delivery Receipt) Save the following JSON payload into a file named `payload.json`: ```json { "app_id": "01EB37HMH1M6SV18BSNS3G135H", "accepted_time": "2020-11-17T15:09:11.659Z", "event_time": "2020-11-17T15:09:13.267185Z", "project_id": "c36f3d3d-1513-2edd-ae42-11995557ff61", "message_delivery_report": { "message_id": "01EQBC1A3BEK731GY4YXEN0C2R", "conversation_id": "01EPYATA64TMNZ1FV02JKF12JF", "status": "QUEUED_ON_CHANNEL", "channel_identity": { "channel": "MESSENGER", "identity": "2734085512340733", "app_id": "01EB27HMH1M6SV18ASNS3G135H" }, "contact_id": "01EXA07N79THJ20WSN6AS30TMW", "metadata": "", "processing_mode": "CONVERSATION" }, "message_metadata": "" } ``` Note: Whenever possible, it is best to use real payloads whenever instead of the example payload provided above. Real payloads will better reflect the actual data received in production, as example payloads may contain fewer fields or simplified data structures. To obtain a real payload, refer to the **Testing your solution** section and extract received payloads using a tool such as [Webhook.site](https://webhook.site/). #### Adjusting Concurrency for Higher Throughput Your webhook server should be able to support a higher concurrency to improve throughput. The `-c 100` value is used as an example, and it should be modified accordingly during server tests to evaluate the server's capability in a high-throughput environment. #### Interpreting the Results After running the test, `ab` will provide output similar to: ```text ... Requests per second: 1876.38 [#/sec] (mean) Time per request: 53.294 [ms] (mean) ... ``` Ensure that your server meets or exceeds the required TPS threshold. If the performance is below expectations, consider optimizing your server infrastructure, increasing available resources, or implementing asynchronous processing mechanisms. ### Best practices Sinch implements buffering to manage temporary slowdowns, but if the webhook endpoint cannot process callbacks efficiently, the buffer may overflow, leading to discarded callbacks. In order to ensure reliability: - Webhooks should scale to meet peak traffic demands. - You should implement asynchronous processing to avoid blocking requests. - Use HTTP status codes (`2xx`) to acknowledge callbacks promptly and prevent unnecessary retries. ## Authenticating Callbacks A callback is a `POST` request with a notification made by the Sinch Conversation API to a URI of your choosing. Because the Conversation API is making a request to your system, Sinch will authenticate against **your system**. Therefore, you must configure the authentication method to be used by Sinch. In general, the Conversation API can authenticate against your system by using: - [**OAuth2.0**](#oauth20) is a standard form of authentication that provides enhanced security through the inclusion of an additional token-provision endpoint. The short-lived access tokens, which are only provided after Sinch successfully authenticates against the token-provision endpoint, reduce the risk of credentials being intercepted. Additionally, client IDs and secrets do not need to be included in calls made to your webhook endpoints; they only need to be included when retrieving a token from the token-provision endpoint. OAuth2.0 can be utilised by the Conversation API by configuring the corresponding webhook with the requisite client ID, client secret, and the URL to fetch OAuth access token. Note that, because callbacks are made by Sinch to a URI of your choosing (rather than a call made by your system to Sinch's endpoints), the access token must be accepted by **your system**. Additionally, you (or your OAuth2.0 service provider) will define the client ID and secret that Sinch will use to retrieve tokens. OAuth2.0 is recommended for most production environments. - Additionally, Conversation API webhooks support [**HMAC**](#hmac) (or hash-based message authentication code). HMAC is a cryptographic authentication technique that uses a hash function and a secret key. HMAC can be configured alongside OAuth2.0. When using HMAC validation, a secret token is included in the call, increasing overall security and allowing for payload integrity validation. Note: When configuring callback authentication, **we recommend using OAuth2.0 authentication**. OAuth2.0 provides more safety through the use of short-lived acess tokens. More information regarding OAuth2.0 and HMAC are provided below. ### OAuth2.0 Achieve more secure webhook authentication by providing OAuth 2.0 access tokens. Access tokens are short lived. Typically, they will only last one hour. This is done to help keep your data (and ours) safer. This is accomplished, in part, because OAuth2.0 helps ensure that requests arriving at your webhook are from the Conversation API, and not a third-party impersonating the Conversation API. When exchanging credentials, your system (or the service you use for OAuth2.0 token provision) will provide Sinch with a long string called an access token. This access token will serve as Sinch's bearer token in the authorization header of webhook calls. When configuring OAuth2.0 for your system, you can either: - Configure your own OAuth token-provision endpoint, as is done in the [example flow we provide](#sample-oauth2.0-flow). - [Make use of a service](#making-use-of-an-oauth2.0-provision-service) that supplies you with an OAuth token-provision endpoint. Either approach is acceptable as long as the endpoint can receive Sinch's client ID and secret and provide an OAuth token. Note that, regardless of your approach, you must [create a corresponding Conversation API webhook](https://community.sinch.com/t5/Conversation-API/How-to-add-a-webhook-to-a-Conversation-API-app/ta-p/8100) and populate it with the client ID, client secret, and token-provision endpoint Sinch is to use to retrieve OAuth tokens. #### OAuth2.0 token request options When configuring OAuth2.0 for your webhook, you can optionally provide additional fields that control how Sinch requests the access token from your OAuth provider. The optional fields are described below: | Field | Description | | --- | --- | | `scope` | Space-separated string per RFC 6749. Used to limit the access granted by the token. Max length: 1024 characters. If blank or omitted, it is not included in the token request. | | `response_type` | This is generally omitted when granting `client_credentals`. If you do provide a value, it will be forwarded to your OAuth provider as-is. Max length: 64 characters. | | `token_request_type` | Controls how `client_id` and `client_secret` are sent to the token endpoint. Options are `BASIC` (default): Sent using the HTTP Authorization header (Basic auth). In this case, `client_id` and `client_secret` are NOT included in the form body.`FORM`: Sent in the form body as `client_id` and `client_secret`. No Authorization header is added.If this field is not set or unrecognized, `BASIC` is used by default. | These options only affect how Sinch obtains the access token. All webhook requests sent to your target will include the resulting access token in the HTTP Authorization header as a Bearer token. #### Making use of an OAuth2.0 provision service Below is a general guide to setting up an account with an OAuth2.0 provider: 1. Choose an OAuth2.0 provider. You may use any provider that successfully and safely provides OAuth access tokens. Create an account with the provider if necessary. 2. Select an OAuth2.0 product offering from the provider. 3. Configure a new OAuth2.0 application. Ensure you select an application that allows APIs to communicate with one another (i.e., a "machine to machine" application). This will allow the Conversation API to automatically communicate with your OAuth2.0 application. 4. Complete the application creation process. This may involve selecting or configuring a token management API, managing permissions, defining domains, establishing client IDs and secrets, reviewing details, and running tests using sample code to ensure the token-provision endpoint is working correctly. 5. After the application is created, locate the corresponding client information Sinch will use to authenticate with the token-provision endpoint. This information is required by the Conversation API. This includes the domain/endpoint against which the Conversation API will authenticate and the client ID and secret used to authenticate against the endpoint. 6. [Create a Conversation API webhook](https://community.sinch.com/t5/Conversation-API/How-to-add-a-webhook-to-a-Conversation-API-app/ta-p/8100) and populate it with the information recorded in the previous step. Once your webhook is configured, you'll need to ensure your system can receive and parse the payload of the callback. #### Sample OAuth2.0 flow You may implement an OAuth2.0 client credentials flow in the way that best suits your organization. We have provided an example of an OAuth2.0 flow (in Node.js, though the general principals can be applied to any language) below. Note that, in this case, token provision is actually managed by the sample code, rather than a [third-party service](#making-use-of-an-oauth20-provision-service): ```javascript const logger = (req, res, next) => { console.dir(req.body, { depth: null }); next(); }; import express from 'express'; import bodyParser from 'body-parser'; const app = express(); import jwt from 'jsonwebtoken'; const secret = 'YOUR_OAUTH_secret_string'; const clientId = 'YOUR_client_ID'; const clientSecret = 'YOUR_client_secret'; // Token endpoint. Issues new tokens which Conversation API will use at callbacks for the webhook endpoint: app.post('/token', (req, res) => { const auth = req.header('Authorization'); if (!auth?.startsWith('Basic ')) { res.sendSatus(401); } const buff = Buffer.from(auth.slice(6), 'base64'); const text = buff.toString('utf-8'); if (text === `${clientId}:${clientSecret}`) { const now = Math.floor(Date.now() / 1000); res.setHeader('Content-Type', 'application/json').status(200).end(JSON.stringify({ access_token: jwt.sign({ iat: now, exp: now + 3600, foo: 'bar' }, secret), token_type: 'Bearer', expires_in: 3600, })); } else { res.sendStatus(401); } }) // Webhook endpoint: app.post('/', bodyParser(), logger, (req, res) => { const token = req.header('Authorization'); if (!token?.startsWith('Bearer ')) { res.sendStatus(401); } try { jwt.verify(token.slice(7), secret); // Here you can accept the payload of the callback res.sendStatus(200); } catch { res.sendStatus(401); } }) const port = 3000; app.listen(port, () => { console.log(`listening on port ${port}`) }) ``` ### HMAC HMAC validation can be used alone or with OAuth2.0. Conversation API callbacks triggered by a registered webhook, with a `secret` set, will contain the following headers: | Field | Description | | --- | --- | | x-sinch-webhook-signature-timestamp | Timestamp, in UTC, of when the signature was computed | | x-sinch-webhook-signature-nonce | A unique nonce that can be used to protect against reply attacks | | x-sinch-webhook-signature-algorithm | The HMAC signature algorithm that was used to compute the signature. For now it's set to HmacSHA256. | | x-sinch-webhook-signature | The signature of the raw HTTP body, the timestamp, and the nonce as computed by the Conversation API. | The receiver of signed callbacks should execute the code found in the following example, after replacing the commented values in the example with their values: ```javascript var crypto = require('crypto') var secret = 'foo_secret1234' // secret used when creating a webhook; replace with your own value //actual raw message; replace with your own value var rawBody = '{"app_id":"","accepted_time":"2021-10-18T17:49:13.813615Z","project_id":"e2df3a34-a71b-4448-9db5-a8d2baad28e4","contact_create_notification":{"contact":{"id":"01FJA8B466Y0R2GNXD78MD9SM1","channel_identities":[{"channel":"SMS","identity":"48123456789","app_id":""}],"display_name":"New Test Contact","email":"new.contact@email.com","external_id":"","metadata":"","language":"EN_US"}},"message_metadata":""}' var nonce = '01FJA8B4A7BM43YGWSG9GBV067' //x-sinch-webhook-signature-nonce; replace with your own value var timestamp= 1634579353 //x-sinch-webhook-signature-timestamp; replace with your own value var signedData = rawBody + '.' + nonce + '.' + timestamp var signature = crypto.createHmac('sha256', secret).update(signedData).digest('base64') console.log("calculated-signature : " + signature) console.log("x-sinch-webhook-signature: 6bpJoRmFoXVjfJIVglMoJzYXxnoxRujzR4k2GOXewOE=") ``` ```python import base64 import hashlib, hmac secret = 'foo_secret1234' #secret used when creating a webhook; replace with your own value #actual raw message; replace with your own value body = '{"app_id":"","accepted_time":"2021-10-18T17:49:13.813615Z","project_id":"e2df3a34-a71b-4448-9db5-a8d2baad28e4","contact_create_notification":{"contact":{"id":"01FJA8B466Y0R2GNXD78MD9SM1","channel_identities":[{"channel":"SMS","identity":"48123456789","app_id":""}],"display_name":"New Test Contact","email":"new.contact@email.com","external_id":"","metadata":"","language":"EN_US"}},"message_metadata":""}' nonce = '01FJA8B4A7BM43YGWSG9GBV067' #x-sinch-webhook-signature-nonce; replace with your own value timestamp= 1634579353 #x-sinch-webhook-signature-timestamp; replace with your own value signed_data = body + '.' + nonce + '.' + str(timestamp) digest = hmac.new(secret.encode('utf-8'), signed_data.encode('utf-8'), hashlib.sha256).digest() print('calculated signature : ' + base64.b64encode(digest).decode()) print('x-sinch-webhook-signature: 6bpJoRmFoXVjfJIVglMoJzYXxnoxRujzR4k2GOXewOE=') ``` Note: HMAC signature validation is sensitive to white spaces, so any white space characters included in the message will impact the final digest. ## Callback Format Each callback dispatched by Conversation API has a JSON payload with the following top-level properties: | Field | Type | Description | | --- | --- | --- | | project_id | string | The project ID of the app which has subscribed for the callback. | | app_id | string | Id of the subscribed app. | | accepted_time | ISO 8601 timestamp | Timestamp marking when the channel callback was accepted/received by the Conversation API. | | event_time | ISO 8601 timestamp | Timestamp of the event as provided by the underlying channels. | | message_metadata | string | Context-dependent metadata. Refer to specific callback's documentation for exact information provided. | | correlation_id | string | The value provided in field [`correlation_id`](/docs/conversation/api-reference/conversation/messages/messages_sendmessage#messages/messages_sendmessage/t=request&path=correlation_id) of a send message request. | | channel_metadata | object | Additional metadata that might be provided by a channel. See specific channel's documentation for details. | Each specific callback type adds additional properties to the payload which are described in detail in the section below. ## Inbound Message This callback delivers contact (end-user) messages to the API clients. The message details are given in a top level `message` field (or `message_redaction` in case of [redacted messages](#redacted-messages)). It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | id | string | The message ID. | | direction | string | The direction of the message, it's always `TO_APP` for contact messages. | | contact_message | object | The content of the message. See [Contact Message](/docs/conversation/callbacks#contact-message) for details. | | channel_identity | object | The identity of the contact in the underlying channel. See [Channel Identity](/docs/conversation/callbacks#channel-identity) for details. | | conversation_id | string | The ID of the conversation this message is part of. Will be empty if `processing_mode` is `DISPATCH`. | | contact_id | string | The ID of the contact. Will be empty if `processing_mode` is `DISPATCH`. | | metadata | string | Usually, metadata specific to the underlying channel is provided in this field. Refer to the individual channels' documentation for more information (for example, [SMS delivery receipts](/docs/conversation/channel-support/sms/message-support#delivery-receipts)). Note that, for Choice message responses or Reply To messages, this field is populated with the value of the `message_metadata` field of the corresponding [Send message request](/docs/conversation/api-reference/conversation/messages/messages_sendmessage#messages/messages_sendmessage/t=request&path=message_metadata). | | accept_time | ISO 8601 timestamp | Timestamp marking when the channel callback was received by the Conversation API. | | sender_id | string | Applicable to channels that support sender IDs (for example, SMS, MMS, WhatsApp, and more). The sender ID to which the contact sent the message, if applicable. For example, originator msisdn/short code for SMS and MMS. | | processing_mode | string | The [Processing Mode](/docs/conversation/processing-modes) of the message. | Metadata assigned to the conversation, if any, is provided in the top level `message_metadata` string field. For example, a string representation of the `metadata_json` field value of a [Create conversation request](/docs/conversation/api-reference/conversation/conversation/conversation_createconversation#conversation/conversation_createconversation/t=request&path=metadata_json), or the value assigned to the `conversation_metadata` field in a [Send message request](/docs/conversation/api-reference/conversation/messages/messages_sendmessage#messages/messages_sendmessage/t=request&path=conversation_metadata). The last value provided in any of those fields is returned, replacing any previous value, if present. ### Example of Inbound Message Callback ```json { "app_id": "01EB37HMH1M6SV18ABNS3G135H", "accepted_time": "2020-11-16T08:17:44.993024Z", "event_time": "2020-11-16T08:17:42.814Z", "project_id": "c36f3d3d-1523-4edd-ae42-11995557ff61", "message": { "id": "01EQ8235TD19N21XQTH12B145D", "direction": "TO_APP", "contact_message": { "text_message": { "text": "Hi!" } }, "channel_identity": { "channel": "MESSENGER", "identity": "2742085512340733", "app_id": "01EB37HMH1M6SV18ABNS3G135H" }, "conversation_id": "01EQ8172WMDB8008EFT4M30481", "contact_id": "01EQ4174TGGY5B1VPTPGHW19R0", "metadata": "", "accept_time": "2020-11-16T08:17:43.915829Z", "sender_id": "12039414555", "processing_mode": "CONVERSATION", "injected": false }, "message_metadata": "{\"arbitrary\": \"json object stringify as metadata\" }", "correlation_id": "correlation-id-1" } ``` #### Redacted Messages Note: This trigger will only function if you have enabled [Smart Conversations](/docs/smart-conversations) and at least one of the related features is enabled. Inbound messages can also be delivered using the `MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION` webhook trigger, where the content of the message goes through an A.I. analysis and is redacted if required. It is possible to use this trigger instead of the `MESSAGE_INBOUND`, which will deliver a payload with a `message_redaction` field instead of a `message`, allowing you to easily differentiate the callbacks: MESSAGE_INBOUND example ```json { "message": { "contact_message": "..." } } ``` MESSAGE_INBOUND_SMART_CONVERSATION_REDACTION example ```json { "message_redaction": { "contact_message": "..." } } ``` For information on the structure of the callback, see [Message Inbound Smart Conversation Redaction callback](/docs/smart-conversations/callbacks/#message-inbound-smart-conversation-redaction-trigger) ### Contact Message The table below shows the properties of the `contact_message` field in inbound message callbacks: | Field | Type | Description | | --- | --- | --- | | reply_to | string | Optional. Included if the contact message is a response to a previous app message. See [Reply To](/docs/conversation/callbacks#reply-to) for details. | It also contains one of the following properties depending on the type of message: `text_message`, `media_message`, `location_message`, `choice_response_message`, `media_card_message`, `fallback_message`, `product_response_message`, `channel_specific_message`. All of these properties are JSON objects described in the sections below. #### Text Message | Field | Type | Description | | --- | --- | --- | | text | string | The text included in the contact message. | #### Media Message For channels with a "Share Contact" feature, you will receive the MO as a `media_message` containing a URL where you can collect a [vCard](https://datatracker.ietf.org/doc/html/rfc6350) formatted file as a standard `media_message`. Standard retention will apply. | Field | Type | Description | | --- | --- | --- | | url | string | The URL of the media. | #### Location Message | Field | Type | Description | | --- | --- | --- | | title | string | The title is shown above the location. The title is sometimes clickable. | | coordinates | object | Geo coordinates of the location specified in the message. See [Coordinates](/docs/conversation/callbacks#coordinates) for details. | | label | string | Label or name for the position. | ##### Coordinates | Field | Type | Description | | --- | --- | --- | | latitude | float | Latitude of the geographic coordinate. | | longitude | float | Longitude of the geographic coordinate. | #### Choice Response Message The choice response message represents a contact response to a choice message. | Field | Type | Description | | --- | --- | --- | | message_id | string | The message id containing the choice. | | postback_data | string | The postback data if defined in the selected choice. Otherwise the default is `message_id_{text, title}` | #### Media Card Message Contact Message containing media and caption. | Field | Type | Description | | --- | --- | --- | | url | string | The URL of the media. | | caption | string | Caption for the media, if supported by channel. | #### Fallback Message Fallback message, appears when original contact message can't be handled. | Field | Type | Description | | --- | --- | --- | | reason | object | Fallback reason. See [Reason](/docs/conversation/callbacks#reason) for details. | | raw_message | string | The raw fallback message if provided by the channel. | #### Reply To The Reply To field contains a reference to the message that the corresponding contact message is responding to. This field is only set if the responding contact message was sent within 3 days of the original message. This feature is only supported on the following channels: WhatsApp, Telegram, Messenger, Instagram, and Viber BM. | Field | Type | Description | | --- | --- | --- | | message_id | object | The ID of the message that this contact message is a response to. | #### Product Response Message | Field | Type | Description | | --- | --- | --- | | products | object array | The selected products. See [Product Item](/docs/conversation/callbacks#product-item) for details. | | text | string | Text that may be sent with the selected products. | | catalog_id | string | The catalog id that the selected products belong to. | ##### Product Item | Field | Type | Description | | --- | --- | --- | | id | string | The ID for the product. | | marketplace | string | The marketplace to which the product belongs. | | quantity | integer | The quantity of the chosen product. | | item_price | float | The price for one unit of the chosen product. | | currency | string | The currency of the `item_price`. | #### Channel Specific Message Contact Message containing a message of a channel-specific message type (not supported by OMNI types) from a specific channel. | Field | Type | Description | | --- | --- | --- | | message_type | string | The type of the message. Possible type: `nfm_reply` | | message | object | The message content. See [Interactive Nfm Reply](#interactive-nfm-reply) for more details. | ##### Interactive Nfm Reply This object represents the message content of a `nfm_reply` message type. | Field | Type | Description | | --- | --- | --- | | type | string | The interactive message type. Always `nfm_reply`. | | nfm_reply | string | The nfm reply message body. See [Nfm Reply](#nfm-reply) for more details. | ##### Nfm Reply | Field | Type | Description | | --- | --- | --- | | name | string | The nfm reply type. Possible types: `flow`, `address_message`. | | response_json | string | The JSON specific data. | | body | string | The message body. | ### Channel Identity The table below shows the properties of the `channel_identity` field in Conversation API callbacks: | Field | Type | Description | | --- | --- | --- | | channel | string | Conversation API identifier of the underlying channel example, `SMS`, `RCS`, `MESSENGER`. | | identity | string | The channel identity example, a phone number for SMS, WhatsApp and Viber Business. | | app_id | string | The app ID if this is an app-scoped channel identity. Empty string otherwise. See [App-scoped Channel Identities](/docs/conversation/callbacks#app-scoped-channel-identities) for details. | #### App-scoped Channel Identities Currently, Facebook Messenger, Instagram, LINE, and WeChat channels are using app-scoped channel identities which means contacts will have different channel identities for different apps. For example, Facebook Messenger uses PSIDs (Page-Scoped IDs) as channel identities. The app_id is pointing to the app linked to the Facebook page for which this PSID is issued. ## Inbound Event This callback delivers channel events such as *composing* to the API clients. The message details are given in a top level `event` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | `id` | string | The event ID. | | `direction` | string | The direction of the event. It's always `TO_APP` for contact events. | | `contact_event` | object | The content of the event. See [Contact Event](/docs/conversation/callbacks#contact-event) for details. Mutually exclusive with `contact_message_event` | | `contact_message_event` | object | The content of the event when `contact_event` is not populated. Note that this object is currently only available to select customers for beta testing. Mutually exclusive with `contact_event`. See [Contact Message Event](/docs/conversation/callbacks#contact-message-event) for details. | | `channel_identity` | object | The identity of the contact in the underlying channel. See [Channel Identity](/docs/conversation/callbacks#channel-identity) for details. | | `contact_id` | string | The ID of the contact. Will be empty if `processing_mode` is `DISPATCH`. | | `conversation_id` | string | The ID of the conversation this event is part of. Will be empty if `processing_mode` is `DISPATCH`. | | `accept_time` | ISO 8601 timestamp | Timestamp marking when the channel callback was received by the Conversation API. | | `processing_mode` | string | The [Processing Mode](/docs/conversation/processing-modes) of the event. | ### Example of Inbound Event Callback Below is an example of an inbound event callback. This is a standard example in which the `contact_event` object is populated: ```json { "app_id": "01EB37HMH1M6SV18ABNS3G135H", "accepted_time": "2020-11-16T08:17:44.993024Z", "event_time": "2020-11-16T08:17:42.814Z", "project_id": "c36f3d3d-1523-4edd-ae42-11995557ff61", "event": { "id": "01GJMQ28NDF6FP0REWQ70N2W3E", "direction": "TO_APP", "contact_event": { "composing_event": {} }, "channel_identity": { "channel": "RCS", "identity": "123456789", "app_id": "" }, "contact_id": "01EQ4174TGGY5B1VPTPGHW19R0", "conversation_id": "01GJMQ3782FWM7TKAZKQZAEF56", "accept_time": "2020-11-16T08:17:43.915829Z", "processing_mode": "CONVERSATION" } } ``` Below is an alternative example of the inbound event callback with the `contact_message_event` object populated: ```json { "app_id": "01EB37HMH1M6SV18ABNS3G135H", "accepted_time": "2020-11-16T08:17:44.993024Z", "event_time": "2020-11-16T08:17:42.814Z", "project_id": "c36f3d3d-1523-4edd-ae42-11995557ff61", "event": { "id": "01GJMQ28NDF6FP0REWQ70N2W3E", "direction": "TO_APP", "contact_message_event": { "payment_status_update_event": { "reference_id": "testing-payment-sep1", "payment_status": "PAYMENT_STATUS_UNKNOWN", "payment_transaction_status": "PAYMENT_STATUS_TRANSACTION_SUCCESS", "payment_transaction_id": "payment-transaction-id1" } }, "channel_identity": { "channel": "RCS", "identity": "123456789", "app_id": "" }, "contact_id": "01EQ4174TGGY5B1VPTPGHW19R0", "conversation_id": "01GJMQ3782FWM7TKAZKQZAEF56", "accept_time": "2020-11-16T08:17:43.915829Z", "processing_mode": "CONVERSATION" } } ``` ### Contact Event The table below shows the properties of the `contact_event` field in inbound event callbacks: | Field | Type | Description | | --- | --- | --- | | `composing_event` | object | Empty object denoting the contact is composing a message. | | `comment_event` | object | Object which contains information of a comment made by an user outside of the main conversation context. Currently only supported on Instagram channel, see [Instagram Private Replies](/docs/conversation/channel-support/instagram/message-support/#sending-private-replies-for-instagram-comment/s) for more details | ### Contact Message Event Note: This functionality is currently only available to select customers for beta testing. The table below shows the properties of the `contact_message_event` field contained within inbound event callbacks: | Field | Type | Description | | --- | --- | --- | | `payment_status_update_event` | object | Object reflecting the current state of a particular payment flow. See [Payment Status Update Event](#payment-status-update-event) for more details. | | `shortlink_activated_event` | object | Object reflecting an event that is created when a contact visits a shortlink. Currently, this is only supported for the Messenger and Instagram channels. See [Shortlink Activated Event](#shortlink-activated-event) for more details. | | `reaction_event` | object | Object reflecting an event that is created when a contact reacts/unreacts with an emoji to a particular MT message. Currently, this is only supported for the Messenger and Instagram channels. See [Reaction Event](#reaction-event) for more details. | ### Payment Status Update Event Note: This functionality is currently only available to select customers for beta testing. The table below shows the properties of the `payment_status_update_event` field that is contained within the `contact_message_event` object: | Field | Type | Description | | --- | --- | --- | | `reference_id` | string | Unique identifier for the corresponding payment of a particular order. | | `payment_status` | string | The stage the payment has reached within the payment flow. | | `payment_transaction_status` | string | The status of the stage detailed in `payment_status`. | | `payment_transaction_id` | string | Unique identifier of the `payment_transaction_status`. | The `payment_status` field can have the following values: - `PAYMENT_STATUS_UNKNOWN` - The status value was not set. Treat it as null or not present field - `PAYMENT_STATUS_NEW` - The user has sent an MT payment message, but the recipient didn’t start a payment yet - `PAYMENT_STATUS_PENDING` - The recipient has started the payment process - `PAYMENT_STATUS_CAPTURED` - The payment was captured - `PAYMENT_STATUS_CANCELED` - The payment was canceled by the recipient and no retry is possible - `PAYMENT_STATUS_FAILED` - The payment attempt failed, but the recipient can retry The `payment_transaction_status` field can have the following values: - `PAYMENT_STATUS_TRANSACTION_UNKNOWN` - The transaction status value was not set. Treat it as null or not present field - `PAYMENT_STATUS_TRANSACTION_PENDING` - The transaction started - `PAYMENT_STATUS_TRANSACTION_FAILED` - The transaction failed - `PAYMENT_STATUS_TRANSACTION_SUCCESS` - The transaction completed successfully. - `PAYMENT_STATUS_TRANSACTION_CANCELED` - The transaction has been cancelled (by either the recipient or the user/business). ### Shortlink Activated Event Note: This functionality is currently only available for the Messenger and Instagram channels. The table below shows the properties of the `shortlink_activated_event` field that is contained within the `contact_message_event` object: | Field | Type | Description | | --- | --- | --- | | `payload` | string | Refers to the payload previously configured to be sent in the postback. | | `title` | string | Only relevant for the Instagram channel. | | `ref` | string | The `ref` parameter from the shortlink the user visited. | | `source` | string | Defaults to "SHORTLINK" for this type of event. | | `source` | string | The source of this referral. For Instagram and Messenger shortlinks, the value of the source is "SHORTLINK". | | `type` | string | The identifier for the referral. For Instagram and Messenger shortlinks, this is always set to "OPEN_THREAD". | | `existing_thread` | string | Set to `true` if target channel's conversation thread already existed at the moment the shortlink was visited. Set to `false` if a new conversation thread began when the shortlink was visited. | ### Reaction event Note: This functionality is currently only available for the Messenger and Instagram channels. The table below shows the properties of the `reaction_event` field that is contained within the `contact_message_event` object: | Field | Type | Description | | --- | --- | --- | | `emoji` | string | Indicates that an emoji reaction was placed on a message. This value is the string representation of the emoji. For example: "\u{2764}\u{FE0F}" | | `action` | string | Type of action. | | `message_id` | string | The ID of the MT message that this reaction is associated with. | | `reaction_category` | string | If present, represents the grouping of emojis. Example values: "smile, "angry, "sad, "wow, "love, "like, "dislike, "other" | The `action` field can have the following values: - `REACTION_ACTION_UNKNOWN` - Unrecognized type of action - `REACTION_ACTION_REACT` - User placed some emoji reaction - `REACTION_ACTION_UNREACT` - User removed previously placed emoji reaction ## Message Delivery Receipt This callback notifies the API clients about status changes of already sent app message. The delivery receipt details are given in a top level `message_delivery_report` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | message_id | string | The ID of the app message. | | conversation_id | string | The ID of the conversation the app message is part of. Will be empty if `processing_mode` is `DISPATCH`. | | status | string | The delivery status. See [Delivery Status](/docs/conversation/callbacks#delivery-status) for details. | | channel_identity | object | The identity of the contact in the underlying channel. See [Channel Identity](/docs/conversation/callbacks#channel-identity) for details. | | contact_id | string | The ID of the contact. Will be empty if `processing_mode` is `DISPATCH`. | | reason | object | Error reason if status is `FAILED` or `SWITCHING_CHANNEL`. See [Reason](/docs/conversation/callbacks#reason) for details. | | metadata | string | Metadata specified in the `message_metadata` field of a [Send Message request](/docs/conversation/api-reference/conversation/messages/messages_sendmessage#messages/messages_sendmessage/t=request&path=message_metadata), if any. | | processing_mode | string | The [Processing Mode](/docs/conversation/processing-modes) of the message. | Additional metadata, if any, is provided in the top level `message_metadata` string field. This is specific to the underlying channel used. Refer to the individual channels' documentation for more information. For example, [SMS delivery receipts](/docs/conversation/channel-support/sms/message-support#delivery-receipts). Note: Delivery receipts depend on the existence of message mapping, which is metadata that enables matching messages by `channelMessageId`. Dispatching is limited to the lesser value between 30 days or the [retention policy](/docs/conversation/keyconcepts#retention-policy) specified by App. Delivery receipts will not be generated after this time. The 30 day restriction is due to `Entity Retention Job`, which permanently removes message mappings from the datastore. Note that a retention cleanup job runs once every twenty-four hours, which can lead to delays between the minute in which message mappings become eligible for deletion and the moment in which the message mappings are actually deleted. ### Example of Message Delivery Receipt The example below shows a receipt for successfully enqueued message with ID `01EQBC1A3BEK731GY4YXEN0C2R` on `MESSENGER` channel: ```json { "app_id": "01EB37HMH1M6SV18BSNS3G135H", "accepted_time": "2020-11-17T15:09:11.659Z", "event_time": "2020-11-17T15:09:13.267185Z", "project_id": "c36f3d3d-1513-2edd-ae42-11995557ff61", "message_delivery_report": { "message_id": "01EQBC1A3BEK731GY4YXEN0C2R", "conversation_id": "01EPYATA64TMNZ1FV02JKF12JF", "status": "QUEUED_ON_CHANNEL", "channel_identity": { "channel": "MESSENGER", "identity": "2734085512340733", "app_id": "01EB27HMH1M6SV18ASNS3G135H" }, "contact_id": "01EXA07N79THJ20WSN6AS30TMW", "metadata": "", "processing_mode": "CONVERSATION" }, "message_metadata": "" } ``` When the sending of the message failed the receipt includes a reason object describing the error: ```json { "app_id": "01EB37HMH1M6SV18BSNS3G135H", "accepted_time": "2020-11-17T16:01:06.374Z", "event_time": "2020-11-17T16:01:07Z", "project_id": "c36f3d3d-1513-4edd-ae42-11995557ff61", "message_delivery_report": { "message_id": "01EQBF0BT63J7S1FEKJZ0Z08VD", "conversation_id": "01EQBCFQR3EGE60P42H6H1117J", "status": "FAILED", "channel_identity": { "channel": "WHATSAPP", "identity": "12345678910", "app_id": "" }, "contact_id": "01EXA07N79THJ20WSN6AS30TMW", "reason": { "code": "OUTSIDE_ALLOWED_SENDING_WINDOW", "description": "The underlying channel reported: Message failed to send because more than 24 hours have passed since the customer last replied to this number", "sub_code": "UNSPECIFIED_SUB_CODE" }, "metadata": "" }, "message_metadata": "" } ``` ### Delivery Status The field `status` is included in delivery receipt callbacks and shows the status of the message or event delivery. The `status` field can have the following values: - `QUEUED_ON_CHANNEL` - `DELIVERED` - `READ` - `FAILED` - `SWITCHING_CHANNEL` Each message and event sent by the API clients to contacts go through the following states: 1. A message has the `QUEUED_ON_CHANNEL` status once it is successfully dispatched to an underlying channel. 2. A delivery receipt is sent from the underlying channel detailing the delivery status on the channel. Depending on the channel response and the processing state of the message, the message is transitioned to one of following states: - `DELIVERED` - the channel delivery receipt indicates the message has reached the end user. Some channels can subsequently send new delivery receipts with a `READ` status. - `READ` - the channel delivery receipt indicates the message was seen or read by the end user. This is a terminal state. There are no more state changes after the message reaches the `READ` state. Some channels will omit sending a delivery receipt with a `DELIVERED` state when the message is seen immediately by the user. In such cases, the `DELIVERED` status is implicit. - `FAILED` - the channel delivery receipt indicates the message delivery failed and there are no more channels to try according to the channel priority defined in the send request. This is a terminal state. There are no more state changes after the message reaches the `FAILED` state. - `SWITCHING_CHANNEL` - the channel delivery receipt indicates that message delivery failed. However, there are more channels through which the Conversation API can try to send the message according to the channel priority defined in the send request. ### Reason The `reason` field in `FAILED` or `SWITCHING_CHANNEL` delivery receipt callbacks provides information for the reason of the failure. The table below shows the properties of the `reason` field: | Field | Type | Description | | --- | --- | --- | | code | string | High-level classification of the error. See [Error Codes](/docs/conversation/callbacks#error-codes) for details. | | description | string | A description of the reason. | | channel_code | string | Error code forwarded directly from the channel. Useful in case of unmapped or channel specific errors. Currently only supported on the WhatsApp channel. | | sub_code | string | The sub code is a more detailed classification of the main error. See [Error Sub-Codes](/docs/conversation/callbacks#error-sub-codes) for details. | ### Error Codes Conversation API provides a set of common reason codes which can be used to automate the error handling by the API clients. The codes are as follow: - `RATE_LIMITED` - the message or event wasn't sent due to rate limiting. - `RECIPIENT_INVALID_CHANNEL_IDENTITY` - the channel recipient identity was malformed. - `RECIPIENT_NOT_REACHABLE` - it wasn't possible to reach the contact, or channel recipient identity, on the channel. When using the [WhatsApp channel](/docs/conversation/channel-support/whatsapp/), you may receive this error code if you send a [marketing template message](https://community.sinch.com/t5/WhatsApp/What-are-WhatsApp-marketing-templates-used-for/ta-p/11801) to a recipient, and the recipient is part of the current WhatsApp [marketing message experiment](https://developers.facebook.com/docs/whatsapp/on-premises/guides/experiments?content_id=86oue5PtwEgcBJl). - `RECIPIENT_NOT_OPTED_IN` - the contact, or channel recipient identity, hasn't opted in on the channel. - `OUTSIDE_ALLOWED_SENDING_WINDOW` - the allowed sending window has expired. See the [channel support documentation](/docs/conversation/channel-support) for more information about how the sending window works for the different channels. - `CHANNEL_FAILURE` - the channel failed to accept the message. The Conversation API performs multiple retries in case of transient errors. - `CHANNEL_BAD_CONFIGURATION` - the channel configuration of the app is wrong. The bad configuration caused the channel to reject the message. - `CHANNEL_CONFIGURATION_MISSING` - the referenced app has no configuration for the channel. This may be considered an [internal error](#internal-errors). - `MEDIA_TYPE_UNSUPPORTED` - indicates that the sent message had an unsupported media type. - `MEDIA_TOO_LARGE` - some of the referenced media files are too large. See the [channel support documentation](/docs/conversation/channel-support) to find out the limitations on file size that the different channels impose. - `MEDIA_NOT_REACHABLE` - the provided media link wasn't accessible from the Conversation API or from the underlying channels. Please make sure that the media file is accessible. - `NO_CHANNELS_LEFT` - no channels to try to send the message to. This error will occur if all applicable channels have been attempted. This may be considered an [internal error](#internal-errors). - `TEMPLATE_NOT_FOUND` - the referenced template wasn't found. This may be considered an [internal error](#internal-errors). - `TEMPLATE_INSUFFICIENT_PARAMETERS` - not all parameters defined in the template were provided when sending a template message. - `TEMPLATE_NON_EXISTING_LANGUAGE_OR_VERSION` - the selected language, or version, of the referenced template didn't exist. Please check the available versions and languages of the template. - `DELIVERY_TIMED_OUT` - the message or event delivery failed due to a channel-imposed timeout. - `DELIVERY_REJECTED_DUE_TO_POLICY` - the message or event was rejected by the channel due to a policy. Some channels have specific policies that must be met to send a message. See the [channel support documentation](/docs/conversation/channel-support) for more information about when this error will be triggered. - `CONTACT_NOT_FOUND` - the provided Contact ID didn't exist. This can be considered an [internal error](#internal-errors). - `BAD_REQUEST` - Conversation API validates send requests in two different stages. The first stage is right before the message is enqueued. If this first validation fails the API responds with 400 Bad Request and the request is discarded immediately. The second validation kicks in during message processing and it normally contains channel specific validation rules. Failures during second request validation are delivered as callbacks to `MESSAGE_DELIVERY (EVENT_DELIVERY)` webhooks with ReasonCode `BAD_REQUEST`. - `UNKNOWN_APP` - missing app. This error may occur when the app is removed during message processing. This can be considered an [internal error](#internal-errors). - `NO_CHANNEL_IDENTITY_FOR_CONTACT` - the contact has no channel identities for the resolved channel priorities. This can be considered an [internal error](#internal-errors). - `NO_PERMISSION` - Check the configuration of the underlying channel (Facebook Messenger or Instagram) to ensure that it has the correct permissions set to be able to access the profile or the page. - `NO_PROFILE_AVAILABLE` - Facebook Messenger profile information cannot be retrieved for accounts created using a phone number. - `UNSUPPORTED_OPERATION` - Generic failure and/or unsupported operation. This can be considered an [internal error](#internal-errors). Contact Support. - `INACTIVE_CREDENTIAL` - The channel specified has not yet completed provisioning or provisioning has failed. Note that a channel is not usable on a Conversation API app until it enters an "active" state. This can be considered an [internal error](#internal-errors). - `MESSAGE_EXPIRED` - The TTL has expired for this message. This can be considered an [internal error](#internal-errors). - `MESSAGE_SPLIT_REQUIRED` - The message exceeds the length limit, or other dimensional constraint(s), of the channel. The message must be split into multiple, smaller segments. This can be considered an [internal error](#internal-errors). - `DELIVERY_REPORT_TIME_OUT` - This error occurs when a positive delivery report for a message is not received within the specified time. Review the [Delivery Report Based Fallback](/docs/conversation/keyconcepts/#delivery-report-base-message-fallback). - `CHANNEL_REJECT` - generic error for channel permanently rejecting a message. - `UNKNOWN` - returned if no other code can be used to describe the encountered error. - `INTERNAL_ERROR` - an internal error occurred. Please save the entire callback if you want to report an error. #### Internal errors Some of the above errors can be categorized as "internal errors". These include: - `CHANNEL_CONFIGURATION_MISSING` - `NO_CHANNELS_LEFT` - `TEMPLATE_NOT_FOUND` - `CONTACT_NOT_FOUND` - `UNKNOWN_APP` - `NO_CHANNEL_IDENTITY_FOR_CONTACT` - `UNSUPPORTED_OPERATION` - `INACTIVE_CREDENTIAL` - `MESSAGE_EXPIRED` - `MESSAGE_SPLIT_REQUIRED` These errors occur before the message is queued on a particular channel. If you encounter these errors, note that the `QUEUED_ON_CHANNEL` status will not be generated for the corresponding message. Other, non-internal errors occur before and after the message reaches the `QUEUED_ON_CHANNEL` status. ### Error Sub-Codes The `sub_code` field is a more detailed classification of the main error. These sub-codes can differ between channels, and may be supplied by the channel itself. Note: If you would like more information on a channel-specific error sub-code, you can search for the error sub-code in the documentation maintained by the corresponding channel. Below are sub-codes that can correspond to errors associated with messages sent on any channel: - `UNSPECIFIED_SUB_CODE` - used if no other sub code can be used to describe the encountered error. - `ATTACHMENT_REJECTED` - occurs when the message attachment has been rejected by the channel due to a policy. Some channels have specific policies that must be met to receive an attachment. ## Message Submit Notification This callback provides a notification to the API clients that the corresponding app message was submitted to a channel. This notification is created before any confirmation from Delivery Receipts. The message submission details are given in a top level `message_submit_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | message_id | string | The ID of the app message. | | conversation_id | string | The ID of the conversation the app message is part of. Will be empty if `processing_mode` is `DISPATCH`. | | channel_identity | object | The identity of the contact in the underlying channel. See [Channel Identity](/docs/conversation/callbacks#channel-identity) for details. | | contact_id | string | The ID of the contact. Will be empty if `processing_mode` is `DISPATCH`. | | submitted_message | object | The app message submitted to the channel. | | metadata | string | Metadata specified in the `message_metadata` field of a [Send Message request](/docs/conversation/api-reference/conversation/messages/messages_sendmessage#messages/messages_sendmessage/t=request&path=message_metadata), if any. | | processing_mode | string | The [Processing Mode](/docs/conversation/processing-modes) of the message. | Additional metadata, if any, is provided in the top level `message_metadata` string field. This is specific to the underlying channel used. Refer to the individual channels' documentation for more information. For example, [SMS delivery receipts](/docs/conversation/channel-support/sms/message-support#delivery-receipts). ### Example of Message Submit Notification The example below shows a notification for a submitted message with ID `01EQBC1A3BEK731GY4YXEN0C2R` on the `MESSENGER` channel: ```json { "app_id":"01EB37HMH1M6SV18BSNS3G135H", "accepted_time": "2020-11-17T15:09:11.659Z", "event_time": "2020-11-17T15:09:13.267185Z", "project_id":"c36f3d3d-1513-2edd-ae42-11995557ff61", "message_submit_notification":{ "message_id":"01EQBC1A3BEK731GY4YXEN0C2R", "conversation_id":"01EPYATA64TMNZ1FV02JKF12JF", "channel_identity":{ "channel":"MESSENGER", "identity":"2734085512340733", "app_id":"01EB27HMH1M6SV18ASNS3G135H" }, "contact_id":"01EXA07N79THJ20WSN6AS30TMW", "submitted_message":{ "text_message":{ "text":"Hello from Conversation API!" } }, "metadata":"", "processing_mode":"CONVERSATION" }, "message_metadata":"" } ``` ## Event Delivery Receipt This callback notifies the API clients about status changes of already sent app events. The delivery receipt details are given in a top level `event_delivery_report` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | event_id | string | The ID of the app event. | | status | string | The delivery status. See [Delivery Status](/docs/conversation/callbacks#delivery-status) for details. | | channel_identity | object | The identity of the contact in the underlying channel. See [Channel Identity](/docs/conversation/callbacks#channel-identity) for details. | | contact_id | string | The ID of the contact. Will be empty if `processing_mode` is `DISPATCH`. | | reason | object | Error reason if status is `FAILED` or `SWITCHING_CHANNEL`. See [Reason](/docs/conversation/callbacks#reason) for details. | | metadata | string | Metadata specified when sending the event if any. | | processing_mode | string | The [Processing Mode](/docs/conversation/processing-modes) of the event. | ### Example of Event Delivery Callback ```json { "app_id": "01EB37HMH1M6SV18BSNS3G135H", "accepted_time": "2020-11-17T15:09:11.659Z", "event_time": "2020-11-17T15:09:13.267185Z", "project_id": "c36f3d3d-1513-2edd-ae42-11995557ff61", "event_delivery_report": { "event_id": "01EQBC1A3BEK731GY4YXEN0C2R", "status": "QUEUED_ON_CHANNEL", "channel_identity": { "channel": "MESSENGER", "identity": "2734085512340733", "app_id": "01EB27HMH1M6SV18ASNS3G135H" }, "contact_id": "01EXA07N79THJ20WSN6AS30TMW", "metadata": "", "processing_mode": "CONVERSATION" }, "message_metadata": "" } ``` ## Conversation Start This callback is sent when a new conversation between the subscribed app and a contact is started. The conversation details are given in a top level `conversation_start_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | conversation | object | The properties of the started conversation | ### Example of Conversation Start Callback ```json { "app_id": "01EB37HMH1M6SV18ASNS3G135H", "project_id": "c36f3d3d-1523-4edd-ae42-11995557ff61", "conversation_start_notification": { "conversation": { "id": "01EQ4174WMDB8008EFT4M30481", "app_id": "01EB37HMH1M6SF18ASNS3G135H", "contact_id": "01BQ8174TGGY5B1VPTPGHW19R0", "active_channel": "MESSENGER", "active": true, "metadata": "" } } } ``` ## Conversation Stop This callback is sent when a conversation between the subscribed app and a contact is stopped. The conversation details are given in a top level `conversation_stop_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | conversation | object | The properties of the stopped conversation. | ### Example of Conversation Stop Callback ```json { "app_id": "01EB37HMH1M6SV17ASNS3G135H", "project_id": "c36f3d3d-1523-4edd-ae42-11995557ff61", "conversation_stop_notification": { "conversation": { "id": "01EPYATZ64TMNZ1FV02JKD12JF", "app_id": "01EB37HMH1M6SV17ASNS3G135H", "contact_id": "01EKA07N79THJ20WAN6AS30TMW", "last_received": "2020-11-17T15:09:12Z", "active_channel": "MESSENGER", "active": false, "metadata": "" } } } ``` ## Contact Create This callback is sent when a new contact is created. The contact details are given in a top level `contact_create_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | contact | object | The properties of the created contact. | ### Example of Contact Create Callback ```json { "app_id": "", "accepted_time": "2020-11-17T15:36:28.155494Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "contact_create_notification": { "contact": { "id": "01EQBDK8771J6A1FV8MQPE1XAR", "channel_identities": [ { "channel": "VIBER", "identity": "9KC0p+pi4zPGFO99ACDxdQ==", "app_id": "01EB37KMH1M6SV18ASNS3G135H" } ], "channel_priority": ["VIBER"], "display_name": "Unknown", "email": "", "external_id": "", "metadata": "", "language": "UNSPECIFIED" } } } ``` ## Contact Delete This callback is sent when a contact is deleted. The contact details are given in a top level `contact_delete_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | contact | object | The properties of the deleted contact. | ### Example of Contact Delete Callback ```json { "app_id": "", "accepted_time": "2020-11-17T15:44:33.517073Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "contact_delete_notification": { "contact": { "id": "01EQBDK8771J6A1FV8MQPE1XAR", "channel_identities": [ { "channel": "VIBER", "identity": "9KC0p+pi4zPGFO99ACDxdQ==", "app_id": "01EB37HMH1M6SV13ASNS3G135H" } ], "channel_priority": ["VIBER"], "display_name": "Unknown", "email": "", "external_id": "", "metadata": "", "language": "UNSPECIFIED" } } } ``` ## Contact Update This callback is sent when a contact is updated. The full updated contact details are given in a top level `contact_update_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | contact | object | The properties of the updated contact. | ### Example of Contact Update Callback ```json { "app_id": "", "accepted_time": "2020-11-17T15:44:33.517073Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "contact_update_notification": { "contact": { "id": "01EQBDK8771J6A1FV8MQPE1XAR", "channel_identities": [ { "channel": "VIBER", "identity": "9KC0p+pi4zPGFO99ACDxdQ==", "app_id": "01EB37HMH1M6SV13ASNS3G135H" } ], "channel_priority": ["VIBER"], "display_name": "Unknown", "email": "", "external_id": "", "metadata": "", "language": "UNSPECIFIED" } } } ``` ## Contact Merge This callback is sent when two contacts are merged. The details of the resulting merged and deleted contacts are given in a top level `contact_merge_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | preserved_contact | object | The properties of the resulting merged contact. | | deleted_contact | object | The properties of the deleted contact. | ### Example of Contact Merge Callback ```json { "app_id": "", "accepted_time": "2020-11-17T15:53:03.457706Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "contact_merge_notification": { "preserved_contact": { "id": "01EQBECE7Z4XP21359SBKS1526", "channel_identities": [ { "channel": "VIBER", "identity": "9KC0p+pi4zPGFO99ACDxdQ==", "app_id": "01EB37KMH1M6SV18ASNS3G135H" } ], "channel_priority": ["VIBER"], "display_name": "Unknown", "email": "", "external_id": "", "metadata": "", "language": "UNSPECIFIED" }, "deleted_contact": { "id": "01EQBEH7MNEZQC0881A4WS17K3", "channel_identities": [ { "channel": "VIBER", "identity": "9KC0p+pi4zaGFO99ACDxdQ==", "app_id": "01EB37KMH1M6SV18ASNS3G135H" } ], "channel_priority": ["VIBER"], "display_name": "Unknown", "email": "", "external_id": "", "metadata": "", "language": "UNSPECIFIED" } } } ``` ## Contact Identities Duplication Notification This callback is sent when duplicates of channel identities are found between multiple contacts in the contact database during message and event processing. Note: Project scoped channel identities are considered duplicates when they have the same channel and identity pair within the same project. App scoped channel identities are considered duplicates when they have the same app ID, channel, and identity combination within the same project. When a duplicate is identified, you must use the [contact](/docs/conversation/api-reference/conversation/contact) endpoint to resolve the issue. This notification points to the contacts which should be [deleted](/docs/conversation/api-reference/conversation/contact/contact_deletecontact), [updated](/docs/conversation/api-reference/conversation/contact/contact_updatecontact), or [merged](/docs/conversation/api-reference/conversation/contact/contact_mergecontact) to resolve the duplication issue. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | duplicated_identities | list | List which contains information regarding the duplicated identities. Each list entry contains information about the channel on which the duplicate identities were found and the set of contact IDs containing those duplicated channel identities. | For more information on contact duplication, see our documentation on Conversation API [contact management](/docs/conversation/contact-management/). ### Example of Contact Identities Duplication Notification ```json { "app_id": "01EB37KMH2M6SV18ASNS3G135H", "accepted_time": "2022-09-29T09:16:22.544813845Z", "event_time": "2022-09-29T09:16:22.544813845Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "duplicated_contact_identities_notification": { "duplicated_identities": [ { "channel": "channel", "contact_ids": [ "01EKA07N79THJ20ZSN6AS30TMW", "01EKA07N79THJ20ZSN6AS30TTT" ] } ] }, "message_metadata": "" } ``` ## Batch Status Update This callback is sent when a batch processing operation is completed, failed, or canceled. It provides information about the status of the batch and any errors that may have occurred during processing. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | `batch_id` | string | Required. The ID of the batch. | | `batch_status` | string | Required. Status of the batch. One of `BATCH_STATUS_READY`, `BATCH_STATUS_SCHEDULED`, `BATCH_STATUS_PROCESSED`, `BATCH_STATUS_CANCELLED`, or `BATCH_STATUS_FAILED`. | | `error_code` | string | Optional. Error code if the batch processing failed. | | `batch_type` | string | Required. Type of the batch. One of `BATCH_TYPE_MESSAGES`, `BATCH_TYPE_CONTACTS`, or `BATCH_TYPE_CONSENTS`. | | `additional_data` | oneof | Optional. Contains batch-type-specific data. This is a oneof field that can contain one of the following types, allowing for schema evolution as new batch types are added in the future. | The `additional_data` field can contain one of the following types: `message_batch_data` (for `BATCH_TYPE_MESSAGES`) | Field | Type | Description | | --- | --- | --- | | `send_after` | timestamp | Optional. Client-supplied time indicating when the batch should be sent. | | `batch_metadata` | map | Optional. Metadata supplied by the user when creating the batch. | The possible values for `batch_status` are explained below: - `BATCH_STATUS_READY`: The batch is ready for processing. - `BATCH_STATUS_SCHEDULED`: The batch is scheduled for processing at a later time. - `BATCH_STATUS_PROCESSED`: The batch has been successfully processed. - `BATCH_STATUS_CANCELLED`: The batch has been cancelled. - `BATCH_STATUS_FAILED`: The batch processing failed. The possible values for `error_code` are explained below: - `BATCH_ERROR_UNSPECIFIED`: Unspecified error. - `BATCH_ERROR_INVALID_BATCH_ID`: The batch ID is invalid. - `BATCH_ERROR_INVALID_METADATA_FILTER`: The metadata filter is invalid. - `BATCH_ERROR_ALREADY_PROCESSED`: The batch has already been processed. - `BATCH_ERROR_ALREADY_CANCELLED`: The batch has already been cancelled. - `BATCH_ERROR_ACCOUNT_CREDIT_LIMIT_BREACHED`: The account credit limit has been breached. - `BATCH_ERROR_INTERNAL`: Internal error. The possible values for `batch_type` are explained below: - `BATCH_TYPE_MESSAGES`: The batch contains messages. - `BATCH_TYPE_CONTACTS`: The batch contains contacts. - `BATCH_TYPE_CONSENTS`: The batch contains consents. ### Example of Batch Status Update Notification ```json { "app_id": "01EB37KMH2M6SV18ASNS3G135H", "accepted_time": "2023-05-15T14:22:33.123456789Z", "event_time": "2023-05-15T14:22:33.123456789Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "batch_status_update_notification": { "batch_id": "01FG37KMH2M6SV18ASNS3G135H", "batch_status": "BATCH_STATUS_PROCESSED", "batch_type": "BATCH_TYPE_MESSAGES", "additional_data": { "message_batch_data": { "send_after": "2023-05-15T12:00:00.000000000Z", "batch_metadata": { "campaign": "summer_sale", "segment": "active_users" } } } }, "message_metadata": "" } ``` ## Capability Check This callback is used to deliver the results of the asynchronous capability checks. The outcome of the capability check is given in a top level `capability_notification` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | `request_id` | string | ID generated when submitting the capability request. Can be used to detect duplicates. | | `contact_id` | string | The ID of the contact. | | `channel` | string | The channel for which the capability lookup was performed. | | `identity` | string | The channel identity. For example, a phone number for SMS, WhatsApp, and Viber Business. | | `capability_status` | string | Status indicating the recipient's capability on the channel. One of `CAPABILITY_FULL`, `CAPABILITY_PARTIAL`, `NO_CAPABILITY`, or `CAPABILITY_UNKNOWN`. | | `channel_capabilities` | string array | When `capability_status` is set to `CAPABILITY_PARTIAL`, this field includes a list of the supported channel-specific capabilities reported by the channel. | | `reason` | object | If the capability check failed, this field contains the reason for the error. See [Reason](/docs/conversation/callbacks#reason) for details. | The possible values for `capability_status` are explained below: - `CAPABILITY_UNKNOWN` : the channel capability for the contact is unknown because the underlying channel has not made this information available. - `CAPABILITY_FULL` : the specified contact supports all the features of the channel. - `CAPABILITY_PARTIAL` : the specified contact supports a subset of the channel features. - `NO_CAPABILITY` : the specified contact supports none of the channel features. ### Example of Capability Check Callback ```json { "app_id": "01EB37KMH2M6SV18ASNS3G135H", "accepted_time": "2020-11-17T16:05:51.724083Z", "project_id": "", "capability_notification": { "contact_id": "01EKA07N79THJ20ZSN6AS30TMW", "identity": "12345678910", "channel": "WHATSAPP", "capability_status": "CAPABILITY_FULL", "request_id": "01EQBF91XWP9PW1J8EWRYZ1GK2" } } ``` ## Opt-in This callback is used to deliver opt-in notifications from the channels. Important: Opt-in and opt-out callbacks are only supported on Conversation API channels that support opt-in and opt-out notification types (for example, the Viber Business Messages channel). These callbacks are not triggered on unsupported channels. For example, a recipient sending a reply on the SMS channel with the text **STOP**, **UNSUBSCRIBE**, etc., will **not** trigger an opt-out callback. The opt-in details are given in a top level `opt_in_notification` field with the following properties: | Field | Type | Description | | --- | --- | --- | | `request_id` | string | ID generated when making an opt-in registration request. Can be used to detect duplicates. | | `contact_id` | string | The ID of the contact which is the subject of the opt-in. Will be empty if `processing_mode` is `DISPATCH`. | | `channel` | string | The channel of the opt-in. | | `identity` | string | The channel identity. For example, a phone number for SMS, WhatsApp and Viber Business. | | `status` | string | Status of the opt-in registration. One of `OPT_IN_SUCCEEDED`, `OPT_IN_FAILED`, or `OPT_IN_STATUS_UNSPECIFIED`. | | `error_details` | object | This field is populated if the opt-in failed. It contains a single string property, `description`, containing a human-readable error description. | | `processing_mode` | string | The [Processing Mode](/docs/conversation/processing-modes) of the opt-in. | The possible values for `status` are explained below: - `OPT_IN_STATUS_UNSPECIFIED` : the underlying channel doesn't support Opt-in. - `OPT_IN_SUCCEEDED` : the Opt-in registration succeeded. - `OPT_IN_FAILED` : the Opt-in registration failed, see reason in `error_details` field. ### Example of Opt-in Callback ```json { "app_id": "01EB37HMH1M6SV18ASNS3G135H", "accepted_time": "2021-06-08T07:54:03.165316Z", "event_time": "2021-06-08T07:54:02.112Z", "project_id": "", "opt_in_notification": { "contact_id": "01EKA07N79THJ20WSN6AS30TMW", "channel": "VIBERBM", "identity": "123456789", "status": "OPT_IN_SUCCEEDED", "request_id": "01F7N9TEH11X7B15XQ6VBR04G7", "processing_mode": "CONVERSATION" } } ``` ## Opt-out This callback is used to deliver opt-out notifications from the channels. Important: Opt-in and opt-out callbacks are only supported on Conversation API channels that support opt-in and opt-out notification types (for example, the Viber Business Messages channel). These callbacks are not triggered on unsupported channels. For example, a recipient sending a reply on the SMS channel with the text **STOP**, **UNSUBSCRIBE**, etc., will **not** trigger an opt-out callback. The opt-out details are given in a top level `opt_out_notification` field with the following properties: | Field | Type | Description | | --- | --- | --- | | `request_id` | string | ID generated when making an opt-out registration request. Can be used to detect duplicates. | | `contact_id` | string | The ID of the contact which is the subject of the opt-out. Will be empty if `processing_mode` is `DISPATCH`. | | `channel` | string | The channel of the opt-out. | | `identity` | string | The channel identity. For example, a phone number for SMS, WhatsApp and Viber Business. | | `status` | string | Status of the opt-out registration. One of `OPT_OUT_SUCCEEDED`, `OPT_OUT_FAILED`, or `OPT_OUT_STATUS_UNSPECIFIED`. | | `error_details` | object | This field is populated if the opt-out failed. It contains a single string property, `description`, containing a human-readable error description. | | `processing_mode` | string | The [Processing Mode](/docs/conversation/processing-modes) of the opt-out. | The possible values for `status` are explained below: - `OPT_OUT_STATUS_UNSPECIFIED` : the underlying channel doesn't support Opt-out. - `OPT_OUT_SUCCEEDED` : the Opt-out registration succeeded. - `OPT_OUT_FAILED` : the Opt-out registration failed, see reason in `error_details` ### Example of Opt-out Callback ```json { "app_id": "01EB37HMH1M6SV18ASNS3G135H", "accepted_time": "2021-06-08T07:54:03.165316Z", "event_time": "2021-06-08T07:54:02.112Z", "project_id": "", "opt_out_notification": { "contact_id": "01EKA07N79THJ20WSN6AS30TMW", "channel": "VIBERBM", "identity": "123456789", "status": "OPT_OUT_SUCCEEDED", "request_id": "01F7N9TEH11X7B15XQ6VBR04G7", "processing_mode": "CONVERSATION" } } ``` ## Channel Event This callback is used to deliver notifications regarding channel-specific information and updates. For example, if your are using the WhatsApp channel of the Conversation API, and your quality rating has been changed to `GREEN`, a `POST` would be made to the `CHANNEL_EVENT` webhook. The event details are given in a top level `channel_event` field with the following properties (note that the `channel_event` field is under the `channel_event_notification` field): | Field | Type | Description | | --- | --- | --- | | `channel` | string | The channel for which the notification is providing information. | | `event_type` | string | The type of event being reported. | | `additional_data` | object | An object containing additional information regarding the event. The contents of the object depend on the `channel` and the `event_type`. | ### Example of Channel Event Callback ```json { "channel": "WHATSAPP", "event_type": "WHATS_APP_QUALITY_RATING_CHANGED", "additional_data": { "quality_rating": "GREEN" } } ``` ## Unsupported Callback Some of the callbacks received from the underlying channels might be specific to a single channel or may not have a proper mapping in Conversation API yet. In such cases the callbacks are forwarded as is all the way to the API clients subscribed to the `UNSUPPORTED` webhook trigger. The source of the unsupported callback is given in a top level `unsupported_callback` field. It's a JSON object with the following properties: | Field | Type | Description | | --- | --- | --- | | channel | string | The channel which is the source of this callback. | | payload | string | Normally a JSON payload as sent by the channel. | | processing_mode | string | The [Processing Mode](/docs/conversation/processing-modes) of the callback. | | id | string | The message ID. | | contact_id | string | The ID of the contact. This field is blank if not supported. | | conversation_id | string | The ID of the conversation this message is part of. This field is blank if not supported. | | channel_identity | object | The identity of the contact in the underlying channel. This field is omitted if not supported. See [Channel Identity](/docs/conversation/callbacks#channel-identity) for details. | Note The inclusion of the `contact_id`, `conversation_id`, and `channel_identity` fields is dependent on channel integration support. These fields may be omitted if the callback is not associated with a specific channel identity, or if the integration doesn't support them. ### Example of Unsupported Callback Example of an unsupported callback without the `contact_id`, `conversation_id`, or `channel_identity` fields: ```json { "app_id": "01EB37HMF1M6SV18ASNS3G135H", "accepted_time": "2020-11-17T15:17:05.723864Z", "event_time": "2020-11-17T15:17:05.683253Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "unsupported_callback": { "channel": "MESSENGER", "payload": "{\"object\":\"page\",\"entry\":[{\"id\":\"107710160841844\",\"time\":1605626225304,\"messaging\":[{\"sender\":{\"id\":\"107710160841844\"},\"recipient\":{\"id\":\"2744085512340733\"},\"timestamp\":1605626225228,\"message\":{\"mid\":\"m_CE0r2yTXOPe1DG_cepCqA9eapH28NipZLg3HuKNf-MA_Edr55tBwvLiTzhrvjZJWQioE4J4gpETQZBTREc7Hdg\",\"is_echo\":true,\"text\":\"Text message from Sinch Conversation API.\",\"app_id\":477442439539985}}]}]}", "id": "01FMAVK07YN3SP1B43FP9D1C0S", "contact_id": "", "conversation_id": "", "processing_mode": "CONVERSATION" } } ``` Example of an unsupported callback with the `contact_id`, `conversation_id`, and `channel_identity` fields: ```json { "app_id": "01EB37HMF1M6SV18ASNS3G135H", "accepted_time": "2021-11-12T19:53:54.728511Z", "event_time": "2021-11-12T19:53:54.542308Z", "project_id": "c36f3a3d-1513-4edd-ae42-11995557ff61", "unsupported_callback": { "channel": "APPLEBC", "payload": "{\"interactiveData\": ... }", "id": "01FMAVDCKE8TNN021VN7XQ1VG2", "contact_id": "01FMAVAPAQTEGDJSFJJWANRX38", "conversation_id": "01FMAVAQBTR4C1HJZS05PVTXZ8", "channel_identity": { "channel": "APPLEBC", "identity": "urn:mbid:AQAAX12RO5I+XVVAhq2b0S22ohAOSfSB+aZkybPFk6iOaMdgrXXstADziEHz/f/x8cA=", "app_id": "01EB37HMF1M6SV18ASNS3G135H" }, "processing_mode": "CONVERSATION" }, "message_metadata": "" } ```