# Close Session Source: https://docs.connectly.ai/ai/agent-graph-close POST /external/v1/ai/agent_graph/close End an active conversation session and release its resources ✌️ Call this endpoint when a conversation is complete to release the session and free associated resources. After closing, you cannot send further messages using the same `sessionId`. Start a new session with the [init endpoint](/ai/agent-graph/init-session) to begin a fresh conversation. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/ai/agent_graph/close ``` ## Request body Your Connectly business identifier. The unique identifier for the customer whose session you are closing. The session ID returned by the [init](/ai/agent-graph/init-session) endpoint. ## Response An empty object `{}` confirming the session was closed successfully. `null` on success. Populated with `code`, `message`, and `details` if an error occurred. ## Example ```http theme={null} POST /external/v1/ai/agent_graph/close HTTP/1.1 Host: api.connectly.ai x-api-key: YOUR_API_KEY Content-Type: application/json { "businessId": "your-business-id", "clientKey": "customer-123", "sessionId": "b6bed81c-5fda-486d-b96c-1ff3af36219a" } ``` ```json theme={null} { "result": { "response": {} }, "error": null } ``` # Health Check Source: https://docs.connectly.ai/ai/agent-graph-health POST /external/v1/ai/agent_graph/health Verify that the Agent Graph API is reachable and ready to accept sessions 🩺 Use this endpoint to confirm the Agent Graph API is operational before starting a conversation, or to monitor availability in your infrastructure. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/ai/agent_graph/health ``` ## Request body Your Connectly business identifier. A client key associated with your business. Does not need to correspond to an active session. ## Response `true` when the Agent Graph API is operational and ready to accept sessions. If `false` or absent, treat the API as unavailable and retry after a short delay. ## Example ```http theme={null} POST /external/v1/ai/agent_graph/health HTTP/1.1 Host: api.connectly.ai x-api-key: YOUR_API_KEY Content-Type: application/json { "businessId": "your-business-id", "clientKey": "your-client-key" } ``` ```json theme={null} { "response": { "healthy": true } } ``` # Init Session Source: https://docs.connectly.ai/ai/agent-graph-init POST /external/v1/ai/agent_graph/init Start a new conversation session with a Connectly AI agent and receive a sessionId for subsequent calls 🎙️ Call this endpoint before sending any messages to a Connectly AI agent. It creates a new conversation session scoped to a specific customer and returns a `sessionId` you must pass to every subsequent invoke and close request. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/ai/agent_graph/init ``` ## Request body Your Connectly business identifier. A unique identifier for the customer starting the conversation. Use a stable ID from your own system (e.g. a customer ID or phone number) so you can correlate sessions with users. ## Response The unique identifier for this conversation session. Save this value — you must include it in all subsequent invoke and close calls for this conversation. Sessions are isolated; events from one session never affect another. Store the `sessionId` immediately after calling this endpoint. Without it you cannot send messages or close the session. ## Example ```http theme={null} POST /external/v1/ai/agent_graph/init HTTP/1.1 Host: api.connectly.ai x-api-key: YOUR_API_KEY Content-Type: application/json { "businessId": "your-business-id", "clientKey": "customer-123" } ``` ```json theme={null} { "response": { "sessionId": "b6bed81c-5fda-486d-b96c-1ff3af36219a" } } ``` # Invoke (Stream) Source: https://docs.connectly.ai/ai/agent-graph-invoke POST /external/v1/ai/agent_graph/invoke Send customer input events to a Connectly AI agent and receive a streaming NDJSON response ⌨️ Send customer input to a Connectly AI agent and receive the agent's reply as a streaming NDJSON response. Each line of the response body is a complete, self-contained JSON event. You must [initialise a session](/ai/agent-graph/init-session) and obtain a `sessionId` before calling this endpoint. The response body is NDJSON — one JSON object per line. Do not call `response.json()` on the raw response; it will throw a parse error. See [NDJSON streaming](/ai/ndjson-streaming) for the correct approach. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/ai/agent_graph/invoke ``` ## Request body Your Connectly business identifier. The unique identifier for the customer in this conversation. The session ID returned by the [init](/ai/agent-graph/init-session) endpoint. Optional override to target a specific agent within your configuration. The list of events to pass to the agent. Include **all** events since the last call — for example, if an automated welcome message was sent, include it as an assistant `messageEvent` so the agent has full context. Each element is an object containing exactly one event key: A message from the user. Set `role` to `"USER"`. The `content` object supports these content types: | Key | Fields | Description | | ----------------- | ------------------------------------------ | -------------------------------------------------- | | `textContent` | `text` (string) | Plain text message. | | `audioContent` | `url` (string) | URL to an audio file (mp3, wav, ogg). | | `imageContent` | `url` (string), `caption` (string) | URL to an image (jpeg, png) with optional caption. | | `locationContent` | `latitude`, `longitude`, `name`, `address` | Geographic location. | ```json theme={null} { "messageEvent": { "role": "USER", "content": { "textContent": { "text": "Hi, I need help" } } } } ``` The customer tapped a quick-reply button. ```json theme={null} { "buttonResponseEvent": { "id": "button-id", "title": "Button Title" } } ``` The customer selected an item from a list message. ```json theme={null} { "listReplyEvent": { "id": "list-item-id", "title": "Option Title", "description": "Option Description" } } ``` The customer submitted a form. ```json theme={null} { "filledFormResponseEvent": { "formData": { "name": "João Silva", "email": "joao@example.com" } } } ``` Associates the conversation with a specific store location. ```json theme={null} { "storeEvent": { "storeId": "22473", "name": "Store Name", "city": "São Paulo", "state": "SP" } } ``` ## Response A `200` status with a streaming NDJSON body. Read the response line by line; each line is a self-contained JSON event from the agent. ## Example ```python theme={null} import requests, json response = requests.post( "https://api.connectly.ai/external/v1/ai/agent_graph/invoke", headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={ "businessId": "your-business-id", "clientKey": "customer-123", "sessionId": "your-session-id", "inputEvents": [{ "messageEvent": { "role": "USER", "content": {"textContent": {"text": "Hi, I need help"}} } }] }, stream=True ) for raw_line in response.iter_lines(): if raw_line: event = json.loads(raw_line.decode("utf-8")) print(event) ``` See [NDJSON streaming](/ai/ndjson-streaming) for a complete parsing guide. # Invoke (Sync) Source: https://docs.connectly.ai/ai/agent-graph-invoke-sync POST /external/v1/ai/agent_graph/invoke_sync Send customer input events to an AI agent and receive a single aggregated JSON response without streaming 🫴 Use this endpoint when you want the agent's complete reply in a single JSON response rather than a streaming NDJSON body. Useful for server-to-server integrations where streaming is inconvenient. You must [initialise a session](/ai/agent-graph/init-session) before calling this endpoint. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/ai/agent_graph/invoke_sync ``` ## Request body Same as Invoke (Stream) — `businessId`, `clientKey`, `sessionId`, optional `agentId`, and `inputEvents` with the same supported event types. ## Response The session ID for this conversation. All agent response events returned in a single array. Each element contains exactly one event key: A text, audio, image, or location message from the agent. A list of product recommendations from the agent. | Field | Type | Description | | ------------------------ | ------ | ----------------------------- | | `products[].productId` | string | Connectly product identifier. | | `products[].title` | string | Product name. | | `products[].description` | string | Product description. | | `products[].url` | string | Product page URL. | | `products[].imageUrl` | string | Product image URL. | | `products[].price` | number | Product price. | | `products[].currency` | string | ISO 4217 currency code. | Suggested follow-up questions for the customer. | Field | Type | Description | | -------------------- | --------- | ----------------------------------- | | `suggestedQuestions` | string\[] | List of suggested question strings. | Signals the conversation should be handed over to a human agent. | Field | Type | Description | | ----------------- | ------- | ---------------------------------- | | `triggerHandover` | boolean | Whether to initiate a handover. | | `reason` | string | Reason for handover (English). | | `reasonNative` | string | Reason in the customer's language. | An interactive WhatsApp message. The `interactive` object contains one of: `listMessage`, `replyButtonMessage`, `singleProductMessage`, or `multiProductMessage`. Indicates the agent has ended the session. An empty object `{}`. ## Example ```http theme={null} POST /external/v1/ai/agent_graph/invoke_sync HTTP/1.1 Host: api.connectly.ai x-api-key: YOUR_API_KEY Content-Type: application/json { "businessId": "your-business-id", "clientKey": "customer-123", "sessionId": "your-session-id", "inputEvents": [ { "messageEvent": { "role": "USER", "content": { "textContent": { "text": "What are your store hours?" } } } } ] } ``` ```json theme={null} { "response": { "sessionId": "your-session-id", "responseEvents": [ { "messageEvent": { "role": "ASSISTANT", "content": { "textContent": { "text": "We're open Monday–Friday, 9am–6pm." } } } } ] } } ``` # Agent Graph Overview Source: https://docs.connectly.ai/ai/agent-graph-overview Build programmable, multi-turn AI conversation sessions with Connectly agents — init, invoke, and close sessions entirely in code 🧑‍💻 The Agent Graph API lets you build multi-turn AI-powered conversations with Connectly agents entirely in code. You control the full session lifecycle — starting a session, passing customer input, receiving AI responses, and closing the session when the conversation ends. ## Use cases Embed a Connectly AI agent in your own web or mobile interface. Guide customers through product discovery and purchase flows programmatically. Resolve common queries without human escalation, integrated into your existing systems. *** ## Session lifecycle Every Agent Graph interaction follows a three-phase lifecycle: Call [POST /agent\_graph/init](https://docs.connectly.ai/api-reference/agent-graph-init) with your `businessId` and `clientKey`. Receive a `sessionId` that identifies this conversation. Call [POST /agent\_graph/invoke](https://docs.connectly.ai/api-reference/agent-graph-invoke) (or `/invoke_sync`) with the `sessionId` and customer `inputEvents`. Receive the agent's response. Repeat for each turn in the conversation. Call [POST /agent\_graph/close](https://docs.connectly.ai/api-reference/agent-graph-close) when the conversation is complete to release the session and its resources. *** ## Endpoints | Method | Endpoint | Description | | ------ | ----------------------------------------- | ------------------------------------------------------ | | `POST` | `/external/v1/ai/agent_graph/init` | Start a session — returns `sessionId`. | | `POST` | `/external/v1/ai/agent_graph/invoke` | Send input, receive a streaming NDJSON response. | | `POST` | `/external/v1/ai/agent_graph/invoke_sync` | Send input, receive a single aggregated JSON response. | | `POST` | `/external/v1/ai/agent_graph/close` | End a session and release its resources. | | `POST` | `/external/v1/ai/agent_graph/health` | Check API availability. | All endpoints are hosted at `https://api.connectly.ai`. *** ## Authentication Include your API key in the `x-api-key` header (lowercase): ```text theme={null} x-api-key: YOUR_API_KEY ``` *** ## Required fields All endpoints (except `/init`) require these three fields in the request body: | Field | Description | | ------------ | ----------------------------------------------------------------------------- | | `businessId` | Your Connectly business UUID. | | `clientKey` | A stable identifier for the customer (e.g. phone number or internal user ID). | | `sessionId` | The session ID returned by `/init`. | *** ## Streaming vs sync The `/invoke` endpoint returns a streaming NDJSON response — one JSON object per line — so your application can begin processing the agent's reply progressively rather than waiting for the full response. Use `/invoke_sync` when you don't need streaming — for example, in a backend job that waits for the complete response before proceeding. It returns a single aggregated JSON object and works with a standard `response.json()` call. Never call `response.json()` on a streaming `/invoke` response — it will throw a parse error. See [NDJSON streaming](https://docs.connectly.ai/ai/parsing-ndjson) for the correct approach. *** ## End-to-end example (Python) ```python theme={null} import requests, json BASE_URL = "https://api.connectly.ai/external/v1/ai/agent_graph" HEADERS = {"Content-Type": "application/json", "x-api-key": "YOUR_API_KEY"} BUSINESS_ID = "YOUR_BUSINESS_ID" CLIENT_KEY = "YOUR_CLIENT_KEY" # 1. Init session_id = requests.post(f"{BASE_URL}/init", headers=HEADERS, json={"businessId": BUSINESS_ID, "clientKey": CLIENT_KEY} ).json()["response"]["sessionId"] # 2. Invoke (streaming) response = requests.post(f"{BASE_URL}/invoke", headers=HEADERS, json={ "businessId": BUSINESS_ID, "clientKey": CLIENT_KEY, "sessionId": session_id, "inputEvents": [{ "messageEvent": { "role": "USER", "content": {"textContent": {"text": "What are your store hours?"}} } }] }, stream=True ) for raw_line in response.iter_lines(): if raw_line: print(json.loads(raw_line.decode("utf-8"))) # 3. Close requests.post(f"{BASE_URL}/close", headers=HEADERS, json={"businessId": BUSINESS_ID, "clientKey": CLIENT_KEY, "sessionId": session_id}) ``` *** ## Next steps Start a conversation and get a `sessionId`. Send input events and parse the NDJSON streaming response. Get a single aggregated response without streaming. How to correctly parse the streaming response body. # NDJSON Streaming Source: https://docs.connectly.ai/ai/parsing-ndjson How to correctly parse the NDJSON streaming response from the Agent Graph invoke endpoint — and why response.json() fails 🤖 The Agent Graph `/invoke` endpoint returns a **stream of newline-delimited JSON objects** (NDJSON) rather than a single JSON response. Each line is a complete, self-contained JSON event. This lets your application begin processing the agent's reply progressively rather than waiting for the entire response to arrive. ## Why `response.json()` fails If you try to parse the raw response body as a single JSON document it will throw an error: ```python theme={null} response = requests.post("https://api.connectly.ai/external/v1/ai/agent_graph/invoke", ...) data = response.json() # ❌ JSONDecodeError ``` The response body contains multiple JSON objects separated by newlines — not a single valid JSON document. The standard `response.json()` method cannot handle that format. ## The correct approach Enable streaming on the request and iterate over the response line by line, parsing each non-empty line as its own JSON object: ```python theme={null} import requests, json response = requests.post( "https://api.connectly.ai/external/v1/ai/agent_graph/invoke", headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={ "clientKey": "customer-123", "businessId": "your-business-id", "sessionId": "b6bed81c-5fda-486d-b96c-1ff3af36219a", "inputEvents": [{ "messageEvent": { "role": "USER", "content": {"textContent": {"text": "Hi"}} } }] }, stream=True # ✅ don't buffer the full response ) for raw_line in response.iter_lines(): # ✅ one line at a time if raw_line: # skip keep-alive empty lines event = json.loads(raw_line.decode("utf-8")) print(event) ``` Key differences from a standard request: * Pass `stream=True` so the response body is not buffered all at once. * Use `response.iter_lines()` to read one line at a time. * Skip empty lines with `if raw_line:` — these are keep-alive bytes with no data. * Decode each line from bytes to UTF-8, then parse with `json.loads()`. ## Parsing approaches compared | Approach | Works with `/invoke`? | Notes | | ---------------------------------------- | --------------------- | ---------------------------------------------- | | `response.json()` | ❌ | Throws `JSONDecodeError`. | | `response.iter_lines()` + `json.loads()` | ✅ | Correct — parse each line individually. | | `response.text.split("\n")` | ⚠️ | Works but error-prone — prefer `iter_lines()`. | | `/invoke_sync` + `response.json()` | ✅ | No streaming; waits for the full response. | If you don't need real-time streaming — for example, in a backend worker that waits for the complete response — use [Invoke (sync)](/ai/agent-graph/invoke-sync) instead. It returns a single aggregated JSON object and works with a standard `response.json()` call. ## What each line contains Each parsed line is a JSON event object from the agent. The structure varies by event type — a `messageEvent` carries the agent's text reply, a `recommendationEvent` carries product suggestions, an `agentHandoverEvent` signals a human handoff. See [Invoke (stream)](https://docs.connectly.ai/ai/agent-graph-invoke) for the full response event schema. # Sofia AI Overview Source: https://docs.connectly.ai/ai/sofia-ai Configure Sofia AI with your business knowledge, test it on WhatsApp, and integrate it into your campaign flows or call it directly via API 🦉 Sofia AI automatically answers common customer questions about your products and services via WhatsApp — instantly, without human agent involvement. Once configured with your business knowledge, Sofia handles inbound queries and hands off to a human agent when needed. ## Setting up Sofia AI Visit [app.connectly.ai/sofia](http://app.connectly.ai/sofia) to begin. Provide Sofia with the information it needs to answer customer questions. You have three options: * **Enter text manually** — type or paste your business information directly. * **Upload documents** — upload PDFs, user manuals, store policies, or Q\&A documents. * **Scan your website** — enter your website URL and Connectly will crawl it automatically. Allow approximately 3 business days for the scan to complete. The more thorough and accurate your knowledge base, the better Sofia's responses will be. We recommend including Q\&A sections, product documentation, store policies, blogs, and any other materials relevant to your customers' common questions. Provide Sofia with the information it needs to answer customer questions. You have three options: * **Enter text manually** — type or paste your business information directly. * **Upload documents** — upload PDFs, user manuals, store policies, or Q\&A documents. * **Scan your website** — enter your website URL and Connectly crawls it automatically. Allow approximately 3 business days for the scan to complete. The more comprehensive your knowledge base, the better Sofia's responses will be. We recommend including Q\&A sections, product documentation, store policies, and any other material relevant to your customers' common questions. For the text entry and document upload options, setup takes under 5 minutes. Once your knowledge base is ready, the **Test on WhatsApp** button becomes active. Click it to open WhatsApp and interact with Sofia directly — send questions your customers commonly ask and review the responses. You can continue adding information at any time to improve accuracy. For text entry and document upload, setup takes under 5 minutes. Once your knowledge base is ready, the **Test on WhatsApp** button becomes active. Click it to open WhatsApp and interact with Sofia directly. You can continue adding information at any time to improve accuracy. *** ## Integrating Sofia into a campaign flow After activating Sofia, you can hand off a conversation to it at any point in your Campaign Builder flow. With Sofia activated, a **Sofia AI Takeover** card appears in the left sidebar of the Campaign Builder. Drag the card onto the canvas and connect it to the point in your flow where you want Sofia to take over. Sofia will handle all subsequent customer messages from that point forward. Drag the card onto your canvas and connect it to the point in your flow where you want Sofia to take over. Sofia handles all subsequent customer messages from that point forward. When you place the Sofia AI Takeover card, a **Simple message** card is created and linked to it automatically. This card sends a welcome message to the customer before Sofia begins responding. Edit it to match your brand's tone and let customers know they are interacting with a virtual assistant. A **Simple message** card is automatically linked to the Sofia AI Takeover card. This sends a welcome message before Sofia begins responding — edit it to match your brand's tone and let customers know they're interacting with a virtual assistant. *** ## Core concepts If you're integrating Sofia through the API, you'll work with these identifiers: | Concept | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `agent_descriptor_id` | Identifies your Sofia agent configuration — knowledge bases, language, product catalogs. Connectly provisions this during onboarding. | | `business_id` | Your Connectly business identifier. | | `api_key` | An API key scoped exclusively to invoking Sofia. It cannot send WhatsApp messages or access campaign data — safe to use in client-side JavaScript. | | `session_id` | A unique identifier for a single customer conversation. Obtained from the [init endpoint](/ai/agent-graph/init-session) and required on every subsequent invoke and close call. | | `state_id` | Identifies a specific point within a conversation. Pass the most recent `state_id` when invoking Sofia to continue from exactly where the customer left off. | Your Sofia `api_key` is restricted to Sofia invocations only. It cannot send WhatsApp messages or access campaign reports, so it's safe to include in browser-side code. *** ## API usage To invoke Sofia programmatically — for example, to embed it in your own chat interface — see [Invoke Sofia AI](https://docs.connectly.ai/ai/sofia-ai-invoke). # Invoke Sofia AI Source: https://docs.connectly.ai/ai/sofia-ai-invoke POST /external/v1/ai/agent_graph/invoke Call the Sofia AI sales assistant with a customer message and receive a streaming AI-generated response 🦾 Sofia AI is built on the Agent Graph infrastructure. Invoking it uses the standard Agent Graph invoke endpoint — the only Sofia-specific value you supply is the `agentId` (your `agent_descriptor_id`) that Connectly provides during onboarding. To set up Sofia AI for production, contact your Connectly Account Manager. They will configure your knowledge base and product catalog, then provide your `agent_descriptor_id`, `business_id`, and `api_key`. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/ai/agent_graph/invoke ``` ## Authentication Use the Sofia-specific API key provided by Connectly — not your general Connectly API key. Pass it in the `x-api-key` header (lowercase). ```text theme={null} x-api-key: YOUR_SOFIA_API_KEY ``` Your Sofia `api_key` is scoped to inference only and is safe to use in client-side JavaScript — it cannot send WhatsApp messages or access campaign data. *** ## Session lifecycle Every Sofia AI conversation follows the standard Agent Graph three-step lifecycle: Call [POST /agent\_graph/init](/ai/agent-graph/init-session) with your `businessId` and `clientKey` to receive a `sessionId`. Call this endpoint with the `sessionId` and your `inputEvents` to send a customer message and receive the agent's reply. Call [POST /agent\_graph/close](/ai/agent-graph/close-session) with the `sessionId` when the conversation ends. *** ## Request body The `business_id` provided by Connectly for your Sofia AI integration. A unique identifier for the customer. Use a stable ID from your system so you can correlate sessions with users. The session ID returned by the [init endpoint](/ai/agent-graph/init-session). Your `agent_descriptor_id` — provided by Connectly during onboarding. Identifies the Sofia agent configuration to use. The events to send to the agent. For a standard text interaction, include a single `messageEvent` with the customer's text. ```json theme={null} { "messageEvent": { "role": "USER", "content": { "textContent": { "text": "I'm looking for a running shoe under $100" } } } } ``` For the full list of supported event types — button responses, list replies, form submissions, store events — see [Invoke (stream)](/ai/agent-graph/invoke-stream). *** ## Response The response is an NDJSON stream — one JSON object per line, each representing one agent response event. Do not call `response.json()` on the raw response — it will throw a parse error. Parse each line individually. See [NDJSON streaming](/ai/ndjson-streaming) for a complete guide. *** ## Full example (Python) ```python theme={null} import requests, json BASE_URL = "https://api.connectly.ai/external/v1/ai/agent_graph" HEADERS = { "x-api-key": "YOUR_SOFIA_API_KEY", "Content-Type": "application/json" } BUSINESS_ID = "your-sofia-business-id" CLIENT_KEY = "customer-456" AGENT_ID = "your-agent-descriptor-id" # 1. Init session init_res = requests.post(f"{BASE_URL}/init", headers=HEADERS, json={"businessId": BUSINESS_ID, "clientKey": CLIENT_KEY}) session_id = init_res.json()["response"]["sessionId"] # 2. Invoke Sofia AI invoke_res = requests.post(f"{BASE_URL}/invoke", headers=HEADERS, json={ "businessId": BUSINESS_ID, "clientKey": CLIENT_KEY, "sessionId": session_id, "agentId": AGENT_ID, "inputEvents": [{ "messageEvent": { "role": "USER", "content": {"textContent": {"text": "I'm looking for a running shoe under $100"}} } }] }, stream=True ) # 3. Parse NDJSON stream line by line for raw_line in invoke_res.iter_lines(): if raw_line: event = json.loads(raw_line.decode("utf-8")) print(event) # 4. Close session requests.post(f"{BASE_URL}/close", headers=HEADERS, json={"businessId": BUSINESS_ID, "clientKey": CLIENT_KEY, "sessionId": session_id}) ``` *** ## Related How to correctly parse the streaming response body. Full parameter docs including all input and response event types. # Campaign Report Source: https://docs.connectly.ai/analytics/campaign-report Download a per-customer CSV breakdown of every message in a campaign sendout — delivery timestamps, engagement, button clicks, and errors 🗂️ Campaign reports give you a per-customer breakdown of every message sent in a sendout — including delivery timestamps, engagement actions, and any errors. Download the CSV from the Connectly UI or retrieve it programmatically via the [Reports API](https://docs.connectly.ai/analytics/reports-api). ## Downloading from the UI Open your campaign in the Connectly UI, navigate to the **Analytics** tab, and click **Download CSV**. The file downloads immediately. ## Column reference Each row represents a single customer in the sendout. Columns always appear in this order. | Column | Description | Example | | ---------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `customer_external_id` | Customer's external ID (typically their phone number). | `+123456789` | | `business_id` | Your Connectly business UUID. | `2d503e81-e270-4467-a05f-21a900efbbc1` | | `campaign_name` | Name of the campaign. | `Test campaign` | | `sendout_id` | UUID of the sendout this message belongs to. | `d69ed06c-5385-4003-aa58-aae87f2e087e` | | `cnt_session_id` | Connectly session identifier for this conversation. | `018f6423-fa75-de08-9f61-59a8526eb5c0` | | `sent_at` | Timestamp when the message was sent. | `2024-05-10 20:13:50.682000` | | `delivered_at` | Timestamp when the message was delivered to the device. | `2024-05-10 20:13:50.682000` | | `read_at` | Timestamp when the message was read. | `2024-05-10 20:13:50.682000` | | `opt_out_at` | Timestamp when the customer opted out. | `2024-05-10 20:13:50.682000` | | `error_code` | Error code if the message was undeliverable. | `131026` | | `error_msg` | Human-readable error description. | `Message Undeliverable.` | | `button_clicks` | JSON array of button interactions — each with `id`, `name`, and `ts`. | `{"id":"f2390b","name":"Yes","ts":"2024-07-16 12:11:03.609000"}` | | `link_clicks` | JSON array of link click events — each with `id`, `name`, and `ts`. | `{"id":"41e753","name":"Click here","ts":"2024-07-16 12:12:03.609000"}` | | `input_variables` | JSON object of variables injected at send time. | `{"name":"Ana"}` | | `output_variables` | JSON object of variables captured from the customer's replies. | `{"replied":true}` | Timestamp columns (`sent_at`, `delivered_at`, `read_at`, `opt_out_at`) are blank when the event hasn't occurred yet. For example, `read_at` is empty if the customer hasn't opened the message. ## Understanding the engagement columns **`button_clicks`** — populated when a customer taps a quick-reply or call-to-action button. Each entry records the button's `id`, display `name`, and click timestamp `ts`. **`link_clicks`** — populated when a customer taps a tracked URL. Same structure as `button_clicks`. **`input_variables` / `output_variables`** — only populated for campaigns using variables or automation logic. `input_variables` reflects values injected at send time; `output_variables` captures data collected from the customer's replies during the conversation flow. ## Programmatic access To retrieve reports automatically — for example to ingest them into a data warehouse on a schedule — use the [Reports API](https://docs.connectly.ai/analytics/reports-api) or subscribe to [report webhooks](https://docs.connectly.ai/analytics/report-webhooks). # Conversion Reporting Source: https://docs.connectly.ai/analytics/conversion-reporting POST /external/v1/businesses/{business_id}/conversion_events Report WhatsApp-attributed purchases and product views to Meta's Conversions API via Connectly 🧾 When a customer clicks a Click-to-WhatsApp (CTWA) ad and later purchases on your site, Connectly can forward that conversion event to Meta's Conversions API on your behalf — so the originating ad gets credit in Ads Manager. You send the event to Connectly; you never need to talk to Meta directly. ## Endpoint ```json theme={null} POST https://api.connectly.ai/external/v1/businesses/{business_id}/conversion_events ``` ## Integration journey Open the Connectly inbox → **Settings** → **General** → **API Key**. Create a new key with all scopes unchecked (full access) or reuse an existing one if you still have the plaintext. The key is shown only once — copy and store it securely. Never expose it client-side or commit it to source control. In the Flow Builder, open the **Audience** step of your Click-to-WhatsApp card and tick **"Track purchases completed on my own site and report them to the Ads Manager"**. Without this, carousel CTA links will not carry the required tracking parameters. Once your campaign sends, Connectly auto-appends five query parameters to every CTA link: ```text theme={null} ?cnct_tracking_id=&sendout_id=&ctwa_clid=&ad_id=&attribution_source=meta_ctwa ``` Persist all five values when the customer lands on your site — store them in the session or against the customer record — so they're available at checkout. When the customer completes a purchase, POST a single conversion event to Connectly with the tracking parameters captured at landing. Connectly forwards the event to Meta. Conversions typically appear in Meta Events Manager and roll up into Ads Manager attribution within a few hours. *** ## Request body ### Top-level fields Verbatim copy of the `cnct_tracking_id` query parameter from the landing URL. Identifies the originating WhatsApp/CTWA session at the customer level. Meta CAPI event name. Accepted values: `"Purchase"` or `"ViewContent"`. Any other value returns `400 INVALID_ARGUMENT`. Verbatim copy of the `sendout_id` query parameter from the landing URL. Credits the conversion to the correct Connectly campaign. CTWA attribution data. Verbatim copy of the `ctwa_clid` query parameter from the landing URL. Forwarded to Meta CAPI as `user_data.ctwa_clid` to credit the originating ad. Verbatim copy of the `ad_id` query parameter from the landing URL. Used in Connectly's per-ad conversion analytics — not forwarded to Meta CAPI directly. Per-event detail. For `Purchase`, `currency` and `value` are required. ISO 4217 currency code (e.g. `"USD"`, `"BRL"`, `"MXN"`). Required for `Purchase`. Total order value as a decimal (e.g. `99.97`). Must be ≥ 0. Required for `Purchase`. Your internal order reference. Strongly recommended — Connectly passes this to Meta as the `event_id` deduplication key so the CAPI event and your browser pixel don't double-count the same purchase. Keep it stable per order; the request is safe to retry. `"product"` for SKU-level catalog matching, or `"product_group"` for group-level. Array of SKU or product IDs matching your Meta product catalog. Max 100 items. Total cart size — sum of `contents[].quantity`. Per-line-item detail. Preferred over `content_ids` alone when quantity matters. Max 100 items. SKU or product ID matching your Meta catalog. Items without an `id` are silently skipped. Units of this item. Without it, multi-item orders appear as single-item orders to Meta's bid optimizer. Per-unit price. Enables Meta to compute revenue per item for value-based bidding. Unix epoch timestamp (seconds) of when the order occurred. Defaults to receive-time if omitted. Important for batched or backfilled events — Meta rejects events older than 7 days. *** ## Example request ```bash theme={null} curl -i -X POST "https://api.connectly.ai/external/v1/businesses//conversion_events" \ -H "Content-Type: application/json" \ -H "X-API-KEY: " \ -d '{ "cnct_tracking_id": "", "event_name": "Purchase", "sendout_id": "", "attribution": { "meta_ctwa": { "ctwa_clid": "", "ad_id": "" } }, "payload": { "currency": "USD", "value": 99.97, "order_id": "ORD-7821", "content_type": "product", "content_ids": ["sku-1", "sku-2"], "num_items": 2, "contents": [ { "id": "sku-1", "quantity": 1, "item_price": 49.99 }, { "id": "sku-2", "quantity": 1, "item_price": 49.98 } ], "event_time": 1746480000 } }' ``` ## Response ```json theme={null} { "events_received": 1 } ``` `events_received: 1` confirms Connectly received and recorded the event. Connectly logs the conversion to your campaign analytics regardless of whether the Meta CAPI forward succeeds. If Meta rejects the event, you receive a non-200 response with Meta's verbatim error message. *** ## Error responses | HTTP status | When | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400 INVALID_ARGUMENT` | Missing or invalid required fields; missing `currency`/`value` for a `Purchase` event; unsupported `event_name`; or Meta rejected the forwarded event. | | `401 Unauthenticated` | Missing or invalid `X-API-KEY`. | | `404 NOT_FOUND` | Business not found, or the business has no WhatsApp Cloud channel configured. | *** ## Notes Send **one event per API call**. For multi-item orders, include all items in the `contents[]` array within a single `Purchase` event — do not send multiple POST requests for the same order. * `cnct_tracking_id`, `sendout_id`, `ctwa_clid`, and `ad_id` must be captured from the landing page URL at visit time and passed back when the customer converts — which may happen later in the same session. * Set `event_time` to the actual order timestamp, not the time you call the API. This matters if you flush events in batches or run backfills. * `order_id` doubles as Meta's dedup key — keep it stable per order so retries are safe. Open the Connectly inbox → **Settings** → **General** → **API Key**. Create a new key with all scopes unchecked (full access) or reuse an existing one if you still have the plaintext. The key is shown only once — copy and store it securely. Never expose it client-side or commit it to source control. In the Flow Builder, open the **Audience** step of your Click-to-WhatsApp card and tick **"Track purchases completed on my own site and report them to the Ads Manager"**. Without this, carousel CTA links will not carry the required tracking parameters. Once your campaign sends, Connectly auto-appends five query parameters to every CTA link: ```text theme={null} ?cnct_tracking_id=&sendout_id=&ctwa_clid=&ad_id=&attribution_source=meta_ctwa ``` Persist all five values when the customer lands on your site — store them in the session or against the customer record — so they're available at checkout. When the customer completes a purchase, POST a single conversion event to Connectly with the tracking parameters captured at landing. Connectly forwards the event to Meta. Conversions typically appear in Meta Events Manager and roll up into Ads Manager attribution within a few hours. *** ## Request body Verbatim copy of the `cnct_tracking_id` query parameter from the landing URL. Identifies the originating WhatsApp/CTWA session at the customer level. Meta CAPI event name. Accepted values: `"Purchase"` or `"ViewContent"`. Any other value returns `400 INVALID_ARGUMENT`. Verbatim copy of the `sendout_id` query parameter from the landing URL. Credits the conversion to the correct Connectly campaign. CTWA attribution data. Verbatim copy of the `ctwa_clid` query parameter from the landing URL. Forwarded to Meta CAPI as `user_data.ctwa_clid` to credit the originating ad. Verbatim copy of the `ad_id` query parameter from the landing URL. Used in Connectly's per-ad conversion analytics — not forwarded to Meta CAPI directly. Per-event detail. For `Purchase`, `currency` and `value` are required. ISO 4217 currency code (e.g. `"USD"`, `"BRL"`, `"MXN"`). Required for `Purchase`. Total order value as a decimal (e.g. `99.97`). Must be ≥ 0. Required for `Purchase`. Your internal order reference. Strongly recommended — Connectly passes this to Meta as the `event_id` deduplication key so the CAPI event and your browser pixel don't double-count the same purchase. Keep it stable per order; the request is safe to retry. `"product"` for SKU-level catalog matching, or `"product_group"` for group-level. Array of SKU or product IDs matching your Meta product catalog. Max 100 items. Total cart size — sum of `contents[].quantity`. Per-line-item detail. Preferred over `content_ids` alone when quantity matters. Max 100 items. SKU or product ID matching your Meta catalog. Items without an `id` are silently skipped. Units of this item. Without it, multi-item orders appear as single-item orders to Meta's bid optimizer. Per-unit price. Enables Meta to compute revenue per item for value-based bidding. Unix epoch timestamp (seconds) of when the order occurred. Defaults to receive-time if omitted. Important for batched or backfilled events — Meta rejects events older than 7 days. *** ```bash theme={null} curl -i -X POST "https://api.connectly.ai/external/v1/businesses//conversion_events" \ -H "Content-Type: application/json" \ -H "X-API-KEY: " \ -d '{ "cnct_tracking_id": "", "event_name": "Purchase", "sendout_id": "", "attribution": { "meta_ctwa": { "ctwa_clid": "", "ad_id": "" } }, "payload": { "currency": "USD", "value": 99.97, "order_id": "ORD-7821", "content_type": "product", "content_ids": ["sku-1", "sku-2"], "num_items": 2, "contents": [ { "id": "sku-1", "quantity": 1, "item_price": 49.99 }, { "id": "sku-2", "quantity": 1, "item_price": 49.98 } ], "event_time": 1746480000 } }' ``` `events_received: 1` confirms Connectly received and recorded the event. Connectly logs the conversion to your campaign analytics regardless of whether the Meta CAPI forward succeeds. If Meta rejects the event, you receive a non-200 response with Meta's verbatim error message. *** ## Error responses | HTTP status | When | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400 INVALID_ARGUMENT` | Missing or invalid required fields; missing `currency`/`value` for a `Purchase` event; unsupported `event_name`; or Meta rejected the forwarded event. | | `401 Unauthenticated` | Missing or invalid `X-API-KEY`. | | `404 NOT_FOUND` | Business not found, or the business has no WhatsApp Cloud channel configured. | *** Send **one event per API call**. For multi-item orders, include all items in the `contents[]` array within a single `Purchase` event — do not send multiple POST requests for the same order. * `cnct_tracking_id`, `sendout_id`, `ctwa_clid`, and `ad_id` must be captured from the landing page URL at visit time and passed back when the customer converts — which may happen later in the same session. * Set `event_time` to the actual order timestamp, not the time you call the API. This matters if you flush events in batches or run backfills. * `order_id` doubles as Meta's dedup key — keep it stable per order so retries are safe. Open the Connectly inbox → **Settings** → **General** → **API Key**. Create a new key with all scopes unchecked (full access) or reuse an existing one if you still have the plaintext. The key is shown only once — copy and store it securely. Never expose it client-side or commit it to source control. In the Flow Builder, open the **Audience** step of your Click-to-WhatsApp card and tick **"Track purchases completed on my own site and report them to the Ads Manager"**. Without this, carousel CTA links will not carry the required tracking parameters. Once your campaign sends, Connectly auto-appends five query parameters to every CTA link: ```text theme={null} ?cnct_tracking_id=&sendout_id=&ctwa_clid=&ad_id=&attribution_source=meta_ctwa ``` Persist all five values when the customer lands on your site — store them in the session or against the customer record — so they're available at checkout. When the customer completes a purchase, POST a single conversion event to Connectly with the tracking parameters captured at landing. Connectly forwards the event to Meta. Conversions typically appear in Meta Events Manager and roll up into Ads Manager attribution within a few hours. *** ## Request body Verbatim copy of the `cnct_tracking_id` query parameter from the landing URL. Identifies the originating WhatsApp/CTWA session at the customer level. Meta CAPI event name. Accepted values: `"Purchase"` or `"ViewContent"`. Any other value returns `400 INVALID_ARGUMENT`. Verbatim copy of the `sendout_id` query parameter from the landing URL. Credits the conversion to the correct Connectly campaign. CTWA attribution data. Verbatim copy of the `ctwa_clid` query parameter from the landing URL. Forwarded to Meta CAPI as `user_data.ctwa_clid` to credit the originating ad. Verbatim copy of the `ad_id` query parameter from the landing URL. Used in Connectly's per-ad conversion analytics — not forwarded to Meta CAPI directly. Per-event detail. For `Purchase`, `currency` and `value` are required. ISO 4217 currency code (e.g. `"USD"`, `"BRL"`, `"MXN"`). Required for `Purchase`. Total order value as a decimal (e.g. `99.97`). Must be ≥ 0. Required for `Purchase`. Your internal order reference. Strongly recommended — Connectly passes this to Meta as the `event_id` deduplication key so the CAPI event and your browser pixel don't double-count the same purchase. Keep it stable per order; the request is safe to retry. `"product"` for SKU-level catalog matching, or `"product_group"` for group-level. Array of SKU or product IDs matching your Meta product catalog. Max 100 items. Total cart size — sum of `contents[].quantity`. Per-line-item detail. Preferred over `content_ids` alone when quantity matters. Max 100 items. SKU or product ID matching your Meta catalog. Items without an `id` are silently skipped. Units of this item. Without it, multi-item orders appear as single-item orders to Meta's bid optimizer. Per-unit price. Enables Meta to compute revenue per item for value-based bidding. Unix epoch timestamp (seconds) of when the order occurred. Defaults to receive-time if omitted. Important for batched or backfilled events — Meta rejects events older than 7 days. *** ```bash theme={null} curl -i -X POST "https://api.connectly.ai/external/v1/businesses//conversion_events" \ -H "Content-Type: application/json" \ -H "X-API-KEY: " \ -d '{ "cnct_tracking_id": "", "event_name": "Purchase", "sendout_id": "", "attribution": { "meta_ctwa": { "ctwa_clid": "", "ad_id": "" } }, "payload": { "currency": "USD", "value": 99.97, "order_id": "ORD-7821", "content_type": "product", "content_ids": ["sku-1", "sku-2"], "num_items": 2, "contents": [ { "id": "sku-1", "quantity": 1, "item_price": 49.99 }, { "id": "sku-2", "quantity": 1, "item_price": 49.98 } ], "event_time": 1746480000 } }' ``` `events_received: 1` confirms Connectly received and recorded the event. Connectly logs the conversion to your campaign analytics regardless of whether the Meta CAPI forward succeeds. If Meta rejects the event, you receive a non-200 response with Meta's verbatim error message. *** ## Error responses | HTTP status | When | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400 INVALID_ARGUMENT` | Missing or invalid required fields; missing `currency`/`value` for a `Purchase` event; unsupported `event_name`; or Meta rejected the forwarded event. | | `401 Unauthenticated` | Missing or invalid `X-API-KEY`. | | `404 NOT_FOUND` | Business not found, or the business has no WhatsApp Cloud channel configured. | *** Send **one event per API call**. For multi-item orders, include all items in the `contents[]` array within a single `Purchase` event — do not send multiple POST requests for the same order. * `cnct_tracking_id`, `sendout_id`, `ctwa_clid`, and `ad_id` must be captured from the landing page URL at visit time and passed back when the customer converts — which may happen later in the same session. * Set `event_time` to the actual order timestamp, not the time you call the API. This matters if you flush events in batches or run backfills. * `order_id` doubles as Meta's dedup key — keep it stable per order so retries are safe. # Report Webhooks Source: https://docs.connectly.ai/analytics/report-webhooks Register a webhook endpoint to receive an instant notification — with a signed download URL — when a campaign report run completes 🔔 Instead of polling the [Reports API](https://docs.connectly.ai/analytics/reports-api) on a schedule, register a webhook endpoint that Connectly calls the moment a report run completes. The payload includes a signed URL so you can download the CSV immediately — no extra API call required. The signed URL in a webhook payload expires after a **few minutes only**. Download the file immediately upon receiving the webhook. If the URL expires before you can download it, call the [Reports API](https://docs.connectly.ai/analytics/reports-api) to retrieve a fresh one. ## Registering the webhook Use the [Create webhook](https://docs.connectly.ai/api-reference/create-webhook) endpoint with `"topic": "report"`: ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "topic": "report", "address": "https://yourdomain.com/connectly-webhooks", "type": "custom" }' ``` | Field | Description | | --------- | ------------------------------------------------------- | | `topic` | Must be `"report"` to receive report completion events. | | `address` | Your publicly accessible HTTPS endpoint. | | `type` | Set to `"custom"` for a generic HTTP webhook. | ## Webhook payload When a report run completes, Connectly POSTs the following payload to your endpoint: ```json theme={null} { "topic": "report", "timestamp": "2025-04-30T12:34:56Z", "report": { "id": "c80716a5-62c5-4ba5-b99d-5c2a6fd39317", "name": "Daily Campaign Report", "type": "campaign.daily.custom", "last_run": { "id": "run-001", "status": "completed", "result": { "url": "https://reports.connectly.ai/abcdef", "expires_at": "2025-05-01T12:34:56Z", "created_at": "2025-04-30T12:00:00Z" } } } } ``` | Field | Description | | ----------------------------------- | --------------------------------------------------------------- | | `topic` | Always `"report"` for this webhook type. | | `timestamp` | ISO 8601 timestamp of when the webhook was sent. | | `report.id` | Unique ID of the report configuration. | | `report.name` | Human-readable report name. | | `report.type` | Report type identifier. | | `report.last_run.id` | ID of the completed run. | | `report.last_run.status` | Always `"completed"` when the webhook fires. | | `report.last_run.result.url` | Signed URL to download the report CSV. Expires at `expires_at`. | | `report.last_run.result.expires_at` | When the signed URL expires — download immediately. | | `report.last_run.result.created_at` | When the report file was generated. | ## Recommended handling Return a `2xx` response as soon as you receive the webhook — before doing any processing. Connectly may retry if it doesn't receive a timely acknowledgement. Read `report.last_run.result.url` and check `expires_at` to confirm the URL is still valid. Perform a GET request to the signed URL right away — it expires within minutes. If your handler is delayed and the URL has expired, call the [Reports API](https://docs.connectly.ai/analytics/reports-api) to retrieve a fresh signed URL for the same report. Your webhook endpoint must be reachable over HTTPS. Connectly does not deliver webhooks to plain HTTP addresses. # Reports API Source: https://docs.connectly.ai/analytics/reports-api GET /v1/businesses/{businessId}/reporting/reports Retrieve signed, time-limited CSV download URLs for your latest scheduled campaign report runs 📉 Retrieve download links for your campaign reports without logging into the Connectly UI. Each response contains a signed URL pointing to the same CSV you would download manually — making it easy to automate ingestion into a data warehouse or BI tool. ## Endpoint ```json theme={null} GET https://api.connectly.ai/v1/businesses/{businessId}/reporting/reports ``` ## Query parameters Filter results to a specific report type (e.g. `campaign.daily.v1`). If omitted, all configured report types are returned. ## How it works This endpoint returns the **most recent completed run** of each report configured for your business. Reports are generated on a fixed schedule (daily, monthly, etc.) — there is no way to trigger a new run on demand via the API. The `url` in each result is a **signed, temporary link** that expires after the report's configured TTL (1 hour by default). Always check the `expiresAt` field and download the file before then. If the URL has expired, call this endpoint again to get a fresh one from the next completed run — or subscribe to [report webhooks](/analytics/report-webhooks) to be notified the moment each new run completes. ## Example ```bash theme={null} curl -X GET "https://api.connectly.ai/v1/businesses/{businessId}/reporting/reports" \ -H "X-API-Key: YOUR_API_KEY" ``` ```json theme={null} { "data": [ { "id": "c80716a5-62c5-4ba5-b99d-5c2a6fd39317", "name": "Daily campaign report", "type": "campaign.daily.v1", "lastRun": { "id": "c80716a5-62c5-4ba5-b99d-5c2a6fd39317", "status": "completed", "result": { "url": "https://reports.connectly.ai/abcdef", "expiresAt": "2025-05-01T13:00:00Z", "createdAt": "2025-04-30T12:00:00Z" } } } ] } ``` ## Response fields | Field | Description | | --------------------------------- | ---------------------------------------------------------------------------- | | `data[].id` | Unique ID of the report configuration. | | `data[].name` | Human-readable report name. | | `data[].type` | Report type identifier — use this with the `type` query parameter to filter. | | `data[].lastRun.id` | ID of the most recent run. | | `data[].lastRun.status` | Run status: `processing`, `completed`, or `failed`. | | `data[].lastRun.result.url` | Signed URL to download the report as a CSV. Expires at `expiresAt`. | | `data[].lastRun.result.expiresAt` | ISO 8601 timestamp when the signed URL expires. Download before this time. | | `data[].lastRun.result.createdAt` | ISO 8601 timestamp when this report run completed. | ## Downloading the CSV Once you have the signed URL from `result.url`, perform a standard HTTP GET to download the file. For the column reference, see [Campaign report](/analytics/campaign-report). Subscribe to [report webhooks](/analytics/report-webhooks) to receive an immediate notification — including a fresh signed URL — the moment a new report run completes, rather than polling this endpoint on a schedule. # Create Asset Source: https://docs.connectly.ai/assets/create-asset POST /v1/businesses/{businessId}/assets Upload a media file to Connectly's CDN by providing a source URL. Returns a stable CDN URL for use in templates and messages 🎧 Upload a media file to Connectly's CDN by providing a publicly accessible source URL. Connectly fetches the file from your URL and stores it, returning a stable CDN URL you can reference in templates, campaigns, and messages. ## Endpoint ```json theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/assets ``` **Rate limit:** 100 requests/second. Exceeding this returns `429 Too Many Requests`. ## Request body Publicly accessible source URL of the file to upload. Must be reachable from Connectly's servers — private or authenticated URLs will fail with a `400` error. Optional custom identifier for the asset. If omitted, Connectly generates a UUID automatically. Useful for associating assets with your own records (e.g. a product SKU or internal asset key). Optional metadata to attach to the asset. Controls who can access the stored asset. Defaults to `ASSET_ACCESS_CONTROL_TYPE_PUBLIC`. | Value | Description | | --------------------------------------- | ----------------------------------------------------- | | `ASSET_ACCESS_CONTROL_TYPE_PUBLIC` | Publicly accessible without authentication (default). | | `ASSET_ACCESS_CONTROL_TYPE_PRIVATE` | Requires authentication to access. | | `ASSET_ACCESS_CONTROL_TYPE_CDN_PRIVATE` | Accessible only via Connectly's CDN. | ## Response Unique identifier for the asset — either your custom `id` or a generated UUID. The business ID that owns this asset. The Connectly CDN URL where the asset is now hosted. Use this when referencing the asset in templates or messages. The original source URL you provided in the request. The access control setting applied to this asset. ## Examples ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses/{businessId}/assets" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "uri": "https://example.com/images/product-photo.png" }' ``` ```json theme={null} { "asset": { "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "ownerId": "550e8400-e29b-41d4-a716-446655440000", "uri": "https://cdn.connectly.ai/assets/6ba7b810-9dad-11d1-80b4-00c04fd430c8", "source": { "uri": "https://example.com/images/product-photo.png" }, "metadata": { "accessControlType": "ASSET_ACCESS_CONTROL_TYPE_PUBLIC" } } } ``` ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses/{businessId}/assets" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "uri": "https://example.com/images/product-photo.png", "id": "product-sku-12345-main-image" }' ``` ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses/{businessId}/assets" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "uri": "https://example.com/documents/invoice.pdf", "metadata": { "accessControlType": "ASSET_ACCESS_CONTROL_TYPE_CDN_PRIVATE" } }' ``` ## Error responses | Status | Meaning | | ------ | --------------------------------------------------------------------------- | | `400` | Missing or invalid `uri`, or source URL unreachable by Connectly's servers. | | `401` | Missing or invalid `X-API-Key`. | | `429` | Rate limit exceeded (100 req/s). | # Get Asset Source: https://docs.connectly.ai/assets/get-asset GET /v1/businesses/{businessId}/assets/{assetId} Retrieve the CDN URL for a previously uploaded asset by its ID 🪪 Look up the CDN URL for an asset you previously uploaded via [Create asset](/assets/create-asset). Use the returned `cdnUri` to reference the asset in templates, campaigns, and messages. ## Endpoint ```json theme={null} GET https://api.connectly.ai/v1/businesses/{businessId}/assets/{assetId} ``` **Rate limit:** 100 requests/second. Exceeding this returns `429 Too Many Requests`. ## Path parameters Your Connectly business ID (UUID format). The ID of the asset to retrieve — either the UUID Connectly generated or the custom ID you provided at upload time. ## Response The Connectly CDN URL where the asset is hosted (e.g. `https://cdn.connectly.ai/assets/6ba7b810-9dad-11d1-80b4-00c04fd430c8`). Use this URL when embedding media in templates or messages. ## Example ```bash theme={null} curl -X GET "https://api.connectly.ai/v1/businesses/{businessId}/assets/6ba7b810-9dad-11d1-80b4-00c04fd430c8" \ -H "X-API-Key: YOUR_API_KEY" ``` ```json theme={null} { "cdnUri": "https://cdn.connectly.ai/assets/6ba7b810-9dad-11d1-80b4-00c04fd430c8" } ``` ## Error responses | Status | Meaning | | ------ | ------------------------------------- | | `400` | Invalid `assetId` format. | | `401` | Missing or invalid `X-API-Key`. | | `404` | No asset found with the specified ID. | | `429` | Rate limit exceeded (100 req/s). | # Assets Overview Source: https://docs.connectly.ai/assets/overview Upload media files to Connectly's CDN and retrieve stable CDN URLs for use in templates, campaigns, and messages 🌁 The Assets API lets you upload media files to Connectly's CDN and retrieve their stable CDN URLs. Instead of hosting media yourself and worrying about availability, you provide a source URL and Connectly fetches and stores the file — returning a CDN URL you can use reliably in templates, campaigns, and messages. ## How it works Call [Create asset](https://docs.connectly.ai/assets/create-asset) with a publicly accessible URL of your file. Connectly fetches the file and stores it on the CDN. The response includes `asset.uri` — a stable CDN URL hosted by Connectly. Store this alongside your asset records. Pass `asset.uri` as the `value` for `header_image`, `header_document`, or any attachment URL in your template and session message requests. If you only saved the asset ID, use [Get asset](https://docs.connectly.ai/assets/get-asset) to look up the CDN URL at any time. The source URL you provide must be publicly accessible from Connectly's servers. Private, authenticated, or localhost URLs will fail with a `400` error. ## Endpoints `POST /v1/businesses/{businessId}/assets` — upload a file by source URL and get a CDN URL back. `GET /v1/businesses/{businessId}/assets/{assetId}` — retrieve the CDN URL for an existing asset by ID. # How to handle WhatsApp Usernames & BSUIDs Source: https://docs.connectly.ai/business-hub/bsuid/handling-bsuid What WhatsApp usernames mean for your business, how customers without phone numbers appear in Connectly, and what you need to do ✍️ **Last updated:** June 2026 — WhatsApp username rollout is live in pilot countries, global rollout in progress. ## What's changing? WhatsApp is rolling out usernames in 2026. Customers can now set a public username (like `@johndoe`) and choose to **hide their phone number** from businesses they message. When a customer hides their phone number, WhatsApp replaces it with a **Business-Scoped User ID (BSUID)** — a stable identifier that looks like `US.13491208655302741918`. Connectly surfaces this ID so you don't lose the conversation when a customer goes phone-less. This is an **additive change** — phone numbers continue to work exactly as before. The vast majority of your existing customers will not be affected. *** ## What is a BSUID? A BSUID is WhatsApp's way of identifying a customer when their phone number isn't shared. Think of it like a phone number — it's a stable ID that represents a specific customer in your conversations. A few important things to know: * **It's unique to your business** — the same customer will have a different BSUID with a different business. You cannot use a BSUID to contact customers of another business. * **A customer can have both** — a phone number and a BSUID at the same time. When both are available, you'll see both. * **It can change** — if a customer changes their phone number, their BSUID is regenerated. Always use the most recent one. * **Store it like a phone number** — if you sync customer data to a CRM, treat the BSUID as an additional identifier alongside the phone number. *** ## How to identify a phone-less customer in your inbox When a customer contacts you, their profile in the Connectly inbox will show one of three states: | Customer type | What you see | | ------------------------------------ | -------------------- | | Existing customer (pre-username) | Phone number only | | Username enabled, phone still shared | Phone number + BSUID | | Username only, phone hidden | BSUID only | For customers in the third state, the BSUID is the only stable way to identify them. Make a note of it if you need to reference this customer in your CRM or other tools. *** ## How to receive messages from customers who hide their phone number Connectly already receives inbound messages from BSUID customers and displays them in your inbox automatically — **no setup required**. You can read, reply to, and manage these conversations exactly like any other. If you also want these customers' events delivered to your **webhook** — for CRM sync, automations, or other integrations — that requires opting in per WhatsApp number. You can enable phone-less webhook delivery per WhatsApp number. Consider starting with a marketing or acquisition number — phone-less customers are more likely there, and it's lower risk than a support line. Before opting in, check whether your CRM, automations, or support tools rely on phone number as the only customer identifier. If they do, a phone-less customer may create incomplete or duplicate records. Reach out to your Connectly Account Manager and let them know which WhatsApp number(s) you'd like to enable for phone-less webhook delivery. They'll activate it for you. *** ## How to handle customers without a phone number in your CRM If your CRM uses phone number as the primary key for customer records, you'll need to update your setup to support BSUIDs as well. Here's how to approach it: Create a new field in your CRM to store the BSUID alongside the phone number. Don't replace the phone number field — customers can have both. When a new inbound message arrives, check for a BSUID in the webhook payload. If a phone number isn't present, use the BSUID to look up or create the customer record. If a customer changes their phone number, their BSUID will change too. Always update your records with the newest BSUID from the latest webhook event. *** ## How to send messages to BSUID customers Sending outbound messages to phone-less (BSUID-only) customers is **not yet available**. This is blocked by a WhatsApp platform capability that Meta has not yet released. We will enable it as soon as Meta does. In the meantime, if a customer without a phone number messages you first, you can reply to them within the 24-hour session window using the Connectly inbox or the session message API — inbound-initiated conversations work normally. *** ## How to use authentication templates with BSUID customers Authentication templates — including one-tap, zero-tap, and copy-code OTP templates — **cannot be sent to BSUID-only customers**. They require a phone number. This is a permanent restriction from Meta and applies regardless of your Connectly setup. If a customer has hidden their phone number, you will not be able to send them an OTP via WhatsApp. *** ## Frequently asked questions No. Existing customers continue to work exactly as expected. Most customers will continue sharing their phone numbers, and Meta maintains phone number associations for customers who have previously interacted with your business. Only brand-new customers who have adopted a username and chosen to hide their phone number will appear as BSUID-only. Probably not immediately. If your business doesn't rely on phone numbers for CRM matching or automations, no action is needed. If it does, we recommend reviewing your workflows before opting in to phone-less webhook delivery — your Account Manager can help you assess the impact. Not yet. The inbox displays the BSUID as the customer identifier when no phone number is available. Username display may be added in a future update. Your inbox continues to receive and display messages from BSUID customers normally. The only difference is that webhook events for phone-less customers won't be delivered to your endpoint — so any CRM sync, automation triggers, or integrations that rely on webhooks won't fire for those customers. Not necessarily. A BSUID can change if the customer changes their phone number. Always use the most recent BSUID from the latest webhook event, and update your records accordingly. *** ## Timeline | Date | What's happening | | -------------- | -------------------------------------------------------------------------------- | | March 2026 | BSUIDs begin appearing in Connectly webhook payloads. | | April 2026 | Meta Contact Book launched — protects existing customer phone number visibility. | | June 2026 | WhatsApp username rollout begins in pilot countries. | | September 2026 | BSUIDs generally available globally. | | TBD | Outbound messaging to BSUID-only customers — pending Meta platform release. | *** ## Resources Shareable overview documents for your team: * 🇬🇧 [English](https://docs.google.com/document/d/1ZFB1xoLoeuvTS0CBlaEoXNV_FM7scxz840dUgcxDFFc/edit?usp=sharing) * 🇧🇷 [Portuguese](https://docs.google.com/document/d/1sAit9sjMROlM0OGIhqgsAVRWdHmCTVRVd8bY13m01YU/edit?tab=t.0) * 🇪🇸 [Spanish](https://docs.google.com/document/d/1HRS3Aj_ODjgvCnXfsfnihf_TRGkcssvBfQUbAtuOS7A/edit?usp=sharing) If you're a developer looking for technical details on webhook payloads, API fields, and integration guidance, see the [BSUID technical reference](https://docs.connectly.ai/messaging/bsuid). # Sign-Up Units & BSUIDs Source: https://docs.connectly.ai/business-hub/bsuid/signup-bsuid How the WhatsApp BSUID rollout affects sign-up unit flows that rely on phone number matching, and how to adapt them 📲 **Audience:** Account Management, Customer Success, Solutions Engineering — for customers using sign-up units with coupon or welcome message flows. ## The problem Many businesses use a **Sign-Up unit** flow that works like this today: The website Sign-Up unit collects the user's phone number. The user is redirected to WhatsApp via a `wa.me` link and sends an inbound message to the business. The bot or autoreply matches the inbound phone number against the number collected on the website and sends back the coupon code. **This breaks with the BSUID rollout.** Per Meta's BSUID documentation, the phone number is only included in the inbound webhook if the business has interacted with that specific phone number within the **last 30 days** (evaluated per business phone number). For new users — which is exactly who a Sign-Up unit targets — the inbound message will arrive with a BSUID and **no phone number**. The matching in step 3 stops working. *** ## Recommended fix: send the coupon as an API campaign The business already has the phone number — it was collected on the website in step 1. Instead of waiting for an inbound message and trying to match it, the business's backend sends the coupon directly to that phone number as a business-initiated campaign via Connectly's Campaign Send API. **New flow:** Website Sign-Up unit collects the phone number — unchanged. The business's backend calls `POST /v1/businesses/{businessId}/send/campaigns` with that phone number, triggering a pre-approved WhatsApp template that delivers the coupon code. The `wa.me` redirect can stay if desired — but the coupon no longer depends on the inbound message arriving. A side benefit: the outbound send counts as a business interaction with that phone number. Per Meta's 30-day rule, subsequent inbound messages from that user will include the phone number again — so any downstream phone-based logic continues to work. *** ## One-time setup Before the first send, the customer needs: 1. A campaign created in Connectly with an approved WhatsApp template containing the coupon message. Note its `campaignId`. 2. The campaign must be **published** — the API rejects draft campaigns. 3. A Connectly API key with `messaging.send` scope (passed as the `X-API-Key` header). *** ## Example API request ```bash theme={null} curl -X POST 'https://api.connectly.ai/v1/businesses/{businessId}/send/campaigns' \ -H 'Content-Type: application/json' \ -H 'X-API-Key: ' \ -d '{ "entries": [ { "client": "+16505551234", "campaignId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "variables": { "coupon_code": "WELCOME10" } } ] }' ``` **Key fields:** | Field | Required | Description | | ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `client` | Yes | Recipient's phone number in E.164 format — the one collected on the website. | | `campaignId` | Yes | UUID of the pre-created coupon campaign. `campaignName` also works but is deprecated — don't mix both in the same request. | | `variables` | No | Values for the template's variables (e.g. the coupon code). Only needed if the template uses variables. | | `sender` | No | Which business phone number to send from. If omitted, the business's default channel is used. Set explicitly if the business runs multiple numbers. | By default, the API prevents sending the same campaign to the same phone number twice (`if_duplicate_check_unspecified: allow_one`). This is usually the right behaviour for a sign-up coupon — a customer should only receive it once. **Example response:** ```json theme={null} { "data": [ { "campaignId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "campaignName": "signup_coupon", "campaignVersion": "v1.0", "sendoutId": "0d300213-1889-448d-b1e5-7503fe4be68f", "status": "created", "acceptedCount": 1, "rejectedCount": 0, "error": null } ] } ``` *** ## Important: pricing impact Today's flow replies inside the **24-hour customer-service window**, so the coupon goes out as a free-form service message with no Meta messaging fee. The recommended flow sends a **business-initiated template message**, which Meta bills per delivered message according to its rate card (rate depends on template category and destination country). Confirm with the customer that they accept this cost before building the integration. *** ## Alternative: request phone number sharing inside WhatsApp If the cost increase is not acceptable, there is a cheaper option for phone-less users specifically. Meta added a native `REQUEST_CONTACT_INFO` message type that lets businesses ask the user to confirm sharing their phone number inside WhatsApp. The user taps one button, the shared phone number arrives on the webhook, and the existing phone-matching logic works again — all inside the free service window. Sending this message type from the Connectly Flow Builder is **not yet available**. A dedicated node is planned. Until it ships, this option cannot be self-served through the platform. Contact your Connectly Account Manager for the latest status. # Click to WhatsApp Ads (CTWA) Setup Guide Source: https://docs.connectly.ai/business-hub/campaigns/ctwa Learn how to set up Click to WhatsApp ad campaigns using Connectly — from linking your Facebook Page to building automated conversation flows. Click to WhatsApp Ads (CTWA) convert ad traffic from Instagram and Facebook into WhatsApp conversations, letting businesses engage with customers faster and in a more personalized way. This guide walks you through the full setup process, from initial configuration to campaign activation. ## Common Use Cases CTWA works across a wide range of business objectives. In every case, it helps you grow your audience by capturing the phone numbers of previously unknown users. | Use Case | Example Prompt | | ---------------------------- | --------------------------------------------------------- | | **Lead capture** | "Want to know more? Start a chat with us!" | | **Customer support** | "Need help? Our team is just a message away." | | **Collect feedback** | "Tell us how we did in 2 quick messages." | | **Re-engage inactive users** | "We miss you! Let's chat and get you a special offer." | | **Product inquiries** | "Ask us anything about our new collection." | | **Discounts & promos** | "Click to get your limited-time coupon in chat!" | | **Book appointments** | "Book your consultation in just a message!" | | **Insurance verification** | "Unsure if your plan is accepted? Message us to confirm." | | **Course sign-ups** | "Ready to start learning? Talk to our advisor now." | *** ## Before You Begin Make sure you have the following in place before setting up your CTWA campaign: An active WhatsApp number connected to Connectly An active Meta Ads Manager account A Facebook Business Page for your brand *** ## Step 1: Connect Your Facebook Page to WhatsApp Business Click your profile picture in the top-right corner, then go to **Settings & Privacy → Settings**. In the left menu under **Permissions**, select **Linked Accounts**, then click **WhatsApp**. Select your country code, enter your WhatsApp Business phone number, and click **Send WhatsApp Code**. Enter the confirmation code you receive and click **Confirm**. If you have an existing number and want to connect a new one, select **Connect another number**. See [Meta's official guidelines](https://www.facebook.com/business/help/4631406400243963) for details. *** ## Step 2: Create the Ad Campaign in Meta Ads Manager The setup steps vary slightly depending on your campaign objective. In all cases, **WhatsApp must be selected as the message destination**. Go to Ads Manager and click **+ Create**. Select **Traffic**, **Engagement**, or **Sales** as your objective, then click **Continue**. Enter a campaign name, review the details, and click **Next**. In the **Conversion** section, select **Message destinations**. Choose the relevant Facebook Page and check the box next to **WhatsApp**. A default goal is set based on your objective. If you selected Engagement, choose your goal from the dropdown. Complete the ad setup until you reach the **Message template** section. Choose **Create new** or **Use existing**. You can use the **Start conversation** template to suggest prompts for customers. Edit and save it. Complete the rest of your ad and click **Publish** when ready. Go to Ads Manager and click **+ Create**. Select **Leads** as your objective, then click **Continue**. Enter a campaign name, review the details, and click **Next**. In the **Conversion** section, select **WhatsApp**. The performance goal defaults to **Maximize number of conversations**. Select the relevant Facebook Page and review the remaining ad set details, then click **Next**. Complete the ad setup until you reach the **Message template** section. Choose **Create new** or **Use existing**. Edit and save it. Complete the rest of your ad and click **Publish** when ready. Go to Ads Manager and click **+ Create**. Select **Awareness** as your objective, then click **Continue**. Enter a campaign name, review the details, and click **Next**. Review the ad set details and click **Next** again. Complete the ad setup until the **Destination** section. Select **Messaging apps**, then choose **WhatsApp**. In the **Message template** section, choose **Create new** or **Use existing**. You can use the **Start conversation** template to suggest prompts. Edit and save it. Complete the rest of your ad and click **Publish** when ready. See [Meta's official guidelines](https://www.facebook.com/business/help/447934475640650?id=371525583593535) for full details on creating Click to WhatsApp ads in Ads Manager. ### Auto-Populated Message Templates When a user clicks your ad, a conversation opens in WhatsApp with a pre-configured template message. This message is set up in your **Meta Ads Manager account** under the Message Template section of the ad, and it determines how the conversation starts. There are two types of customer actions you can configure: A customizable first message that is automatically pre-filled for the user when they open the chat. The user can edit it before sending. Up to 5 suggested questions the user can tap to start the conversation. No automated response is needed — you can build a customized flow for each option in Connectly's FlowBuilder. Both template types can be saved for reuse in future campaigns. See [Meta's guidelines on pre-filled messages](https://web.facebook.com/business/help/687252309996046) for more detail. *** ## Step 3: Build the Customer Journey in Connectly Create the campaign flow that triggers when users message your business through the ad. Input variables are not supported in CTWA flows because the user is unknown at the time of contact. You can use **output variables** to collect information from users during the conversation. Log in to your [Connectly account](https://app.connectly.ai/), click **Create a new campaign** in the top-left corner, and select **Inbound Flow**. Always begin the flow with an **If Customer Replies** node. * If your ad uses a **pre-filled message or FAQs**, add an **If Customer Replies** node for each option with the corresponding regex pattern. * Also add an extra **If Customer Replies** node **without** a regex pattern to capture free-text responses. * Build the appropriate response flow for each branch. You can use any message type or node (time delays, Sofia AI takeover, etc.) to create the best experience. When the flow is complete, click **Save** to approve it, then click **Next**. ### Link the Ad to the Campaign After approving the flow, you'll be taken to the **Audience** step. Choose **Click to WhatsApp** and enter the corresponding **Ad ID(s)**. You can add up to 50 Ad IDs. Check the box to automatically receive performance metrics in your Ads Manager. Click **Next**, review the campaign details, and click **Confirm**. The **Ad ID** is a unique identifier assigned to each ad in Meta Ads Manager. Follow [Meta's guidelines](https://www.facebook.com/business/help/2534657046763437) to find your Ad ID — it's visible in the Ads Manager table view. *** ## Step 4: Track Campaign and Ad Performance You can monitor performance from two places: Connectly's Analytics dashboard and Meta Ads Manager. From the Analytics dashboard, select a timeframe and campaign name, then click **Send Report** to access: * User phone numbers (generated leads) * Message delivery rates (sent, delivered, read) * Customer opt-outs * Button and link clicks * Output variable values See [Meta's reporting guide](https://www.facebook.com/business/help/318580098318734?id=369013183583436) for full instructions. Available metrics include: * Amount spent on the ad * Impressions (total times the ad was displayed) * Reach (unique users who saw the ad) * Ad clicks ### Recommended Reporting Template Combine both data sources for a complete view of your CTWA campaign performance: | Metric | Source | Formula | | ------------------------------- | ---------- | ---------------------------------------- | | Ad Spend | Meta | — | | Reach | Meta | — | | Impressions | Meta | — | | CPM (Cost per Mille) | Calculated | (Ad Spend / Impressions) × 1,000 | | Conversations Started | Meta | — | | CPR (Cost per Result) | Calculated | Ad Spend / Conversations Started | | Ad Clicks | Meta | — | | CTR (Click-Through Rate) | Calculated | (Total Clicks / Total Impressions) × 100 | | Cost per Click | Calculated | Ad Spend / Total Ad Clicks | | # Conversations (messages sent) | Connectly | — | | Cost per Conversation | Calculated | Ad Spend / # Conversations | # Inbox Analytics Source: https://docs.connectly.ai/business-hub/inbox/analytics Measure your team's support performance — ticket volume, response and resolution times, CSAT, and trends over time 📊 Inbox analytics shows how your support operation is performing — how many tickets you handle, how fast you respond and resolve, and how those numbers trend over time. Find it under **Analytics → Inbox**. The Inbox tab is a single page made of stacked cards. This overview covers how to read the dashboard and its headline metrics; the tables and live views have their own articles: Per-ticket data plus team and agent performance tables. Real-time open tickets, queued rooms, and auto-assignment performance. ## How to read the dashboard * **Date range** — pick **Last 7 days**, **Last 1 month**, **Last 3 months**, or a custom range. Every metric compares the selected period against the previous equivalent period, so each card shows a change (up/down) versus before. * **Store** — if your business has store locations, filter the dashboard to a specific store (or **All Stores**). * **Download CSV** — most cards have their own **Download CSV** button, and the export usually contains more columns than what's shown on screen. * **Drill down** — where you see an eye icon, click it to open that conversation in the Inbox. What you see depends on your role. **Agents** see only their own **Agent Performance** card (covered in [Tickets, teams & agents](/inbox/analytics-performance)). **Owners** and analytics roles see the full dashboard. Some cards depend on your plan and configuration and may not appear. ## Business Performance The top card is a grid of business-wide KPIs. Each shows its current value and the change versus the previous period. | Metric | What it measures | | ---------------------- | ---------------------------------------------------------------------------------- | | **Tickets** | Total number of tickets created in the period. | | **Closed Rate** | Share of tickets resolved and closed in the period (Closed ÷ Total). | | **Avg Resolve Time** | Average time to resolve a ticket, from creation. | | **Avg Response Time** | Average time to respond to follow-up messages across tickets. | | **Avg 1st Response** | Average time to the first response on a ticket. | | **Avg Time to Close** | Average time to fully close a ticket, from creation. | | **Reopen Rate** | Share of tickets the customer messaged again after "resolved" but before "closed". | | **Auto-resolved** | Number of tickets auto-resolved (by inactivity) rather than by an agent. | | **Auto-closed** | Number of tickets auto-closed rather than by an agent. | | **Avg CSAT Score** | Average post-ticket satisfaction score. | | **CSAT Response Rate** | Share of tickets where the customer answered the CSAT survey. | The **Avg CSAT Score** and **CSAT Response Rate** cards appear when the [Customer Satisfaction Survey](/inbox/customer-satisfaction-survey) is enabled and survey data exists. Business Performance KPI cards showing tickets, closed rate, response and resolution times, reopen rate, auto-resolved, auto-closed, and CSAT ## Timegraph The **Timegraph** plots ticket activity over the selected date range so you can spot trends. It charts four series: * **Created Tickets** * **Resolved Tickets** * **Closed Tickets** * **Reopened Tickets** You can narrow the chart by **topic** — selecting one adds a line for closed tickets of that topic. The chart also has its own **Download CSV**. Time series chart tracking created, resolved, closed, and reopened tickets over time, with the All Tickets table below # Analytics: live monitoring & auto-assignment Source: https://docs.connectly.ai/business-hub/inbox/analytics-live See open tickets and queued conversations in real time, and measure how auto-assignment is performing 📊 The bottom of the Inbox analytics page (**Analytics → Inbox**) has real-time views for monitoring your team right now, plus a breakdown of how automatic ticket assignment is performing. These are available to owners and analytics roles. ## Current Open Tickets A **live** snapshot of open tickets by agent — it's not tied to the date range. It shows each agent and how many open tickets they're holding, so you can spot who's overloaded at a glance. * Click an agent to see that agent's open tickets, and open any conversation from there. * If your business uses teams, you can **Group by Team** to see open tickets organized by team (with buckets for conversations with no team or a missing assignment). * Search agents and **Download CSV** are available. Current Open Tickets live view listing each agent and their open ticket count, with a Store filter ## Currently Queued Rooms A **live** table of conversations waiting to be automatically assigned to an agent. It refreshes on its own and shows: | Column | What it is | | ------------------- | ---------------------------------------- | | **Customer** | The customer waiting in the queue. | | **Phone** | The customer's phone number. | | **Handover Time** | When the conversation entered the queue. | | **Est. Assignment** | Estimated time until it's assigned. | Rooms wait here for the next auto-assignment cycle. Click the eye icon to open a conversation. This view is relevant when [Ticket Routing](/inbox/ticket-routing) is set to automatically assign conversations. Currently Queued Rooms live view describing rooms waiting for the next auto-assignment cycle ## Auto-Assignment Performance This section measures how well automatic assignment is working over the selected period. (It's hidden when there were no handovers in the period.) **KPI cards:** | Metric | What it measures | | ----------------------- | --------------------------------------------------------- | | **Total Handoffs** | Conversations handed off to be assigned. | | **Instant Assignments** | Handoffs assigned immediately to an available agent. | | **Queued Assignments** | Handoffs that had to wait in the queue before assignment. | | **Timed Out** | Handoffs that waited too long without being assigned. | | **Avg Queue Time** | Average time spent waiting in the queue. | | **Total Assignments** | Total assignments made. | Auto-Assignment Performance KPI cards and the Daily Trend chart of handoffs, instant, queued, and timed-out assignments **Breakdowns below the KPIs:** * **Daily Trend** — a chart of handoffs, instant, queued, and timed-out assignments per day. * **By Team** — Team, Assignments, To Agent, Team Only, Avg Queue. * **By Agent** — Agent, Assignments, Avg Queue. * **Autoassigned Ticket Details** — a per-ticket table (customer, handoff, outcome, team, agent, queue time, assigned-at) with an eye icon to open the conversation. Auto-assignment breakdowns: By Team, By Agent, and Autoassigned Ticket Details tables Each part has its own **Download CSV**. # Analytics: tickets, teams & agents Source: https://docs.connectly.ai/business-hub/inbox/analytics-performance Dig into per-ticket data and compare performance across teams and agents 📊 Below the headline KPIs, the Inbox analytics page (**Analytics → Inbox**) breaks performance down to the ticket, team, and agent level. These tables are available to owners and analytics roles; agents see their own personal card at the end. Every table has its own **Download CSV**, and the export includes more columns than the on-screen view — useful for deeper analysis in a spreadsheet. ## All Tickets A row-per-ticket table for the selected period. On screen it shows: | Column | What it is | | -------------------- | ------------------------------------------------------------------- | | **Ticket Number** | The ticket's number. | | **Customer Name** | The customer on the ticket. | | **Created Date** | When the ticket was created. | | **Status** | Open, Resolved, or Closed. | | **Current Assignee** | The team and/or agent it's assigned to. | | **CSAT** | The customer's satisfaction score and label (when CSAT is enabled). | Click the **eye icon** on a row to open that conversation in the Inbox. The **CSV export is much richer** — it adds fields like the ticket summary, customer contact and channel, final topic, resolution reason, first-response and resolution timestamps, time-to-first-response, average response time, initial/final assignees, number of reassignments, reopen count, and CSAT — roughly two dozen columns in total. ## Teams Performance One row per team, comparing how teams are doing. | Column | What it measures | | ------------------------------- | -------------------------------------------------- | | **Name** | The team (deleted teams appear as "Deleted Team"). | | **Tickets Handled** | Unique tickets the team worked on. | | **Tickets Resolved** | Unique tickets the team resolved. | | **Average First Response Time** | Average time to the first response. | | **Average Resolution Time** | Average time to resolve. | | **Avg CSAT** | Average satisfaction score (when CSAT is enabled). | The CSV adds reassignment counts (to others / to me) and average response time. Teams performance table with tickets handled, tickets resolved, average first response time, average resolution time, and average CSAT ## Agents Performance One row per agent, with the same shape as the teams table. | Column | What it measures | | ------------------------------- | -------------------------------------------------- | | **Name** | The agent. | | **Tickets Handled** | Unique tickets the agent worked on. | | **Tickets Resolved** | Unique tickets the agent resolved. | | **Average First Response Time** | Average time to the first response. | | **Average Resolution Time** | Average time to resolve. | | **Avg CSAT** | Average satisfaction score (when CSAT is enabled). | The CSV adds reassignment counts and average response time. Agents Performance table with total tickets handled, tickets resolved, average first response time, average resolution time, and average CSAT per agent ## Agent Performance (your own view) When an individual agent opens Inbox analytics, they see a single personal card with their own numbers for the selected period: | Metric | What it measures | | -------------------- | --------------------------------------------------------- | | **Reassignments** | Times one of your tickets was reassigned to someone else. | | **Resolutions** | Tickets you marked as resolved. | | **Tickets Closed** | Tickets you closed. | | **Tickets Assigned** | Tickets assigned to you. | | **Close Rate** | Share of your assigned tickets that you closed. | # Customer Panel Source: https://docs.connectly.ai/business-hub/inbox/customer-panel View and edit a customer's profile, add notes, and review their ticket history — all beside the conversation 👤 The customer panel is the right-hand panel of a conversation. It shows who you're talking to and lets you keep their details and notes in one place. Toggle it with **Show Profile** / **Hide Profile** in the conversation's action bar. The panel is organized into sections: Customer panel showing Basic Profile, Preferences, Tickets History, and Notes sections ## Basic Profile The customer's core details, which you can edit and save: * **First Name** * **Last Name** * **Customer ID** — the customer's phone number (or `@username` for WhatsApp contacts who hide their phone). * **Email** Edit any field and save your changes. If you enter a value starting with `+` in Customer ID, it's validated as a phone number. ## Notes **Notes** is a shared free-text field for anything your team should know about this customer. Type your note and save it. Notes are a single shared field on the customer's profile — they're visible to everyone on your team who opens this customer, not private to you. There's no separate list of individual notes; you're editing one shared note. ## Tickets History A read-only list of the customer's past tickets. Each entry shows the ticket number, date, status, topic, who it was assigned to, and its [AI summary](/inbox/managing-tickets). Click an entry to jump to where that ticket started in the conversation. ## Shopify Profile If the customer is connected to a Shopify channel, this section shows their Shopify details — **Name**, **Email**, **ID**, **Last Order**, **Total Spent**, and **Total Orders**. You can also use **Update Shopify Profile** to search Shopify (by ID, last order number, or email) and link the right customer record. ## Preferences This section is reserved for future use — it currently shows *No preferences for now*. Applying **tags**, **assigning an agent**, and toggling **automations** are done from the conversation's action bar, not this panel. See [Working a conversation](/inbox/working-a-conversation) and [Managing tickets](/inbox/managing-tickets). For example, tags are applied from a popover in the action bar, where you can add, create, and manage them: Tags popover in the conversation action bar with an applied tag, Add Tag, and Manage Tags # Customer Satisfaction Survey Source: https://docs.connectly.ai/business-hub/inbox/customer-satisfaction-survey Send a satisfaction survey to customers after a ticket is closed, so you can measure how your support is doing ⚙️ The **Customer Satisfaction Survey** lets you automatically ask customers how their support experience went, right after their ticket wraps up. You configure it in **Settings → Inbox**. Use the toggle to turn it on or off. > Send a satisfaction survey when a ticket is closed after a real conversation between an agent and a customer Customer Satisfaction Survey editor with the survey message, rating options, and a WhatsApp preview ## Why it matters A satisfaction survey is the simplest way to hear directly from your customers about the quality of the support they received. Because the survey is sent **when a ticket is closed after a real conversation between an agent and a customer**, the feedback you collect reflects genuine, human-handled interactions — not conversations that closed without anyone stepping in. Turning this on gives your team a steady signal of how support is landing with customers, which you can use to spot problems early, recognize strong performance, and track whether changes to your process are actually improving the experience. ## What you can customize When the survey is on, you can tailor exactly what the customer receives, with a live **WhatsApp preview** shown alongside your changes. **Survey message** — the message sent to the customer when their ticket closes. The default is: > Hi! Your conversation with our team has been closed. We'd love to hear how we did. > > How would you rate the support you received? **Rating options** — the quick-reply choices the customer taps to rate you. Each option has an emoji and a label; the defaults are: | Emoji | Label | | ----- | ------- | | 🤩 | Amazing | | 😊 | Great | | 🙂 | Good | | 😐 | Fair | | 😞 | Poor | You can change each option's emoji and label, add options, or remove them. How they appear to the customer depends on how many you have: **2–3 options show as buttons, and 4–5 show as a list.** Click **Save changes** when you're done. ## How it fits with the ticket lifecycle The survey is tied to the moment a ticket **closes**, so it works together with the ticket lifecycle settings in [Ticket management & automation](/inbox/ticket-management): * A ticket reaches the *closed* state either when an agent closes it, or automatically via **Ticket Closure Time** after it's been marked resolved. * Two extra conditions must be met before a survey goes out: the ticket must have been closed **after a real conversation between an agent and a customer**. Tickets that close without genuine agent–customer interaction don't trigger a survey. In other words, whatever closes the ticket, the survey only fires for conversations a person actually handled. # Finding Conversations Source: https://docs.connectly.ai/business-hub/inbox/finding-conversations Filter, search, and switch views to find the right conversations in your inbox — by campaign, status, ticket number, tags, and more 🔍 The conversation list has three ways to narrow down what you see: **views**, **filters**, and **search**. You can combine them to zero in on exactly the conversations you need. ## Views Views are the top-level slices of your inbox. Which ones you see depends on your role. | View | Shows | | ------------------ | -------------------------------------------------------- | | **All** | Every conversation in the business. | | **My teams** | Conversations belonging to teams you're a member of. | | **My tasks** | Tickets assigned to you. | | **Requires agent** | Conversations waiting for a human agent to pick them up. | | **Sofia AI** | Conversations currently handled by Sofia AI. | ## Filters Filters narrow the current view. You can select several values in one filter (they combine as "any of"), and stack different filters together (they narrow the results). | Filter | What it does | | -------------------- | ----------------------------------------------------------------------------------- | | **Status** | Show tickets that are **Open**, **Resolved**, or **Closed**. | | **Ticket Number** | Enter one or more ticket numbers (for example, `123, 456, 789`). | | **Teams** | Show conversations assigned to specific teams. | | **Campaigns** | Show conversations tied to a specific campaign — useful for following up on a send. | | **Channel** | **WhatsApp**, **Messenger**, or **Instagram**. | | **Tags** | Filter by conversation tags. | | **Topics** | Filter by ticket topics. | | **Show Unread Only** | Show only conversations with unread messages. | The **Campaigns** filter is the quickest way to see who you're talking to from a specific campaign — select the campaign and the list narrows to those conversations. Inbox filter panel with ticket status, ticket number, teams, campaigns, shortcuts, and channel filters ## Search Type in the search bar to look up conversations. Search matches: * **Customer name** * **Phone number or handle** * **Campaign name** — to surface the recipients of a specific campaign, including people who were sent a message but haven't replied yet. Search doesn't look inside message content, and it doesn't search by ticket number. To find a conversation by its ticket number, use the **Ticket Number** filter above. ## Sorting The conversation list is always sorted by **most recent activity first**, so the conversations that need attention rise to the top automatically. There's no manual sort option. # Managing Tickets Source: https://docs.connectly.ai/business-hub/inbox/managing-tickets Open, assign, resolve, and reopen tickets, manage automations, and read the AI summary when a ticket closes 🎫 A ticket tracks a customer's request from the moment a human gets involved until it's wrapped up. You manage tickets from the action bar at the top of a conversation. ## Ticket states | State | Meaning | | ------------ | -------------------------------------------------------------------------- | | **Open** | The ticket is active and being worked on. | | **Resolved** | An agent marked it as solved — no longer active, but not yet fully closed. | | **Closed** | The ticket is fully wrapped up. | How long a resolved ticket waits before it closes, and whether tickets auto-resolve after inactivity, are configured in [Ticket management & automation](/inbox/ticket-management). ## Open a ticket A ticket opens when you **assign it to yourself** or **send a reply**. If your business requires a ticket topic, you'll be asked to pick one when the ticket opens. ## Assign and reassign * **Assign to me** — take the ticket yourself. If you belong to teams, you may be asked to route it to one of them. * **Assign to someone else** — assign the ticket to another agent or a team. * **Reassign** — hand an open ticket to a different agent or team. Assignment dropdown with Teams and Agents tabs for assigning or reassigning a ticket How tickets are automatically created and routed to agents or teams is covered in [Ticket Routing](/inbox/ticket-routing). ## Resolve a ticket Click **Mark as resolved** when the customer's issue is handled. Depending on your business settings, you may be asked to add a **resolution reason** — this can be optional or required. (Set which one in [Ticket management & automation](/inbox/ticket-management).) ## Reopen a ticket A resolved ticket can be reopened with **Reopen** if the customer comes back or the issue isn't fully solved. A ticket also reopens automatically if you reply to a resolved conversation. There's no manual "close" button — tickets move from resolved to closed automatically based on your closure settings. Closing a ticket is what generates its AI summary (below). ## Ticket topics Assign a **topic** to categorize what the ticket is about. Topics can be optional or required — manage the available topics in [Topics & tags](/inbox/topics-and-tags). Ticket Topic dropdown listing the available topics with their colors ## Pause and resume automations The action bar has an **Automations** toggle for the conversation: * **Automations active** — automated messages and Sofia AI can run in this conversation. * **Automations paused** — automations are held while a human handles the conversation. The toggle is disabled while a ticket is open; the tooltip tells you what to do to re-enable it (for example, *Resolve ticket to enable automations*). When Sofia AI is involved, its status (active or paused) is shown alongside the toggle. When paused automations resume is controlled by your [Automation resume timing](/inbox/ticket-management) setting. ## The AI summary When a ticket **closes**, Connectly generates an **AI summary** of the conversation and shows it inline in the chat thread. You'll also find each ticket's summary in the customer panel under [ticket history](/inbox/customer-panel). The summary condenses the conversation into clear sections — such as the customer's initial problem, why it was escalated to a human, the actions the agent took, how it was resolved, and notes for future reference — so anyone reviewing the ticket later gets the full story at a glance. AI summary of a ticket in the ticket history, broken into initial problem, reason for transfer, agent actions, resolution, and notes # Inbox Overview Source: https://docs.connectly.ai/business-hub/inbox/overview Manage every customer conversation in one place — messages from customers, agents, campaigns, Sofia AI, and automations, with full context 💬 The Inbox is where you manage all your customer conversations in one place. It centralizes every message related to a customer — from the customer, from agents, from campaigns, from Sofia AI, and from auto-replies and automations — so your team always has full context without switching tools. ## Tour of the interface The Inbox is split into three zones: the conversation list on the left, the active conversation in the center, and the customer panel on the right. Browse and find conversations. * Search by customer name or phone number. * Unread messages appear in **bold**. * Each row shows a preview of the last message. * See the ticket number, status, and topic for every conversation. Read the full history and reply. * View the complete chat history, including delivery errors. * Send messages, templates, files, emojis, or audio. * Open a ticket while you reply, without leaving the conversation. See who you're talking to and keep their details in one place. * View and edit the customer's profile. * Add notes about the customer. * Review the customer's ticket history. * See connected details like their Shopify profile. Actions like managing tags, assigning the ticket, and toggling automations live in the conversation's top action bar — see [Managing tickets](/inbox/managing-tickets). ## What you can do in the Inbox Beyond reading and replying, the Inbox gives your team the tools to find the right conversations, handle tickets end to end, and keep every customer's context in one place. Here's where each task lives: Filter by campaign, status, tags, topics, channel, and ticket number — plus search and saved views. Reply with text, templates, quick replies, files, audio, emoji, and reactions. Open, assign, resolve, reopen, set topics, pause automations, and read the AI summary. View and edit the customer's profile, add notes, and see ticket history. Looking to change how the inbox behaves for your whole team — routing, ticket automation, surveys, topics, or tags? Those live in [Inbox Settings](/inbox/settings). # Inbox Settings Source: https://docs.connectly.ai/business-hub/inbox/settings Configure how your team's inbox works — send behavior, ticket routing and automation, satisfaction surveys, topics, and tags ⚙️ Inbox Settings is where you configure how your team works inside the Inbox. To open it, go to **Settings** and select **Inbox** from the settings menu. This page is a quick reference for **what each setting does and how to configure it**. For a deeper explanation of *how a feature works*, follow the **Learn more** link on each one. ## All inbox settings | Setting | What you configure | Details | | -------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------- | | **Send Behavior** | Whether pressing Enter sends a message | *(below)* | | **Ticket Routing** | Automatically create and assign tickets when conversations need a human | [Learn more](/inbox/ticket-routing) | | **Ticket Closure Time** | When tickets auto-close after being resolved | [Learn more](/inbox/ticket-management) | | **Auto-resolve idle tickets** | Auto-resolve open tickets after inactivity | [Learn more](/inbox/ticket-management) | | **Resolution reason** | Whether agents add a reason when resolving | [Learn more](/inbox/ticket-management) | | **Automation resume timing** | When paused automations resume | [Learn more](/inbox/ticket-management) | | **Customer Satisfaction Survey** | Send a survey when a ticket is closed | [Learn more](/inbox/customer-satisfaction-survey) | | **Ticket Topics** | The topics for your chatroom | [Learn more](/inbox/topics-and-tags) | | **Conversation Tags** | The tags for your conversations | [Learn more](/inbox/topics-and-tags) | ## How to configure each one ### Send Behavior A single toggle — **Send message using Enter key**. Turn it **on** so pressing Enter sends your message, or **off** to send by clicking the send button instead. This is a per-person preference. Send Behavior setting with the 'Send message using Enter key' toggle ### Ticket Routing Toggle Ticket Routing on to automatically create and assign tickets when a conversation needs a human. Once it's on, you can choose the routing scope, the routing algorithm, a per-agent ticket cap, a queue timeout, and what happens when a solved conversation reopens. Scopes, algorithms, caps, queue timeout, and reopen behavior — explained in full. ### Ticket lifecycle: closure, auto-resolve, resolution reason & automation resume These four settings control how tickets move from open → resolved → closed: * **Ticket Closure Time** — toggle on to override the default (tickets close **72 hours** after being resolved). * **Auto-resolve idle tickets** — off by default; toggle on to auto-resolve after a period of inactivity (defaults to **720 hours** / 30 days). * **Resolution reason** — use the dropdown to make a reason **optional** or required. * **Automation resume timing** — use the dropdown to resume automations **on resolve** or **on close**. The ticket lifecycle and how these four settings fit together. ### Customer Satisfaction Survey Toggle it on to automatically send a satisfaction survey when a ticket is closed after a real conversation between an agent and a customer. What the survey measures and when it's sent. ### Ticket Topics & Conversation Tags Open each section to create and manage your **Ticket Topics** (categories for tickets) and **Conversation Tags** (labels for conversations). What topics and tags are for, and why they matter. # Ticket Management & Automation Source: https://docs.connectly.ai/business-hub/inbox/ticket-management How tickets move through your inbox — auto-resolve, closure time, resolution reason, and automation resume ⚙️ Several inbox settings work together to move tickets through your inbox automatically — resolving them, closing them, and controlling what happens along the way. This article explains how they work; you configure them in **Settings → Inbox**. ## How a ticket moves: open → resolved → closed Most of these settings act on one part of a ticket's life, so it helps to keep the three states in mind: The ticket is active and being worked on by an agent. An agent has marked the ticket as solved — it's no longer active, but not yet fully closed. The ticket is fully wrapped up. Two settings automate these transitions: **Auto-resolve idle tickets** moves a ticket from *open → resolved*, and **Ticket Closure Time** moves it from *resolved → closed*. Together they let a ticket travel all the way from open to closed without manual steps. **Resolution reason** and **Automation resume timing** act at the moment a ticket is resolved or closed. Ticket creation and assignment happen through [Ticket Routing](/inbox/ticket-routing) — the entry point that turns a conversation needing a human into an assigned ticket. *** ## Auto-resolve idle tickets **Auto-resolve idle tickets** controls the *open → resolved* transition: it moves open tickets to resolved after a period of inactivity, so conversations don't stay open indefinitely when there's nothing left to do. > Automatically transition open tickets to resolved after a period of inactivity. Off by default — opt in per business. This setting is **off by default**. When you turn it on, the inactivity window is set to **720 hours** (30 days). Adjust it in the **Hours**, **Minutes**, and **Seconds** fields and click **Save**. Auto-resolve idle tickets setting with hours, minutes, and seconds fields *** ## Ticket Closure Time **Ticket Closure Time** controls the *resolved → closed* transition: how long after a ticket is marked as resolved it's automatically closed. > Configure when tickets should be automatically closed after being marked as resolved By default, tickets close **72 hours** after being marked as resolved. Use the toggle to set your own closure time instead — enter it in the **Hours**, **Minutes**, and **Seconds** fields and click **Save**. Ticket Closure Time setting showing 72 hours by default *** ## Resolution reason **Resolution reason** controls whether agents record why a ticket was solved at the moment they mark it as resolved. Capturing a reason helps you understand what your team is actually resolving — and it applies whether a ticket is resolved manually by an agent or automatically by auto-resolve. > Choose whether your agents will need to add a resolution reason the moment they mark a ticket as resolved Use the dropdown to make a resolution reason **optional** or required. Resolution reason dropdown with Required and Optional options *** ## Automation resume timing When a human agent steps into a conversation, running automations are paused. **Automation resume timing** controls when those paused automations start up again — and it hinges on the ticket lifecycle above: whether automations wake up at the *resolved* state or wait for the *closed* state. > Choose when paused automations resume after an agent gets involved. "On resolve" continues messaging immediately when the ticket is marked solved. "On close" waits until the ticket is fully closed. Use the dropdown to choose between the two options: | Option | When automations resume | | --------------------- | --------------------------------------------- | | **Resume on resolve** | Immediately when the ticket is marked solved. | | **Resume on close** | Only once the ticket is fully closed. | Automation resume timing dropdown with Resume on resolve and Resume on close options # Ticket Routing Source: https://docs.connectly.ai/business-hub/inbox/ticket-routing Automatically create and assign tickets when conversations need a human — and control how agents are chosen, capped, queued, and reassigned ⚙️ **Ticket Routing** decides whether Connectly automatically creates a ticket when a conversation needs a human, and controls exactly how that ticket is assigned. You configure it in **Settings → Inbox**. Use the toggle to turn it on or off. > Automatically create tickets when conversations need human intervention > If this setting is enabled, when a conversation is identified as needing a human handover, Connectly will check for an available agent and automatically create a ticket if possible. Ticket Routing enabled, showing routing algorithm, max tickets per agent, queue timeout, and reopen assignment behavior ## How it connects to agent handover Ticket Routing is the bridge between a conversation that needs a person and an agent actually picking it up. When a conversation is identified as **needing a human handover**, Connectly looks for an available agent and — if it finds one — creates a ticket and assigns it automatically. ## Where conversations are routed Ticket Routing directs each conversation using one of two scopes: | Scope | Where the conversation goes | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Agent Routing** | To any available agent in the business. | | **Team Routing** | To a **team** based on your handover setup, then to an agent within that team. This is also how conversations reach a **store** — a store is a team tied to a physical location. | With **Team Routing**, you decide which team (or store) handles each type of handover in your agent configuration, and you can route customers to their nearest store location. That setup lives in its own article: Organize agents into teams, connect teams to store locations, and route conversations to the right group. Routing scopes, teams, and store routing are **advanced features** that may need to be enabled for your business. If you only see agent-based routing, contact your Connectly Account Manager. ## Routing Algorithm Choose how Connectly picks which agent gets the next ticket. | Option | What it does | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Round Robin** | Cycle through agents in order, queue when all agents are at ticket capacity. | | **Balanced Workload** | Assign to agent with least open tickets, round-robin for tie-breaking, queue when all agents are at ticket capacity. | ## Max tickets per agent Control how many open tickets a single agent can hold at once. | Option | What it does | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Set Limit** | When an agent reaches this limit, additional tickets cannot be assigned to them. *We recommend setting this to 10 or less.* Enter the number and click **Save** (default is **5**). | | **Unlimited** | Agents can receive unlimited tickets. No cap will be applied. | When every eligible agent is at capacity, new tickets wait in the queue until someone frees up. ## Queue timeout > Rooms waiting in the queue will be removed from the "Requires Agent" tab after this many days if not assigned. Enter the number of days and click **Save**. The default is **15 days**. ## Reopen assignment behavior > Choose what happens to a solved conversation when the customer replies and it reopens. | Option | What it does | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Keep the last agent** | Reassign the reopened conversation to the last agent who handled it, for continuity. | | **Route to the queue** | Send the reopened conversation back to the routing queue so it is routed normally. Useful for e-commerce or shift-based teams where the last agent may be off shift. | # Ticket Topics & Conversation Tags Source: https://docs.connectly.ai/business-hub/inbox/topics-and-tags Manage the topics for your chatroom and the tags for your conversations, so your team stays organized ⚙️ Topics and tags are how your team keeps the inbox organized. You manage both in **Settings → Inbox**, each in its own section. ## Ticket Topics > Manage topics for your chatroom **Ticket Topics** are the categories your team uses to describe what a ticket is about. Ticket Topics section with the Topic Requirement toggle and a list of topics with colors ### Managing topics * **Topic Requirement** — turn this on to require agents to set a ticket topic when responding to a conversation. * **Add a topic** with **+ Add**, then give it a **name** and a **color**. * Edit a topic's name or color inline, or remove it with the trash icon. ### Why they matter Topics turn a pile of individual tickets into something you can make sense of. When every ticket is categorized, you can see what your customers are actually contacting you about — which questions come up most, which issues are growing, and where your team is spending its time. That makes it easier to prioritize, staff, and improve the parts of your product or service that generate the most tickets. ## Conversation Tags > Manage tags for conversations **Conversation Tags** are labels your team applies to conversations to organize and find them. Conversation Tags section with a list of tags and their colors ### Managing tags * **Add a tag** with **+ Add**, then give it a **name** and a **color**. * Edit a tag's name or color inline, or remove it with the trash icon. * Unlike ticket topics, tags have **no requirement toggle** — applying them is always optional. ### Why they matter Tags give you a flexible way to group conversations that belong together, even when they don't fit a single ticket category. Your team can use them to mark conversations for follow-up, flag noteworthy cases, or filter the inbox down to exactly the conversations they care about — keeping a high-volume inbox navigable instead of overwhelming. # User Management Source: https://docs.connectly.ai/business-hub/inbox/user-management Add users, assign roles, and control who can access what in your workspace 👥 User Management is where workspace owners add and remove team members and control what each person can do. ## Open User Management Click your profile icon at the bottom-left of the sidebar, then select **Settings**. Profile menu showing Switch Business, Settings, and Logout options In the Settings sidebar, click **Users**. Settings sidebar with Users highlighted The Users page shows your team members in a sortable table with their **name**, **email**, and **role**. You can sort by Name or Role using the column headers. Users page showing 84 Users, the Add User button, and a user row with name, email, and role Only **Owners** can add users, change roles, or remove users. Everyone else sees the user list in read-only mode. ## Roles Every user has exactly one role. The role determines which parts of Connectly they can access. | Role | What they can do | | ------------- | -------------------------------------------------------------------------------------------------------- | | **Owner** | Full access — including API keys, user management, and every feature in the sidebar. | | **Marketing** | Full access — **except** API keys and user management. | | **Agent** | Inbox, Teams, and personal Settings only. Agents see only conversations assigned to them or their teams. | | **Analytics** | Analytics dashboards and reports only. No access to Inbox, Campaigns, or Settings. | ### Feature access by role | Feature | Owner | Marketing | Agent | Analytics | | ----------------- | :---: | :-------: | :---: | :-------: | | Inbox | ✅ | ✅ | ✅ | — | | Campaigns | ✅ | ✅ | — | — | | Automations | ✅ | ✅ | — | — | | Audiences | ✅ | ✅ | — | — | | Analytics | ✅ | ✅ | — | ✅ | | AI Agents / Sofia | ✅ | ✅ | — | — | | Tools | ✅ | ✅ | — | — | | Templates | ✅ | ✅ | — | — | | Sign-up Units | ✅ | ✅ | — | — | | Teams | ✅ | ✅ | ✅ | — | | Settings | ✅ | ✅ | ✅ | — | | User Management | ✅ | — | — | — | | API Keys | ✅ | — | — | — | Roles control workspace-level access. Team membership controls **which conversations** an agent sees inside the Inbox. See [Teams](/teams-and-stores/teams) for details. ### What each role can do in detail Owners have unrestricted access to every feature. On top of what Marketing users can do, Owners are the only role that can: * **Manage users** — add, remove, and change roles from Settings → Users. * **Access API keys** — view and manage secrets in Settings → Secrets. * **Configure notifications** — set up notification preferences in Settings → Notification Settings. Marketing users see every section in the sidebar and can manage day-to-day operations: * **Inbox** — view all conversations, assign and reassign tickets, reply, resolve, and reopen. * **Campaigns** — create, schedule, and send broadcast campaigns. * **Automations & Flows** — build and manage automation workflows. * **Audiences** — create and manage customer segments. * **Analytics** — view all dashboards and reports. Cannot edit report configurations. * **AI Agents / Sofia** — configure AI agents and Sofia AI. * **Templates** — create and manage message templates. * **Tools & Sign-up Units** — manage external tools and registration forms. * **Teams** — create teams, add members, and manage folders. * **Settings** — access General, Integrations, Quick Replies, Campaigns, Users (read-only), Inbox, Teams, and widget settings. **What Marketing cannot do:** * Add, remove, or change user roles (read-only on the Users page). * Access API keys (Secrets tab is hidden). * Edit analytics report configurations. Agents are designed for frontline work. Their sidebar shows only **Inbox**, **Teams**, and **Settings**. **Inbox access:** * Agents see only conversations **assigned to them or their teams** — not the full conversation list. * The **All** and **Sofia AI** tabs are hidden. Agents work from **My Tasks** and **Requires Agent**. * Agents can reply, resolve, and reopen their assigned tickets. **Settings access:** * **General** — edit their own profile and change their password. * **Quick Replies** — view and use quick replies. * **Integrations**, **Campaigns**, **Inbox**, **Teams**, **Whatsapp Widget** — visible but may be read-only depending on the setting. * **Users** — visible in read-only mode (no add/remove/change role). * **Secrets** and **Notification Settings** — hidden. **What Agents cannot do:** * See conversations outside their assignment scope. * Create or manage campaigns, automations, audiences, or templates. * Access analytics dashboards (except their own performance if the page is reachable). * Manage users or access API keys. The Analytics role is purpose-built for users who need data without operational access. * **Analytics** — full access to all dashboards, metrics, and reports. Can edit and customize report configurations. * **No sidebar sections** other than Analytics are visible — no Inbox, Campaigns, Automations, Settings, or Teams. **What Analytics cannot do:** * Access the Inbox or interact with any conversation. * Create or send campaigns. * Manage users, teams, or any workspace setting. ### Settings pages by role Not all Settings pages are visible to every role: | Settings page | Owner | Marketing | Agent | | --------------------- | :----: | :---------: | :---------: | | General | ✅ | ✅ | ✅ | | Integrations | ✅ | ✅ | ✅ | | Quick Replies | ✅ | ✅ | ✅ | | Campaigns | ✅ | ✅ | ✅ | | Users | ✅ edit | ✅ read-only | ✅ read-only | | Inbox | ✅ | ✅ | ✅ | | Teams | ✅ | ✅ | ✅ | | WhatsApp Widget | ✅ | ✅ | ✅ | | Webchat Widget | ✅ | ✅ | ✅ | | Secrets (API Keys) | ✅ | — | — | | Notification Settings | ✅ | — | — | The **Analytics** role does not have access to Settings at all. ## Add a user 1. Click **Add User** at the top-right of the Users page. 2. Fill in the form: * **Email** — the new user's email address. * **Password** — a temporary password for the user's first login. They can change it later in **Settings → General**. * **Role** — choose one of the four roles. Defaults to **Agent**. Add User modal with Email, Password, and Role fields Open the **Role** dropdown to see all available options. Add User modal with the Role dropdown open, showing Agent, Marketing, Analytics, and Owner 3. Click **Submit**. The new user appears in the list immediately and can log in with the email and temporary password you set. ## Change a user's role 1. Hover over the user's row to reveal the **⋮** menu on the right. User row showing the three-dot actions menu on hover 2. Click the **⋮** menu. Select the new role from the list — each option shows a short description and a checkmark next to the current role. Role selection menu showing Owner, Marketing, Agent, and Analytics with descriptions and a Remove User option The change takes effect immediately — no confirmation step. ## Remove a user 1. Hover over the user's row and click the **⋮** menu. 2. Click **Remove User** at the bottom of the menu. 3. A confirmation panel appears with the user's name. Click the red **Remove** button to confirm. Remove User confirmation dialog asking 'Remove User: Test Agent?' with a warning message and a red Remove button Removing a user revokes their access instantly. This action cannot be undone from the UI — you would need to add them again as a new user. # Working Conversations Source: https://docs.connectly.ai/business-hub/inbox/working-a-conversation Reply to customers with text, templates, quick replies, files, audio, emoji, and reactions — right from the conversation 💬 Once you open a conversation, the composer at the bottom is where you reply to the customer. It supports much more than plain text. ## Ways to reply | Option | What it's for | | ----------------- | ----------------------------------------------------------------------------------------------------------- | | **Text** | Type a reply in the composer (it's pre-labeled *Reply to* ). | | **Templates** | Send an approved WhatsApp message template — available on WhatsApp and Blip channels. | | **Quick replies** | Insert one of your team's saved snippets. You can add, edit, and delete quick replies inline. | | **Files** | Attach an image, document, or other file (one attachment per message). You can also drag-and-drop or paste. | | **Audio** | Record and send a voice note. | | **Emoji** | Insert emoji into your reply (available on desktop). | The **Templates** button opens your message library, with **Quick Replies** and **Template Messages** tabs: Quick Replies tab in the message library with saved reply snippets Template Messages tab in the message library listing approved WhatsApp templates Add emoji from the emoji picker: Emoji picker with frequently used emoji and categories ## React and reply to a specific message * **Reactions** — add an emoji reaction to a specific message in the thread. * **Reply / quote** — reply to a specific earlier message so the customer sees the context you're responding to. ## Send behavior Choose whether pressing **Enter** sends your message or adds a new line. This is a personal preference — see [Send Behavior](/inbox/settings) in Inbox Settings. ## The send button adapts to the ticket The send button's label tells you what will happen when you send, based on the ticket's state: | Label | What happens | | -------------------------- | ------------------------------------------------------------- | | **Send** | Sends your reply in the normal case. | | **Open ticket and send** | There's no open ticket yet — sending opens one. | | **Reopen ticket and send** | The ticket was resolved — sending reopens it. | | **Assign to me and send** | The ticket isn't assigned to you — sending assigns it to you. | ## The session window For channels like WhatsApp, you can reply freely within the customer's active session window. The composer shows a countdown of how long you have left to respond with a regular message. Once the window closes, you'll need an approved **template** to re-engage the customer. Composer with the attachment menu (audio, image, document), the session countdown, and the send button # Teams & Stores Source: https://docs.connectly.ai/business-hub/teams-and-stores/overview Organize your agents into teams, connect teams to store locations, and route conversations to the right group of people ⚙️ Teams let you organize your agents into working groups and route conversations to the right people. You can also connect teams to physical **store locations**, so customers reach the store nearest them. Teams and store routing are **advanced features** managed by workspace **owners**. Some capabilities may need to be enabled for your business — if you don't see them, contact your Connectly Account Manager. ## Teams vs. stores * **Teams** are working groups of agents. Agents only see conversations assigned to their teams, and routing directs conversations to a team. * **Stores** are simply teams tied to a physical location, so you can route customers to the branch nearest them. Create teams, organize them in folders, add members, and edit or delete them. Turn your store locations into teams, in bulk, from your location data. ## How teams and stores receive conversations Teams and stores receive conversations through **ticket routing**: In **Settings → Inbox → Ticket Routing**, set the routing scope to **Team Routing** so conversations are directed to teams instead of any available agent. See [Ticket Routing](/inbox/ticket-routing). In your AI agent's handover configuration, map each handover reason to the team (or store) that should handle it. Connectly applies your routing algorithm (Round Robin or Balanced Workload) to pick an available agent inside the matched team, then creates and assigns the ticket. With Team Routing, tickets are only assigned to agents who belong to a team. Agents who aren't a member of any team won't receive tickets. # Stores Source: https://docs.connectly.ai/business-hub/teams-and-stores/stores Turn your store locations into teams so you can route customers to the right branch ⚙️ In Connectly, a **store** is a [team](/teams-and-stores/teams) that's tied to a physical location. Setting up stores lets you route each customer to the team that runs their nearest branch — ideal for retailers, franchises, and multi-branch businesses. You can turn a single team into a store by giving it a **Location Address** when you [create it](/teams-and-stores/teams#create-a-team). If you have many locations, create them in bulk with either of the two options below (both under **Add Team**). ## Import Teams from Locations If your store locations already exist in Connectly, **Import Teams from Locations** turns them into teams. Pick the locations you want, optionally create **Sales** and **Customer Support** subteams for each, and import. Import Locations as Teams dialog with a list of selectable store locations and optional Sales and Customer Support subteams Team names match the location names. You can reorganize teams into folders and rename them after importing. ## Upload Teams with Locations To create location-based teams from scratch, **Upload Teams with Locations** uses a Google Sheet. It's a two-step flow: fill in the spreadsheet with your team names and addresses, then upload and process it. Batch Create Location-Based Teams via Google Sheets dialog with an Edit step and an Upload step The spreadsheet has a column for the team name, an optional description, and the address: Google Sheet with Team Name, Team Description, and Address columns filled with store locations Removing a team's row from the spreadsheet will **delete** that team on the next upload. Only remove rows for teams you actually want to delete. ## Routing customers to a store Once your stores exist, routing customers to their nearest store is configured in your **AI agent's handover** (with **Team Routing** turned on in [Ticket Routing](/inbox/ticket-routing)). There, you can have the agent ask the customer to choose a store and route the conversation to that location's team. That setup is part of the agent configuration and is covered separately. # Teams Source: https://docs.connectly.ai/business-hub/teams-and-stores/teams Create teams, organize them in folders, and manage their members ⚙️ A team is a working group of agents within your business. Teams do two things: they **organize who sees what** (agents only see conversations assigned to their teams), and they **receive routed conversations** (routing directs a conversation to a team, and an agent within it picks it up). Manage them in **Settings → Teams**. Teams page with the What are Teams panel, search, Add Team and Add Folder buttons, and a list of teams ## Team visibility Each team has a visibility model: | Visibility | What it means | | ---------------- | -------------------------------------------------------------------------------------------------- | | **Hierarchical** | Agents can see conversations from their team *and* its child teams. | | **Isolated** | Agents can see *only* their own team's conversations — useful for franchises or individual stores. | ## Create a team Click **Add Team** to see the ways to create teams. **Add Team Manually** opens the create dialog; the other options create location-based teams in bulk (see [Stores](/teams-and-stores/stores)). Add Team menu with Add Team Manually, Upload Teams with Locations, and Import Teams from Locations In the **Create Team** dialog you can set: * **Name** (required) — the team's name. * **Description** (optional). * **Location Address** (optional) — links the team to a physical store location, turning it into a [store](/teams-and-stores/stores). * **Folder** (optional) — place the team inside a folder for organization. Create Team dialog with Name, Description, Location Address, and Folder fields A team can live in a **folder** or be tied to a **location**, but not both at once. ## Folders Folders **visually group** teams on the Teams page. A folder only has a name — it has no members and doesn't receive conversations itself. Use **Add Folder** to create one. Create Folder dialog with a Name field ## Members Open a team's menu to **Add Member**, **Edit Team**, or **Delete Team**. Team menu showing Add Member, Edit Team, and Delete Team **Add Member** lets you pick a user from your business to add to the team. Remove a member from the team's menu. Add Team Member dialog with a Select User dropdown listing agents There are no team-level roles. Permissions come from a person's business role — workspace **owners** act as admins and can manage every team, while other agents simply belong to the teams they're added to. ## Edit or delete a team Use **Edit Team** to update its name, description, or folder, then **Save Changes**. **Delete Team** removes it. Edit Team dialog with Name, Description, and Folder fields and a Save Changes button # Create Template Source: https://docs.connectly.ai/business-management/create-template POST /v1/businesses/{businessId}/create/template Submit a WhatsApp template to Meta for approval ⏳ Submit a new WhatsApp template to Meta for review. Templates cannot be used until Meta approves them — typically within 5 minutes but up to 24 hours. Check approval status using [Get templates](/business-management/get-templates). ## Endpoint ```json theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/create/template ``` ## Request body | Field | Type | Required | Description | | --------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | string | Yes | Template name. Used to reference the template when sending messages. | | `language` | object | Yes | Language object with a single `code` field (e.g. `{ "code": "en_US" }`). See [Meta's supported languages](https://developers.facebook.com/docs/whatsapp/api/messages/message-templates#supported-languages). | | `category` | string | Yes | Template category: `MESSAGE_TEMPLATE_GROUP_CATEGORY_MARKETING`, `MESSAGE_TEMPLATE_GROUP_CATEGORY_UTILITY`, or `MESSAGE_TEMPLATE_GROUP_CATEGORY_AUTHENTICATION`. | | `template_components` | array | Yes | Ordered list of component objects defining the template structure. See [Template components](/business-management/templates#template-components). | ## Response ```json theme={null} { "entity": { "id": "template_name", "name": "template_name", "category": "MARKETING", "templates": [ { "id": "1385622408844485", "language": { "code": "en_US" }, "status": "MESSAGE_TEMPLATE_STATUS_PENDING", "createdAt": "2023-01-18T01:07:20.075195879Z", "updatedAt": "2023-01-18T01:07:20.075195879Z", "templateComponents": [...], "rejectionReason": null } ] } } ``` The template starts with `status: MESSAGE_TEMPLATE_STATUS_PENDING`. Once Meta reviews it, the status transitions to approved or rejected. *** ## Examples ```json theme={null} { "name": "simple_greeting", "language": { "code": "en_US" }, "template_components": [ { "body": { "text": { "text": "Thanks for reaching out! We'll be in touch shortly." } } } ] } ``` Variables use `{{N}}` syntax, numbered sequentially from 1. ```json theme={null} { "name": "order_confirmation", "language": { "code": "en_US" }, "template_components": [ { "body": { "text": { "text": "Hi {{1}}, your order {{2}} has been confirmed and will arrive by {{3}}." } } } ] } ``` Provide an example image URL in `example` to speed up Meta's approval review. ```json theme={null} { "name": "product_launch", "language": { "code": "en_US" }, "template_components": [ { "header": { "media": { "type": "TYPE_IMAGE", "example": ["https://cdn.connectly.ai/example/product.png"] } } }, { "body": { "text": { "text": "Hi {{1}}, check out our new product — {{2}}. Available now!" } } } ] } ``` Text headers support one variable (`{{1}}`). ```json theme={null} { "name": "appointment_reminder", "language": { "code": "en_US" }, "template_components": [ { "header": { "text": { "text": "Reminder: {{1}}" } } }, { "body": { "text": { "text": "Your appointment is scheduled for {{1}} at {{2}}." } } }, { "footer": { "text": { "text": "Reply CANCEL to cancel." } } } ] } ``` ```json theme={null} { "name": "invoice_template", "language": { "code": "en_US" }, "template_components": [ { "header": { "media": { "type": "TYPE_DOCUMENT", "example": ["https://cdn.connectly.ai/example/invoice.pdf"] } } }, { "body": { "text": { "text": "Please find your invoice attached." } } }, { "footer": { "text": { "text": "Contact support@example.com for questions." } } } ] } ``` Up to 3 quick reply buttons. Cannot be combined with URL or phone buttons. ```json theme={null} { "name": "confirm_appointment", "language": { "code": "en_US" }, "template_components": [ { "body": { "text": { "text": "Can you make your appointment on {{1}}?" } } }, { "button": { "quickReply": { "text": "Confirm" } } }, { "button": { "quickReply": { "text": "Reschedule" } } }, { "button": { "quickReply": { "text": "Cancel" } } } ] } ``` Do not use shortened URLs — WhatsApp rejects them. You can add a dynamic suffix variable to the URL. ```json theme={null} { "name": "track_order", "language": { "code": "en_US" }, "template_components": [ { "body": { "text": { "text": "Your order is on its way! Track it here:" } } }, { "button": { "url": { "text": "Track my order", "url": "https://example.com/track/{{1}}" } } } ] } ``` ```json theme={null} { "name": "contact_support", "language": { "code": "en_US" }, "template_components": [ { "body": { "text": { "text": "Need help? Call our support team directly." } } }, { "button": { "phoneNumber": { "text": "Call support", "phoneNumber": "+16044441234" } } } ] } ``` Carousel templates display multiple horizontally scrollable cards. Each card has its own header image, body, and buttons. ```json theme={null} { "name": "carousel_demo_1", "language": { "code": "en_US" }, "category": "MESSAGE_TEMPLATE_GROUP_CATEGORY_MARKETING", "template_components": [ { "body": { "text": { "text": "Hey there! 👋 Check out our latest products." } } }, { "carousel": { "cards": [ { "components": [ { "header": { "media": { "type": "TYPE_IMAGE", "example": ["https://example.com/card1.png"] } } }, { "body": { "text": { "text": "Perfect for you, {{1}}! 🍓" } } }, { "button": { "url": { "text": "Shop now", "url": "https://example.com/store/{{1}}" } } }, { "button": { "quickReply": { "text": "See more" } } } ] }, { "components": [ { "header": { "media": { "type": "TYPE_IMAGE", "example": ["https://example.com/card2.png"] } } }, { "body": { "text": { "text": "Unlock coupons for just {{1}}! 🎉" } } }, { "button": { "url": { "text": "Get coupons", "url": "https://example.com/coupons/{{1}}" } } }, { "button": { "quickReply": { "text": "Explore more" } } } ] } ] } } ] } ``` To send this carousel template, see [Send template message — Carousel](/message-api/send-template-message#carousel-template). # Delete Template Source: https://docs.connectly.ai/business-management/delete-template DELETE /v1/businesses/{businessId}/templates/{templateGroupId} Permanently remove a WhatsApp template from Meta and Connectly ✏️ Delete a WhatsApp template group from your account. This removes it from Meta (via the WhatsApp Business Management API) and from Connectly. Deleting unused templates frees capacity against your WABA's template limit. Deletion is permanent. Any active or scheduled campaign that references this template by name may fail to deliver after deletion. Review your campaigns and automations in the Connectly dashboard before deleting. ## Endpoint ```text theme={null} DELETE https://api.connectly.ai/v1/businesses/{businessId}/templates/{templateGroupId} ``` The request takes no body. ## Path parameters | Parameter | Type | Description | | ----------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `businessId` | string | Your business ID (UUID format). | | `templateGroupId` | string | The template **name** — the same `name` you set when creating it (e.g. `order_confirmation`). This is not an internal UUID. Deleting by name removes the entire template group, including all language variants. | To find the template name, call [Get templates](/business-management/get-templates) and use the `name` field from the template group object. ## Response A successful deletion returns HTTP `200` with an empty body. ```json theme={null} {} ``` This endpoint is **idempotent** — deleting a template that doesn't exist or was already deleted also returns `200`. ## Example ```bash theme={null} curl -X DELETE "https://api.connectly.ai/v1/businesses/{businessId}/templates/order_confirmation" \ -H "X-API-Key: YOUR_API_KEY" ``` If the same template name is registered on more than one WhatsApp channel under your business, this endpoint removes it from the default channel. Targeting a specific channel is not yet supported. # Get Templates Source: https://docs.connectly.ai/business-management/get-templates POST /v1/businesses/{businessId}/get/templates List all WhatsApp templates for your account, including their approval status and components 📋 Retrieve all WhatsApp templates associated with your business account. Templates with the same name but different language translations are grouped together as a **template group**. Use this endpoint to check approval status after submitting a new template. ## Endpoint ```text theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/get/templates ``` This endpoint uses POST with no request body. ## Response Returns a list of template groups under `entity.templateGroups`. Each group contains one or more language variants. **Template status values:** | Status | Description | | ---------------------------------- | ------------------------------------------------------ | | `MESSAGE_TEMPLATE_STATUS_PENDING` | Submitted to Meta and awaiting review. | | `MESSAGE_TEMPLATE_STATUS_APPROVED` | Approved by Meta — ready to use. | | `MESSAGE_TEMPLATE_STATUS_REJECTED` | Rejected by Meta. Check `rejectionReason` for details. | ## Example ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses/{businessId}/get/templates" \ -H "X-API-Key: YOUR_API_KEY" ``` ```json theme={null} { "entity": { "templateGroups": [ { "id": "order_confirmation", "name": "order_confirmation", "category": "ACCOUNT_UPDATE", "templates": [ { "id": "651213231234567", "language": { "code": "en" }, "status": "MESSAGE_TEMPLATE_STATUS_REJECTED", "createdAt": "2022-03-09T23:07:01Z", "updatedAt": "2022-03-09T23:07:01Z", "rejectionReason": "INVALID_FORMAT", "templateComponents": [ { "header": { "media": { "type": "TYPE_IMAGE", "example": ["https://example.com/header.png"] } } }, { "body": { "text": { "text": "Welcome to Connectly!", "example": [] } } }, { "footer": { "text": { "text": "Connectly.ai", "example": [] } } }, { "button": { "quickReply": { "text": "Continue" } } }, { "button": { "quickReply": { "text": "Stop" } } } ], "externalTemplate": { "whatsapp": { "id": "651213231234567", "name": "order_confirmation", "language": "en", "status": "REJECTED", "rejectedReason": "INVALID_FORMAT", "qualityScore": { "score": "UNKNOWN" } } } } ] } ] } } ``` The `externalTemplate.whatsapp` object contains the raw template data as returned directly by Meta, alongside Connectly's own representation in `templateComponents`. Use `templateComponents` for the Connectly-native format. # Migrate WhatsApp Accounts Source: https://docs.connectly.ai/business-management/migrate-whatsapp Move an existing WhatsApp Business number from another BSP to Connectly — what transfers, what doesn't, and how to do it ↪️ If your WhatsApp Business number is currently managed by another Business Solution Provider (BSP), you can migrate it to Connectly without losing your display name, quality rating, messaging limits, or approved templates. The migration moves the phone number between WhatsApp Business API accounts while keeping the same Facebook Business Manager ID. Migration only works if the source and destination accounts share the **same Facebook Business Manager ID**. If the IDs differ, migration is not possible. *** ## What transfers and what doesn't | | Transfers? | Notes | | ------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | Display name | ✅ Yes | Kept as-is after migration. | | Quality rating | ✅ Yes | Carried over to the new account. | | Messaging limit tier | ✅ Yes | No reset on migration. | | Official Business Account (green badge) | ✅ Yes | Requires 2FA to be disabled during migration. | | High-quality approved templates | ✅ Yes | Copied to the destination WABA — no re-review needed. | | Low-quality, rejected, or pending templates | ❌ No | Only high-quality approved templates are migrated. | | Existing templates in destination WABA | ❌ No | They are not overwritten. | | Message & chat history | ❌ No | Conversation history does not migrate. | | Pre-migration billing | ❌ No | Messages sent before migration are still charged to the previous BSP. Messages sent after migration are charged to Connectly. | *** ## Requirements Before starting the migration, make sure you have: * Physical access to the phone number to receive a 6-digit OTP via SMS or voice call. * A **verified** Facebook Business Manager account linked to the number. Accounts in "Sandboxed" mode cannot be migrated. * **Two-Factor Verification (2FA) disabled** on the current BSP — you must request this from your existing BSP before migrating. * The **same Facebook Business Manager ID** on both the source and destination accounts. You can verify your Facebook Business Manager ID [here](https://www.facebook.com/business/help/1181250022022158?id=180505742745347). *** ## Migration steps Contact your current BSP and ask them to disable Two-Factor Verification (2FA) on the WhatsApp number you want to migrate. Get written confirmation that 2FA is disabled before proceeding. Sign up at [connectly.ai/signup](https://connectly.ai/signup) if you don't already have an account. Log into [inbox.connectly.ai](https://inbox.connectly.ai) and click **Connect WhatsApp**. Follow the steps to link your WhatsApp Business Account. You **must** select the same Facebook Business Manager ID as the one associated with your existing WhatsApp number. If the Business Manager is only an admin (not the owner), the migration will not work. When prompted to add a phone number at the final step of the setup flow — **stop**. Do not enter the number. Connectly will handle this step during the migration. Email [contact@connectly.ai](mailto:contact@connectly.ai) with the subject line **"Migrate WhatsApp account"** and include your business name. Connectly will coordinate the migration timing with you. During the migration, WhatsApp will send a 6-digit OTP to your phone number via SMS or voice call. Connectly will coordinate with you to enter this code at the right moment to complete the transfer. *** ## After migration * Messages sent before migration are charged to your previous BSP. * Messages sent after migration are charged to Connectly. * Re-enable Two-Factor Verification on your number after migration is complete. * Your approved templates are immediately available in your new Connectly account — no resubmission needed. *** Need help with your migration? Email [contact@connectly.ai](mailto:contact@connectly.ai) or contact your Connectly Account Manager. # Business Management Overview Source: https://docs.connectly.ai/business-management/overview Manage WhatsApp message templates, monitor quality signals, and configure your business account via the Connectly API 🚀 The Business Management API covers the account-level operations that support your messaging — creating and managing WhatsApp templates, and monitoring your account's quality score and messaging limits. ## What's available Understand how WhatsApp templates work — components, variables, categories, and the approval process. `POST /v1/businesses/{businessId}/create/template` — submit a new template to Meta for approval. `POST /v1/businesses/{businessId}/get/templates` — list all templates and their approval status. `DELETE /v1/businesses/{businessId}/templates/{templateGroupId}` — remove a template from Meta and Connectly. `GET /v1/businesses/{businessId}/get/quality_signals` — fetch your messaging limit tier and quality score. Need to upload media for use in template headers or messages? See the [Assets API](/assets/overview) — it's a separate section but closely related. # Get Quality Signals Source: https://docs.connectly.ai/business-management/quality-signals GET /v1/businesses/{businessId}/get/quality_signals Fetch your WhatsApp messaging limit tier and quality score 💯 Retrieve your WhatsApp account's current quality signals — the messaging limit tier and quality score that Meta assigns based on your message delivery and customer feedback. Use these to monitor your account health and understand how many business-initiated conversations you can send per day. ## Messaging limit tiers Meta defines four tiers for business-initiated conversations in a rolling 24-hour period: | Tier value | Conversations per 24 hours | | ---------------------------------------- | -------------------------- | | `WHATS_APP_MESSAGE_LIMIT_TIER_1K` | 1,000 unique customers | | `WHATS_APP_MESSAGE_LIMIT_TIER_10K` | 10,000 unique customers | | `WHATS_APP_MESSAGE_LIMIT_TIER_100K` | 100,000 unique customers | | `WHATS_APP_MESSAGE_LIMIT_TIER_UNLIMITED` | Unlimited | The `messagesLimitTier` field only updates when Meta registers a change. If your account is new, it may appear as `WHATS_APP_MESSAGE_LIMIT_TIER_UNSPECIFIED` until your first tier assignment. This is expected and does not indicate an error. See [Meta's messaging limits documentation](https://developers.facebook.com/docs/whatsapp/messaging-limits) for full details. *** ## Endpoint ```json theme={null} GET https://api.connectly.ai/v1/businesses/{businessId}/get/quality_signals ``` ## Response ```json theme={null} { "entity": { "signals": [ { "whatsappQualitySignals": { "phoneNumber": "+16501234567", "wabaId": "649852411234567", "messagesLimitTier": "WHATS_APP_MESSAGE_LIMIT_TIER_UNSPECIFIED", "qualityScore": "WHATS_APP_QUALITY_SCORE_HIGH" } } ] } } ``` **Response fields:** | Field | Description | | ------------------------------------------ | --------------------------------------------------------------------- | | `whatsappQualitySignals.phoneNumber` | The WhatsApp phone number these signals apply to. | | `whatsappQualitySignals.wabaId` | Your WhatsApp Business Account ID. | | `whatsappQualitySignals.messagesLimitTier` | Your current messaging limit tier. `UNSPECIFIED` if not yet assigned. | | `whatsappQualitySignals.qualityScore` | Your current quality score: `HIGH`, `MEDIUM`, or `LOW`. | ## Example ```bash theme={null} curl -X GET "https://api.connectly.ai/v1/businesses/{businessId}/get/quality_signals" \ -H "X-API-Key: YOUR_API_KEY" ``` # Templates Overview Source: https://docs.connectly.ai/business-management/template-management How WhatsApp message templates work — components, variable syntax, button types, categories, and the Meta approval process ⏭️ WhatsApp message templates are pre-approved message formats you manage through Connectly and Meta's WhatsApp Manager. Because Meta approves them in advance, templates can be sent to any customer at any time — including first-time contacts and those outside the 24-hour session window. Once a template is approved, use it to send messages via [Send template message](https://docs.connectly.ai/messaging/template-messages). *** ## Template categories Every template must belong to one of three categories: | Category | Description | | ------------------------------------------------ | ------------------------------------------------------------------------------- | | `MESSAGE_TEMPLATE_GROUP_CATEGORY_MARKETING` | Promotional messages, offers, product launches. | | `MESSAGE_TEMPLATE_GROUP_CATEGORY_UTILITY` | Transactional messages — order confirmations, shipping updates, account alerts. | | `MESSAGE_TEMPLATE_GROUP_CATEGORY_AUTHENTICATION` | OTP and verification codes. | *** ## Template components A template is built from `template_components` — an ordered list of component objects. Each component is one of: header, body, footer, or button. ### Header Template headers support four types: | Type | Description | | --------------- | ---------------------------------------------------------------- | | `TYPE_IMAGE` | Image header. Provide an example image URL to speed up approval. | | `TYPE_VIDEO` | Video header. Provide an example video URL. | | `TYPE_DOCUMENT` | Document header. Provide an example document URL. | | `TYPE_TEXT` | Text header. Supports one variable (`{{1}}`). | ### Body The main text of the template. Supports variables using `{{N}}` syntax — `{{1}}`, `{{2}}`, `{{3}}`, etc., numbered sequentially from 1 to 15. Variables must be numbered sequentially from 1. You cannot skip numbers or use duplicates. For example, `{{1}}`, `{{2}}`, `{{3}}` is valid; `{{1}}`, `{{3}}` is not. ### Footer Optional plain text displayed below the body. Does not support variables. ### Buttons Up to 3 buttons per template. Three types are supported: | Type | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `quickReply` | Sends the button text as a message when tapped. Can trigger automated responses. | | `url` | Opens a URL in the customer's browser. Supports one dynamic suffix variable. Do not use shortened URLs (e.g. bit.ly) — WhatsApp automatically rejects them. | | `phoneNumber` | Dials a phone number directly. | You cannot mix Quick Reply and Call-to-Action buttons in the same template. Use up to 3 Quick Reply buttons **or** Call-to-Action buttons (URL and/or phone) — not both. *** ## Carousel templates Carousel templates display multiple cards in a horizontally scrollable format. Each card has its own header image, body text, and buttons. Create a carousel template by including a `carousel` component in `template_components`. See [Create template](https://docs.connectly.ai/api-reference/create-template) for the full payload structure. *** ## Approval process After you submit a template via the [Create template](https://docs.connectly.ai/business-management/create-template) endpoint, Meta reviews it automatically. Templates are typically approved within 5 minutes but can take up to 24 hours. The template status starts as `MESSAGE_TEMPLATE_STATUS_PENDING` and transitions to approved or rejected. Check the current status of your templates at any time using [Get templates](https://docs.connectly.ai/business-management/create-template). Media size limits apply to template headers. See [Meta's media documentation](https://developers.facebook.com/docs/whatsapp/on-premises/reference/media#post-processing) for the full list of constraints. *** ## Next steps Submit a new template to Meta for review. Use an approved template to send a message. # Campaigns Overview Source: https://docs.connectly.ai/campaigns/overview Send a complete sequence of WhatsApp messages to multiple customers from a single API call using Connectly's Flow Builder and Campaigns API 📢 Connectly campaigns let you trigger a full message flow — multiple steps, interactive buttons, conditional branching, and time delays — to a list of recipients in one API call. You build the flow visually in the Flow Builder, then kick off sendouts by passing the `campaignName` to the Campaigns API. ## How it works Go to [inbox.connectly.ai](https://inbox.connectly.ai) and click **Create new Campaign**. Follow the steps in the wizard and send the campaign to yourself to verify it works. Go to [inbox.connectly.ai](https://inbox.connectly.ai) and click **Create new Campaign**. Design your message sequence — add steps, buttons, and branching logic — then send a test to your own WhatsApp number to verify it works end-to-end. After a successful test send, navigate back to the **Flow Builder** section of the inbox. Select **Resend or Edit** next to your campaign. The name displayed is your `campaignName` — copy it exactly as shown. Once your test send succeeds, go back to the **Flow Builder**, select **Resend or Edit** next to your campaign, and copy the name exactly as shown. This is the `campaignName` you'll pass in every API request. Pass the `campaignName` in your API request entries. See the example below. Finalize and publish the campaign from the Connectly UI. The API will reject sendouts for unpublished campaigns with a `409` error. Call `POST /v1/businesses/{businessId}/send/campaigns` with your recipient list. Connectly groups entries by campaign name and version and returns a per-sendout status report. Your Account Manager can help you set up a campaign flow and share the `campaignName` to use in API requests. *** ## Adding time delays The Flow Builder includes a **Time Delay** card you can drop between any two message steps. After the initial message is sent, Connectly waits for the configured duration before delivering the follow-up. **Maximum delay: 20 hours.** | Condition | Behaviour | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | | All customers | Follow-up is sent to every customer who received the initial message. | | Did not click a button | Follow-up is sent only to customers who received the message but didn't tap any button — useful for re-engagement nudges. | To add a delay, drag the **Time delay** card from the sidebar onto your canvas, position it between the two message cards, and connect the nodes. Set the duration and condition in the card settings, then publish as normal. Use the "did not click a button" condition to avoid messaging customers who already responded. *** ## Minimal API example The smallest valid request — one recipient, one campaign, no variables: ```json theme={null} { "entries": [ { "client": "+16045552331", "campaignName": "your_campaign_name" } ] } ``` For the full request reference — variables, versioning, bulk sends, scheduling, and error handling — see [Send Campaigns](/campaigns/send-campaigns). *** ## Next steps Full endpoint reference — entries, variables, versioning, options, and error responses. Track delivery status and button clicks for each message in your campaign flow. # Send Campaigns Source: https://docs.connectly.ai/campaigns/send-campaigns POST /v1/businesses/{businessId}/send/campaigns Trigger one or more campaign flows to multiple recipients with variables, versioning, and per-entry status reporting 📩 ## Endpoint ```json theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/send/campaigns ``` | Parameter | Location | Description | | ------------ | -------- | --------------------------- | | `businessId` | Path | Your Connectly business ID. | | `X-API-Key` | Header | Your API key. | Campaigns must be published in the Connectly Flow Builder before you call this endpoint. Sending to an unpublished campaign returns a `409` error. *** ## Request body ```json theme={null} { "entries": [...], "options": {...} } ``` ### `entries` array (required) Each object in `entries` represents one recipient. You can mix entries for different campaigns in the same request. | Field | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client` | string | Yes\* | Recipient's WhatsApp number in E.164 format (e.g. `+16505551234`), or a BSUID — bare (`US.13491208655302741918`, as received in webhooks) or `bsuid:`-prefixed. \*Not required when `userId` is set. | | `userId` | string | No | BSUID (e.g. `US.13491208655302741918`), exactly as received in webhooks, to target a customer by BSUID. Only read when `client` is empty — if both are set, `client` wins. | | `campaignName` | string | Yes | Exact campaign name copied from the Flow Builder. | | `variables` | object | No | Key-value pairs substituted into the flow's variable placeholders for this recipient. | | `campaignVersion` | string | No | Specific campaign version to target. If omitted, resolved by `options.if_version_unspecified`. | | `scheduledAt` | string | No | ISO 8601 datetime for future delivery (e.g. `"2024-03-15T10:00:00Z"`). Alpha — contact your Account Manager before using. | ### `options` object (optional) | Field | Values | Description | | -------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `if_version_unspecified` | `reuse_last_active` | How to resolve the version when `campaignVersion` is not set. `reuse_last_active` targets the latest published version (default). | | `if_duplicate_check_unspecified` | `allow_one` / `allow_multiple` | `allow_one` (default) prevents sending the same campaign to the same customer twice. Set `allow_multiple` to override this — for example, for recurring service alerts. | *** ## Examples The smallest valid entry. Use this for campaigns that have no variable placeholders. ```json theme={null} { "entries": [ { "client": "+16505551236", "campaignName": "campaign_basic" } ] } ``` Variables are defined in the campaign flow in the Connectly UI. Pass the same key names here. ```json theme={null} { "entries": [ { "client": "+16505551237", "campaignName": "campaign_with_variables", "variables": { "username": "Jane Doe", "product": "Smart Watch" } } ] } ``` Connectly creates a separate sendout for each unique `campaignName` + `campaignVersion` combination. This request produces three sendouts: `campaign_A` at `v1.0`, `campaign_A` at its latest active version, and `campaign_B` at `v1.0`. ```json theme={null} { "entries": [ { "client": "+14155558234", "campaignName": "campaign_A", "campaignVersion": "v1.0", "variables": { "key1": "value1", "key2": "value2" } }, { "client": "+14155559345", "campaignName": "campaign_A", "variables": { "key1": "value3", "key2": "value4" } }, { "client": "+14155550456", "campaignName": "campaign_B", "campaignVersion": "v1.0", "variables": { "Date": "2024-03-15", "Name": "Alice", "Price": "$99" } } ], "options": { "if_version_unspecified": "reuse_last_active" } } ``` By default, the API prevents sending the same campaign to the same customer twice. Override with `allow_multiple` for use cases like recurring alerts. ```json theme={null} { "entries": [ { "client": "+14155558234", "campaignName": "service_alert", "variables": { "incident": "Scheduled maintenance" } } ], "options": { "if_duplicate_check_unspecified": "allow_multiple" } } ``` *** ## Response The API always returns HTTP `200` with a `data` array. Each element corresponds to a unique `campaignName` + `campaignVersion` sendout generated by the request. | Field | Type | Description | | ----------------- | -------------- | ------------------------------------------------------ | | `campaignId` | string | Unique identifier for the campaign. | | `campaignName` | string | Name of the campaign. | | `campaignVersion` | string | Version used for this sendout. | | `sendoutId` | string | Unique identifier for the sendout. | | `status` | string | `created`, `updated`, or `error`. | | `acceptedCount` | integer | Number of entries accepted into the sendout. | | `rejectedCount` | integer | Number of entries rejected. | | `error` | object \| null | Error detail if `status` is `error`, otherwise `null`. | If you call the API again for the same campaign and recipient group, `status` returns `"updated"` rather than `"created"` — no duplicate sendout is created. **Success response** ```json theme={null} { "data": [ { "campaignId": "2963626c-90ea-43e5-9b66-4ce70f003fe3", "campaignName": "campaign_basic", "campaignVersion": "018c5c51-8631-28b5-3c81-b70ecf14faef", "sendoutId": "183e801b-1438-4177-b283-909135096e69", "status": "created", "acceptedCount": 1, "rejectedCount": 0, "error": null } ] } ``` *** ## Error responses Entry-level errors appear inside the `data` array with `status: "error"` — they don't cause the whole request to fail. HTTP-level errors indicate request-wide failures. | HTTP status | Meaning | | ----------- | -------------------------------------------------------------------------- | | `400` | Request body is malformed or an entry is invalid. | | `401` | Missing or invalid API key. | | `404` | One or more referenced campaigns do not exist. | | `409` | Campaign is not in a state that accepts sendouts (e.g. not yet published). | | `429` | Rate limit exceeded. | | `500` | Internal server error. | ```json theme={null} { "data": [ { "campaignId": null, "campaignName": "wrong_name", "campaignVersion": null, "sendoutId": null, "status": "error", "acceptedCount": 0, "rejectedCount": 1, "error": { "message": "Campaign not found", "type": "ERROR_TYPE_NOT_FOUND", "code": "ERROR_CODE_CAMPAIGN_NOT_FOUND", "userTitle": "Campaign not found", "userMessage": "Please review the campaign name and/or campaign id." } } ] } ``` ```json theme={null} { "data": [ { "campaignId": "2963626c-90ea-43e5-9b66-4ce70f003fe3", "campaignName": "campaign_basic", "campaignVersion": "018c4a55-f3df-580c-1629-602f3b14d190", "sendoutId": null, "status": "error", "acceptedCount": 0, "rejectedCount": 1, "error": { "message": "Campaign version not found", "type": "ERROR_TYPE_NOT_FOUND", "code": "ERROR_CODE_CAMPAIGN_VERSION_NOT_FOUND", "userTitle": "Campaign version not found", "userMessage": "Please review the campaign version." } } ] } ``` Triggered when required flow variables are absent or the entry payload is otherwise invalid. ```json theme={null} { "data": [ { "campaignId": "2963626c-90ea-43e5-9b66-4ce70f003fe3", "campaignName": "campaign_with_variables", "campaignVersion": "018c5c51-8631-28b5-3c81-b70ecf14faef", "sendoutId": "183e801b-1438-4177-b283-909135096e69", "status": "error", "acceptedCount": 0, "rejectedCount": 1, "error": { "message": "Campaign entry is invalid", "type": "ERROR_TYPE_INVALID_REQUEST", "code": "ERROR_CODE_CAMPAIGN_ENTRY_INVALID", "userTitle": "Campaign entry is invalid", "userMessage": "Please check the inputs to the campaign entry." } } ] } ``` *** ## Rate limiting This endpoint is limited to **200 requests per second**. Exceeding this returns HTTP `429 Too Many Requests`. Use exponential backoff in your client if you expect high-volume sendouts. # Authentication & API Keys Source: https://docs.connectly.ai/get-started/authentication-and-api-keys Generate and manage API keys from the Connectly Settings page — with scoped permissions and optional expiration 🗝️ API keys authenticate your requests to the Connectly API. You can create multiple keys with different scopes and expiration dates — for example, a key scoped to messaging only for your backend, and a separate key with full access for internal tooling. *** ## Generate an API key In the Connectly inbox, click **Settings** in the left sidebar, then select the **General** tab. Scroll down to the **API Key and Webhook Secret** section and click **Create API Key**. Give the key a descriptive name so you can identify it later — for example, `Production Backend` or ` Integration`. Choose which permissions this key should have. If no scopes are selected, the key has full access to all operations. | Scope | What it allows | | --------------------- | -------------------------------------------------------------- | | `ai.manage` | Manage AI tools and internal diagnostics | | `campaign.manage` | Manage campaigns, templates, flows, automations, and audiences | | `commerce.manage` | Manage orders, checkouts, billing, and delivery controls | | `conversation.manage` | Manage rooms, inbox actions, tickets, and tags | | `customer.manage` | Manage customer records and profile data | | `key.manage` | Create, revoke, and manage API keys | | `messaging.send` | Send outbound messages and campaigns | | `system.manage` | Perform privileged internal system operations | | `workspace.manage` | Manage business, channels, users, teams, and integrations | Follow the principle of least privilege — only select the scopes your integration actually needs. For example, a webhook integration only needs `messaging.send` and `campaign.manage`. By default, keys have no expiration. If you want the key to automatically expire — for example, for a temporary integration or a contractor — select an expiration period from the dropdown. Click **Create Key**. Your API key is displayed **once only** — copy it immediately and store it somewhere secure such as a password manager or a secrets manager. The key is shown only once at creation time. Connectly does not store the plaintext value. If you lose it, you'll need to generate a new one. *** ## Use your API key Include the key in the `X-API-Key` header on every request to the Connectly API: ```bash theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses//send/messages \ --header 'Content-Type: application/json' \ --header 'X-API-Key: ' \ --data '{...}' ``` Any request missing a valid `X-API-Key` header returns an `ERROR_TYPE_AUTHENTICATION` error. *** ## Manage existing keys From **Settings → General → API Key and Webhook Secret** you can view all keys associated with your account, see their names, scopes, and expiration dates, and revoke any key that is no longer needed. Generating a new key does **not** automatically invalidate existing keys. To revoke a key, delete it explicitly from the Settings page. *** ## Security best practices * **Never expose your API key client-side** — don't include it in frontend JavaScript, mobile apps, or public repositories. * **Use scoped keys** — restrict each key to only the permissions it needs. * **Rotate keys regularly** — especially after team member offboarding or if you suspect a key has been compromised. * **Set expiration dates** for temporary integrations or external collaborators. * **Store keys in a secrets manager** — use a tool like AWS Secrets Manager, HashiCorp Vault, or a password manager rather than hardcoding keys in config files. # Quick Start Source: https://docs.connectly.ai/get-started/quick-start Set up your account and send your first WhatsApp message in under 5 minutes ⏲️ ## Before you begin You'll need three things in place before making your first API call: You need a Connectly business account and a **business ID** to scope your API requests. Your **API key** authenticates every request — contact your Account Manager if you don't have one yet, or generate one from the Connectly Dashboard at any time. Keep your API key secret. Never commit it to source control or expose it in client-side code. If a key is compromised, regenerate it immediately from the Dashboard. You need a **verified Facebook Business Manager account** with admin access. Meta uses this to own and manage your WhatsApp Business Account, and to review and approve your message templates. A WABA is required to send and receive WhatsApp messages as a business. Connectly is a WhatsApp BSP (Business Service Provider) and can onboard you directly from the Connectly inbox. To register a WABA you'll need: | Resource | Purpose | | ---------------------------------- | -------------------------------------------------------------- | | Facebook Page | Required by Meta to link your business presence to your WABA. | | Facebook Business Manager | Where your WABA lives and your templates are managed. | | Phone number (SMS or call capable) | Used for one-time verification of your WhatsApp sender number. | Your Account Manager will walk you through the registration once these are in place. *** ## Step 1: Authenticate your requests Every Connectly API request must include your API key as an `X-API-Key` header. There are no session tokens or OAuth flows — just a single header on every call. ```bash cURL theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages \ --header 'Content-Type: application/json' \ --header 'X-API-Key: ' \ --data '{}' ``` ```python Python theme={null} import requests headers = { "Content-Type": "application/json", "X-API-Key": "" } response = requests.post( "https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages", headers=headers, json={} ) ``` ```javascript JavaScript theme={null} const axios = require('axios'); const headers = { "Content-Type": "application/json", "X-API-Key": "" }; axios.post( "https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages", {}, { headers } ); ``` Any request missing a valid `X-API-Key` header returns an `ERROR_TYPE_AUTHENTICATION` error. *** ## Step 2: Send a template message Template messages are pre-approved message formats managed in your Facebook Business Manager. They are not subject to the 24-hour reply window — you can send them to any customer at any time, even if they have never messaged you before. Your template must be created and approved in [Facebook Business Manager](https://business.facebook.com) before you can use it in an API call. Contact the Connectly team if you need help getting a template approved. ```bash cURL theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages \ --header 'Content-Type: application/json' \ --header 'X-API-Key: ' \ --data '{ "number": "+1XXXXXXXXXX", "templateName": "your_template_name", "language": "en_US", "parameters": [ { "name": "body_1", "value": "John" }, { "name": "body_2", "value": "your order" } ] }' ``` ```python Python theme={null} import requests payload = { "number": "+1XXXXXXXXXX", "templateName": "your_template_name", "language": "en_US", "parameters": [ { "name": "body_1", "value": "John" }, { "name": "body_2", "value": "your order" } ] } response = requests.post( "https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages", headers={ "Content-Type": "application/json", "X-API-Key": "" }, json=payload ) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios'); const payload = { number: "+1XXXXXXXXXX", templateName: "your_template_name", language: "en_US", parameters: [ { name: "body_1", value: "John" }, { name: "body_2", value: "your order" } ] }; axios.post( "https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages", payload, { headers: { "Content-Type": "application/json", "X-API-Key": "" } } ).then(r => console.log(r.data)); ``` **Request fields** | Field | Type | Description | | -------------- | ------ | ------------------------------------------------------------------------------------ | | `number` | string | Recipient's WhatsApp number in E.164 format (e.g. `+1XXXXXXXXXX`). | | `templateName` | string | Exact name of the approved template in your WhatsApp Manager. | | `language` | string | Language code for the template translation (e.g. `en_US`, `pt_BR`, `es`). | | `parameters` | array | Name/value pairs that substitute variables in the template body, header, or buttons. | **Response** ```json theme={null} { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` The `id` is your message ID. Use it to correlate delivery status events if you're subscribed to [webhooks](/message-api/webhook-api). *** ## Step 3: Reply with a session message Once a customer messages you, a **24-hour session window** opens. During that window you can send free-form session messages — no template or pre-approval needed. If the customer hasn't messaged you in the last 24 hours — or has never messaged you at all — you must use a template message instead. The session endpoint will return an error outside this window. ```bash cURL theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses//send/messages \ --header 'Content-Type: application/json' \ --header 'X-API-Key: ' \ --data '{ "recipient": { "id": "+1XXXXXXXXXX", "channelType": "whatsapp" }, "message": { "text": "Hello! How can we help you today?" } }' ``` ```python Python theme={null} import requests payload = { "recipient": { "id": "+1XXXXXXXXXX", "channelType": "whatsapp" }, "message": { "text": "Hello! How can we help you today?" } } response = requests.post( "https://api.connectly.ai/v1/businesses//send/messages", headers={ "Content-Type": "application/json", "X-API-Key": "" }, json=payload ) print(response.json()) ``` ```javascript JavaScript theme={null} const axios = require('axios'); const payload = { recipient: { id: "+1XXXXXXXXXX", channelType: "whatsapp" }, message: { text: "Hello! How can we help you today?" } }; axios.post( "https://api.connectly.ai/v1/businesses//send/messages", payload, { headers: { "Content-Type": "application/json", "X-API-Key": "" } } ).then(r => console.log(r.data)); ``` **Response** ```json theme={null} { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` Add an optional `sender` object (`{ "id": "+14151111234", "channelType": "whatsapp" }`) to choose which of your WhatsApp numbers sends the message. If omitted, Connectly uses your default number. *** ## Which message type should I use? | Situation | Use | | --------------------------------------------- | ---------------- | | First message to a customer | Template message | | Customer hasn't replied in over 24 hours | Template message | | Replying within the 24-hour window | Session message | | Proactive notifications, campaigns, reminders | Template message | | Bulk send to many recipients at once | Campaigns API | # Intercom Source: https://docs.connectly.ai/integrations/intercom Mirror WhatsApp conversations in Intercom and reply to customers via the Intercom inbox using Make (Integromat) as the connector 📠 With the Intercom integration, inbound WhatsApp messages appear as conversations in your Intercom inbox. Replies you post in Intercom are sent back to the customer as WhatsApp messages. The integration is built using [Make](https://www.make.com) (formerly Integromat) as the connector between Connectly and Intercom. ## Prerequisites * A fully onboarded Connectly account with a working WhatsApp business number. * A [Make](https://www.make.com/en/register) account. * An Intercom account. * Your Connectly **Business ID** and **API key** — contact your Account Manager to obtain these. *** ## Setup Sign up for a Make account at [make.com](https://www.make.com/en/register) and authenticate your Intercom account following [this video guide](https://www.loom.com/share/aa7284f0f5e9443ca471fbdcd14cfc70). When prompted, click **Authorize Access**. Download the [Connectly to Intercom JSON file](https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTPnSja6RkBiRLGAbuXVJ%2Fuploads%2FaOdASFJ9EJh1UkoIoHZA%2Fconnectly_to_intercom.json?alt=media\&token=b64f4180-1ec9-47d5-86ad-773655ff1511) (right-click → Save Link As). Then follow [this video guide](https://www.loom.com/share/8ca183caa6f543a0b4944f2dc1f886e1) to create a new Make scenario and import the file. Download the [Intercom to Connectly JSON file](https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FTPnSja6RkBiRLGAbuXVJ%2Fuploads%2FwNfv5yLaQ7Ni2r6hHcif%2Fintercom_to_connectly.json?alt=media\&token=1e29c3a8-6c28-49b7-b034-657cac9c8d9a) (right-click → Save Link As). Follow [this video guide](https://www.loom.com/share/ad91ced2432647d7b3d47c0d4077bfa3) to import it as a second Make scenario. The setup relies on video guides and downloadable scenario files. If any links become unavailable, contact your Account Manager for assistance. # Klaviyo Source: https://docs.connectly.ai/integrations/klaviyo Trigger WhatsApp campaigns from Klaviyo flows using the Connectly API — set up webhooks, map variables, and automate customer messaging from your e-commerce data 🛍️ Klaviyo is a marketing automation platform built for e-commerce. When connected to Connectly and an e-commerce platform like Shopify, you can use real-time customer data to trigger highly targeted WhatsApp campaigns automatically. ## Use cases Greet new customers when they subscribe or make their first purchase, setting a positive tone for future interactions. Send reminders to customers about items left in their cart to reduce abandonment rates and recover lost sales. Send automated order confirmations and real-time shipping updates to keep customers informed post-purchase. Build loyalty with a personalised thank you message after purchase, and follow up with a feedback or survey request. Foster personal relationships by celebrating customer birthdays and first-purchase anniversaries with special messages and promotions. *** ## Requirements * A Connectly account with a working WhatsApp business number * A Klaviyo account * An e-commerce or CRM platform integrated with Klaviyo (e.g. Shopify, Salesforce, Magento, Wix, WooCommerce — [see the full list](https://www.klaviyo.com/integrations)) *** ## Setup ### Step 1: Get your Connectly API key In the Connectly platform, go to **Settings → General** and scroll down to **API Key and Webhook Secret**. Click **Generate API Key**. Connectly Settings — API Key and Webhook Secret section Click **Create API Key** to generate and display your key. Create API Key button and Business ID The API key is shown **only once**. Copy it immediately and store it somewhere secure (e.g. a password manager). Generating a new key will invalidate the previous one. *** ### Step 2: Get your campaign destination URL and JSON body You need the campaign's endpoint URL and JSON payload to configure the Klaviyo webhook. When setting up a new campaign, select **"Use the Connectly API"** on the **Choose Audience** step and click **Next**. Choose Audience — Use the Connectly API option selected Navigate to the **Campaigns** section and click the **`<>`** icon next to the campaign you want to connect. Campaigns list with the code icon highlighted Both options display the **Curl Snippet** panel with your destination URL and JSON body highlighted. Curl snippet showing destination URL and JSON body Curl snippet showing destination URL and JSON body *** ### Step 3: Create a flow in Klaviyo Log into Klaviyo, navigate to **Flows**, and click **Create Flow**. Klaviyo Flows list with Create Flow button Choose an existing template or click **Build your own** to set up a custom trigger and filters. Klaviyo Create Flow — template options *** ### Step 4: Add and configure the Webhook node Drag the **Webhook** node from the left sidebar and drop it into your flow at the point where you want the WhatsApp campaign to be triggered. Klaviyo flow with Webhook node in the sidebar and dropped onto the canvas If this is your first time using the Webhook node, Klaviyo will prompt you to set up Multi-Factor Authentication before proceeding. Multi-Factor Authentication required prompt Click on the Webhook node to open its settings panel. Fill in the following fields: Webhook details panel with Destination URL, Headers, and JSON body fields | Field | Value | | ------------------- | ---------------------------------------------------------- | | **Destination URL** | The URL from your Connectly campaign curl snippet | | **Headers — Key** | `x-api-key` | | **Headers — Value** | Your Connectly API key | | **JSON body** | The JSON payload from your Connectly campaign curl snippet | *** ### Step 5: Map campaign variables Your JSON body contains placeholder values that need to be replaced with Klaviyo profile properties. In the webhook settings panel, click the **Expand** icon on the JSON body section to see the full payload. Each value marked `CHANGE_TO_*` needs to be replaced. Expanded JSON body showing placeholder values to replace Click the **Preview** button in the Settings panel header. The **Preview Profile Info** panel appears, showing all available profile properties. Hover over any property to reveal its Klaviyo variable name, then click to copy it. Klaviyo Trigger Preview showing profile properties with variable name tooltip Klaviyo Trigger Preview showing profile properties with variable name tooltip Preview Profile Info panel with variable name tooltip on hover Go back to the JSON body and replace each placeholder with the corresponding Klaviyo variable. For example, replace `"CHANGE_TO_TARGET_PHONE_NUMBER"` with `"{{ person.phone_number|default:'' }}"` and `"CHANGE_TO_VALUE"` with `"{{ person.first_name|default:'' }}"`. JSON body with Klaviyo variables replacing the placeholders JSON body with Klaviyo variables replacing the placeholders Repeat for every variable in the payload. *** ### Step 6: Test and activate the flow Click the **Trigger** node to open its settings panel, then click **Preview**. The Trigger Preview tool shows which profiles would enter the flow and whether they pass your filters. Use the search bar to look up a specific test user. Klaviyo Trigger Preview panel showing profiles and filter results Klaviyo Trigger Preview panel showing profiles and filter results Trigger Preview search results showing Pass/Fail status for profiles Trigger Preview search results showing Pass/Fail status for profiles Click on a flow message node to open its details. Change the status dropdown from **Draft** to **Manual**. In Manual mode, messages are scheduled as if live but held for your review instead of being sent automatically. Flow message details panel with Manual status selected Flow message details panel with Manual status selected Select the flow message and click **View details** in the Performance section of the right sidebar. Flow message details with View details button highlighted Flow message details with View details button highlighted Navigate to the **Recipient activity** tab and click **Needs review**. A list of eligible recipients appears — you can approve or cancel sends individually or for the entire list. Recipient activity tab showing Needs review filter with Send All and Cancel All buttons Recipient activity tab showing Needs review filter with Send All and Cancel All buttons Recipient activity tab showing Needs review filter with Send All and Cancel All buttons When a message status is updated from Manual to Live, recipients in **Waiting** will receive the message automatically at send time. Recipients in **Needs Review** will still require manual approval. Once you've finished building and testing, click **Save** and then **Review and turn on** in the top right corner. Check the tutorial video here: [How to adjust variables at Klaviyo](https://drive.google.com/open?id=1qT15UA5qzrdAhgV_TKN89VJm-ZaU8pl7) ## Analytics Campaign results and key metrics are available in the Connectly platform under **Analytics**. Select your campaign and timeframe to view delivery, engagement, and performance data. Connectly Analytics dashboard showing campaign metrics # MoEngage (Alpha) Source: https://docs.connectly.ai/integrations/mo-engage-alpha Send WhatsApp campaign messages triggered from MoEngage via the Connectly API. Currently in Alpha 🧪 The MoEngage integration is currently in **Alpha**. Full documentation is coming soon. Contact your Connectly Account Manager to join the Alpha programme and get setup instructions. The MoEngage integration allows you to trigger WhatsApp campaign messages from MoEngage using Connectly's messaging infrastructure. Once enrolled in the Alpha, Connectly will provide you with the endpoint details and configuration instructions. # Integrations Overview Source: https://docs.connectly.ai/integrations/overview Connect Connectly with your existing CRM, marketing automation, and customer engagement tools ⚒️ Connectly integrates with popular customer engagement and CRM platforms, letting you route WhatsApp conversations directly into the tools your team already uses. ## Available integrations Send WhatsApp campaigns and automated flows from WebEngage using Connectly as your messaging channel. Route inbound WhatsApp conversations to Zendesk tickets and reply directly from the Zendesk interface. Mirror WhatsApp conversations in Intercom and send replies from your Intercom inbox via Make/Integromat. Send WhatsApp campaign messages triggered from MoEngage. Currently in Alpha. Trigger WhatsApp campaigns from Klaviyo flows using the Connectly API. *** ## Scripts The Campaign Builder also supports custom JavaScript scripts — useful for fetching external APIs, applying conditional logic, and writing data back to session variables within a flow. See [Tools](/integrations/tools) for the full reference. *** All integrations require a fully onboarded Connectly account with a working WhatsApp business number. Contact your Account Manager to enable any integration for your account. # Tools Source: https://docs.connectly.ai/integrations/tools Create standalone JavaScript tools that can be versioned, tested, and used as WhatsApp Flow endpoints — directly from your campaign flows 📓 Tools are JavaScript functions you create and manage in the **Tools** section of Connectly (`/tools`). Each tool is versioned, deployable, and reusable across multiple flows. Tools also automatically receive a public HTTPS URL you can use as a WhatsApp Flow data-exchange endpoint, with end-to-end encryption handled by Connectly. ## Tools section The Tools section has four tabs: | Tab | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Tools** | Your personal tool library. Each tool has a code editor, version selector, deploy button, and a built-in test panel. Tools you create here are private to your account unless explicitly shared. | | **Shared Tools** | Tools shared with your business by Connectly or other team members. Shared tools can be referenced in any flow without copying the code. | | **Executions** | A filterable log of every tool call across your business. Use this for debugging and monitoring — see [Monitoring executions](#monitoring-executions) below. | | **Integrations** | Pre-built integrations that expose external services as callable operations inside your tools. For example, the **Commerce Platform** integration provides e-commerce operations (checkout, cart, orders, and more) that your tool code can invoke directly, without manually writing HTTP calls. Each integration shows the number of available operations and how many are required to configure it. | ## Tool structure Every tool exports a single `onExecute` function: ```javascript theme={null} export async function onExecute(input, config) { const { id } = input.sessionContext.variables; const resp = await fetch(`https://api.example.com/validate?id=${id}`, { headers: { Authorization: `Bearer ${config.secrets.API_KEY}` } }); const result = await resp.json(); return [{ valid: result.valid }, 'SUCCESS']; } ``` ## Parameters ### `input` Contains the current session context: ```json theme={null} { "sessionContext": { "business": { "id": "..." }, "customer": { "phoneNumber": "..." }, "variables": { "VAR1": "1" } } } ``` | Field | Description | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `sessionContext.business.id` | Your Connectly business ID. | | `sessionContext.customer.phoneNumber` | The current customer's phone number. | | `sessionContext.variables` | All session variables set so far in the flow, including any variables passed when the campaign was triggered. | **Campaign input variables** — Variables sent with a campaign (e.g. `name`, `id`) are automatically available in `input.sessionContext.variables`. No extra setup required. ```javascript theme={null} const { name, id } = input.sessionContext.variables; ``` Example `sessionContext.variables` when a campaign is sent with custom variables: ```json theme={null} { "channel_type": "whatsapp_cloud", "external_id": "+5521986260652", "name": "Maria", "id": "123456789" } ``` ### `config` Contains secrets configured for the tool: ```json theme={null} { "secrets": { "SECRET1": "value" } } ``` ## Return value Tools must return a tuple `[data, outcome]`: ```javascript theme={null} return [{ FIELD1: 'one', FIELD2: 42 }, 'OUTCOME2'] ``` | Return value | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | object | JSON object — any fields you want to save to session variables. | | `outcome` | string | A string that maps to an outgoing edge in the flow. Each possible outcome must be configured as an edge in the Campaign Builder. There is no default or fallback outcome. | ## Versioning and deployment Tools support versioning. Use the version selector in the editor header to manage releases, and click **Deploy** to publish a new version. Previous versions remain available for rollback. ## Testing tools Each tool has a built-in **Test** panel on the right side of the editor. Paste a mock JSON payload into the input field and click **Run Test** to execute the tool and inspect the output and logs without triggering an actual campaign. ## Tool URL as a WhatsApp Flow endpoint Every tool is automatically assigned a public URL: ```text theme={null} https://app.connectly.ai/tools/{tool-id} ``` You can use this URL directly as the **data-exchange endpoint** for a WhatsApp Flow. Connectly handles E2E encryption transparently — no key management required in your tool code. ### WhatsApp Flow handler example WhatsApp Flows send an `init` action first, then `data_exchange` actions as the user navigates screens. Route on `action` and `screen`: ```javascript theme={null} export async function onExecute(input, config) { const { action, screen, data } = input; // Respond to the initial ping with the first screen if (action !== 'data_exchange') { return [{ screen: 'FIRST_SCREEN', data: {} }, 'SUCCESS']; } // Route by the screen that submitted the data exchange switch (screen) { case 'SCREEN_PERSONAL': case 'SCREEN_DOC': return [await handleDocScreen(data, config), 'SUCCESS']; default: return [ { screen: '', data: { error_message: 'Unknown screen: ' + screen } }, 'ERROR', ]; } } ``` See [WhatsApp Flow endpoints](/messaging/whatsapp-flow-endpoints) for the full data-exchange protocol. ## Monitoring executions The **Executions** tab shows a log of every tool call with filters for: * **Tool** — narrow to a specific tool * **Status** — filter by success or error * **Time range** — 15 min, 30 min, 1h, 6h, 1d, 2d, 3d, 7d, 15d, or custom * **Execution ID / Session ID / Room ID** — look up a specific call from a campaign run ## Using a tool in a flow In the Campaign Builder, add a **Tool node** and select the tool you want to call. Map the tool's `data` output fields to session variables for use in downstream nodes. | Raw param | Maps to session variable | | ------------ | --------------------------------------------- | | `outcome` | Any variable you configure (e.g. `VARX`) | | `data` | Any variable (receives the whole data object) | | `data.FOO.1` | A specific nested field (e.g. `VAR_FOO1`) | # WebEngage Source: https://docs.connectly.ai/integrations/webengage Send WhatsApp campaigns and automated messaging flows from WebEngage using Connectly as your WhatsApp channel 🔗 WebEngage is a marketing automation and customer engagement platform. With this integration, you can trigger WhatsApp messages through Connectly directly from WebEngage campaigns and automation flows. ## Prerequisites * A fully onboarded Connectly account with a working WhatsApp business number. * Contact your Account Manager to enable WebEngage integration permissions for your business ID before starting setup. You'll need three values from Connectly: * **Business ID** * **API key** * **WhatsApp phone number** *** ## Step 1: Set up the integration in WebEngage Go to your WebEngage dashboard and navigate to **Integrations**. Select **WhatsApp** from the channel list and choose **Private WhatsApp integration**. On the configuration page, provide your **Business ID**, **API key**, and **WhatsApp phone number**. Set the endpoint URL to: ```text theme={null} https://api.connectly.ai/proxy/businesses//send_whatsapp_templated_message ``` Replace `` with your Connectly business ID. *** ## Step 2: Configure delivery status webhooks To receive delivery analytics back in WebEngage, register a Connectly webhook using the [Create webhook](/webhooks/create-webhook) endpoint with the following body: You'll need a WebEngage token — contact your WebEngage account manager or reach them at [webengage.com/contact-us](https://webengage.com/contact-us/). ```json theme={null} { "topic": "messages", "address": "http://wt.webengage.com/tracking/events", "type": "webengage", "configuration": { "webengageConfiguration": { "timezone": "", "webengage_token": "", "webengage_webhook_url": "http://wt.webengage.com/tracking/events" } } } ``` Replace `` with your timezone in `Area/City` format (e.g. `Asia/Riyadh`) and `` with the token from your WebEngage account manager. # Zendesk Source: https://docs.connectly.ai/integrations/zendesk Route inbound WhatsApp conversations to Zendesk tickets and reply to customers directly from the Zendesk interface 🖇️ With the Zendesk integration, inbound WhatsApp messages from your customers are surfaced as tickets in Zendesk. When your team replies to a ticket, the customer receives a WhatsApp message with the reply — keeping the entire conversation in one place. ## Prerequisites * A fully onboarded Connectly account with a working WhatsApp business number. * A Zendesk account with API access. You must contact your Account Manager to request Zendesk integration access before starting setup. The integration will not work without this step. *** ## Setup Follow [Zendesk's guide to generating an API token](https://support.zendesk.com/hc/en-us/articles/4408889192858-Generating-a-new-API-token). This token gives Connectly read access to your Zendesk ticket data only. Your Zendesk domain is in the format `COMPANY.zendesk.com`. See [this guide](https://support.zendesk.com/hc/en-us/articles/4409381383578-Where-can-I-find-my-Zendesk-subdomain-) if you're unsure where to find it. Email your Account Manager with the following: * Zendesk API key * Zendesk domain (e.g. `yourcompany.zendesk.com`) * WhatsApp welcome message contents * Zendesk ticket subject line * Zendesk first ticket comment text Connectly will complete the integration setup on your behalf. *** ## What Connectly can access | Resource | Access | | ------------------------------- | --------------------------------------------------------------------------- | | Zendesk ticket data | Read only — scoped to the API token you provide | | WhatsApp account & phone number | Full management via Meta — messages, WABA info, delivery stats, and billing | **Security & privacy:** All customer data is encrypted at rest and in transit. See [Connectly's privacy policy](https://www.connectly.ai/privacypolicy) for full details. # Introduction Source: https://docs.connectly.ai/introduction Welcome to our developer documentation portal 👋 Connectly is a WhatsApp Business messaging platform that lets your business send and receive messages at scale — from one-off notifications to million-recipient campaigns. Whether you need to run marketing campaigns, deliver transactional notifications, automate chatbot conversations, onboard new customers, send reminders, or dispatch OTP codes, Connectly gives you the infrastructure and tooling to do it reliably and securely. *** ## Why Connectly? Connect via REST in minutes using your API key. No low-level protocol work, no hosting overhead — Connectly manages the WhatsApp Cloud API connection for you. Messaging endpoints support **200 requests/second** with burst capacity up to **1,000 requests/second** — enough headroom for large campaign send-outs. All messages travel over WhatsApp's encrypted transport. Customer data stays private in transit without any extra configuration on your end. Send images, video, audio, documents, location pins, interactive list messages, reply buttons, and carousels — not just plain text. Drop Sofia AI or a custom Agent Graph bot into any conversation flow to handle inbound questions, qualify leads, and hand off to human agents automatically. Connectly's engineering and support teams are available around the clock across every time zone to help you resolve issues and optimise delivery rates. *** ## Key use cases | Use case | Description | | ------------------------ | ----------------------------------------------------------------------------------------- | | **Customer engagement** | Start two-way conversations with customers on the channel they already use every day. | | **Notifications** | Deliver order confirmations, shipping updates, and account alerts via approved templates. | | **Chatbots** | Build automated conversation flows that respond to inbound session messages in real time. | | **Onboarding** | Guide new users through sign-up steps with structured, interactive messages. | | **Reminders** | Send appointment, payment, or renewal reminders before they fall through the cracks. | | **OTP & authentication** | Deliver one-time passwords and verification codes securely over WhatsApp. | *** ## How it works Every request is authenticated with an `X-API-Key` header scoped to your business account. Generate one from the Connectly Dashboard at any time. Use the **template message endpoint** to initiate a conversation with a customer. Templates are pre-approved by Meta, so they can be sent at any time — no prior interaction needed. Once a customer messages you back, a 24-hour window opens. During that window, use the **session message endpoint** to send free-form replies — no template required. Subscribe to Connectly webhooks to receive real-time events: delivery status updates, inbound customer messages, button clicks, and more. *** ## Get started Send your first WhatsApp message in under 5 minutes. Full reference for template and session messages. Receive delivery status and inbound message events. # Business-Scoped User IDs (BSUID) Source: https://docs.connectly.ai/messaging/bsuid How Connectly surfaces WhatsApp BSUIDs for customers who adopt a username, and how to send messages to them 📥 WhatsApp is rolling out usernames. When a customer adopts one, WhatsApp may stop sharing their phone number with businesses. In that case, Meta identifies the customer with a **Business-Scoped User ID (BSUID)** — a stable, per-business identifier — instead of a phone number. Connectly surfaces BSUIDs in webhooks and lets you use them when sending, so you don't lose the conversation when a customer goes phone-less. BSUID support is **off by default** and is configured per WhatsApp number. There is no self-serve toggle — contact your Account Manager or Connectly support to enable it for specific numbers. See [Enabling BSUID support](#enabling-bsuid-support). *** ## What is a BSUID? * **Format** — an ISO 3166 alpha-2 country code, a period, then up to 128 alphanumeric characters. Example: `US.13491208655302741918`. Parent BSUIDs (for businesses enrolled in Meta's multi-portfolio program) carry an `ENT` segment: `US.ENT.11815799212886844830`. * **Business-scoped** — a BSUID only works with WhatsApp numbers your business owns. You cannot message another business's customer using their BSUID. * **Can change** — regenerated if the customer changes their phone number. Always re-key on the newest `userId` value from webhooks. * **Phone may still be present** — adopting a username doesn't always hide the phone number. If you've interacted with the customer recently, Meta may still send both. *** ## Enabling BSUID support Support is configured per WhatsApp number, so you can accept phone-less customers on a marketing line while keeping a support line phone-only. To enable it, contact your Account Manager or Connectly support and specify which WhatsApp number(s) should accept username-only customers. **While a number is not enabled:** * Inbound messages from phone-less customers appear in the Connectly inbox, but **no webhooks are delivered** to your endpoints for them — from your integration's point of view the conversation is invisible. * Customers who still have a visible phone number are completely unaffected. *** ## Receiving BSUIDs in webhooks Once enabled, the customer identifier in webhook payloads gains two new fields — `userId` and `phoneNumber` — alongside the existing `id`. This applies wherever a customer appears: as `sender` on inbound message webhooks and as `recipient` on delivery-status webhooks. | Customer type | `id` | `userId` | `phoneNumber` | | -------------------------- | ----- | -------- | ------------- | | Phone-less (username only) | BSUID | BSUID | `""` | | Phone + BSUID | BSUID | BSUID | phone | | Legacy phone-only | phone | `""` | phone | All three fields are always present — empty values are returned as `""` (and `name` as `null`), never omitted. The `id` field always holds a usable identifier: the BSUID when Meta has shared one, otherwise the phone number. **Example — delivery-status webhook for a phone-less customer:** ```json theme={null} "recipient": { "id": "US.13491208655302741918", "channelType": "whatsapp", "name": "John Snow", "userId": "US.13491208655302741918", "phoneNumber": "" } ``` Store the `userId` value against your customer record. It's the only stable identifier for customers who have hidden their phone number. *** ## Sending messages to a BSUID One rule for every endpoint: **copy the BSUID exactly as you received it in the webhook, and put it in the `userId` field.** | Endpoint | How to pass the BSUID | | ----------------------------------------- | --------------------------------------------------------------------- | | `POST …/send/messages` (session message) | Set `recipient.userId` to the BSUID (e.g. `US.13491208655302741918`). | | `POST …/send/whatsapp_templated_messages` | Set `userId` to the BSUID. | | `POST …/send/campaigns` | Set the entry's `userId` field to the BSUID. | The legacy identifier fields (`recipient.id`, `number`, `client`) also accept a BSUID — bare or `bsuid:`-prefixed — and take precedence over `userId` when both are set. No prefix is required anywhere; `bsuid:` is a legacy form that stays supported for existing integrations. Parent BSUIDs (`US.ENT.…`) are accepted wherever a BSUID is. Sending to phone-less (BSUID-only) customers is **generally available**. A value that looks like a BSUID but isn't valid (wrong characters, missing country code) is rejected with a `400` and a BSUID-specific error message. **Reply with the same identifier you received.** Always use the `userId` from the webhook. If your business is enrolled in Meta's parent-BSUID program you also receive `parentUserId` — pick one of the two and use it consistently for each customer. Each identifier creates its own conversation thread in Connectly, so mixing them splits one customer's history in two. **Authentication templates cannot use BSUIDs.** One-tap, zero-tap, and copy-code authentication templates require a phone number. This is a permanent restriction from Meta. *** ## Things to keep in mind * **Keep handling phone numbers.** Most customers keep their phone number visible. BSUIDs are additive — when both are available, you receive both. * **BSUIDs can change** when a customer changes their phone number. Always re-key your records on the newest `userId` from webhooks. * **BSUIDs are portfolio-scoped** — they only work with WhatsApp numbers your business owns. # Error Codes Source: https://docs.connectly.ai/messaging/error-codes Reference for all Connectly API error types and error codes — structure, causes, and how to handle them ❌ When a Connectly API call fails, the response body contains a structured JSON object that tells you exactly what went wrong. ## Error response structure ```json theme={null} { "message": "Failed to send a message because more than 24 hours have passed since the customer last replied", "type": "ERROR_TYPE_DEADLINE_EXCEEDED", "code": "ERROR_CODE_MESSAGE_OUTSIDE_OF_ELIGIBILITY_WINDOW", "userTitle": "Failed to send a message because more than 24 hours have passed since the customer last replied", "userMessage": "Pass along the connectly trace id 'cnct_trace_id' to the team for more information.", "cntTraceId": "11824978358485860716", "details": {} } ``` | Field | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `message` | Human-readable description of the error. | | `type` | Broad error category. See [Error types](#error-types) below. | | `code` | Specific error code identifying the exact failure. See [Error codes](#error-codes) below. | | `userTitle` | Expanded error description suitable for display. | | `userMessage` | Troubleshooting instructions. | | `cntTraceId` | Connectly trace ID. Include this when filing a support request — it lets Connectly engineers locate the exact failed request. | | `details` | Additional structured detail, when available. | *** ## Error codes | Code | Cause | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ERROR_CODE_INVALID_SENDER_ID` | The phone number used as the sender is not valid. | | `ERROR_CODE_PHONE_NUMBER_BAD_FORMAT` | The recipient phone number is in an incorrect format. | | `ERROR_CODE_MESSAGE_ATTACHMENTS_NUMBER_INVALID` | Invalid number of attachments. Connectly supports only one attachment per message. | | `ERROR_CODE_MESSAGE_TEXT_EMPTY` | The message `text` field is empty. | | `ERROR_CODE_MESSAGE_TEMPLATE_NOT_FOUND` | No template found matching the specified name and language. Create and approve the template first. | | `ERROR_CODE_MESSAGE_TEMPLATE_INPUT_INVALID` | The template inputs provided are not valid. | | `ERROR_CODE_MESSAGE_ATTACHMENT_EMPTY` | The attachment URL field is empty. | | `ERROR_CODE_MESSAGE_ATTACHMENT_INVALID` | The attachment URL is invalid and cannot be parsed. | | `ERROR_CODE_BUSINESS_MISSES_MATCHING_CHANNEL` | The business account is missing a sender channel of the required channel type. | | `ERROR_CODE_MESSAGE_OUTSIDE_OF_ELIGIBILITY_WINDOW` | More than 24 hours have passed since the customer last messaged you. Use the [template message API](/message-api/send-template-message) instead. | | `ERROR_CODE_BUSINESS_REACHED_LIMITS` | The business has reached messaging restrictions on this channel — too many previous messages were blocked or flagged as spam. | | `ERROR_CODE_MESSAGE_SELF_INVALID` | This message type cannot be sent to yourself. Use a different recipient. | | `ERROR_CODE_MESSAGE_PHONE_NUMBER_INVALID` | The recipient phone number is invalid. | | `ERROR_CODE_MESSAGE_CUSTOMER_OPTED_OUT` | The customer has opted out of receiving template messages. | | `ERROR_CODE_MESSAGE_CUSTOMER_NO_CONVERSATION_INITIATION` | You cannot use the session message API when the customer has not sent the first message. Use the [template message API](/message-api/send-template-message) to reach out first. | | `ERROR_CODE_BUSINESS_KEY_SECRET_ABSENT` | The business API secret has not been initialized. Contact the Connectly team. | | `ERROR_CODE_MESSAGE_TEMPLATE_DELETED` | New language content cannot be added while existing language content is being deleted. Try again in 4 weeks, or create a new template. | | `ERROR_CODE_MESSAGE_TEMPLATE_INVALID_PARAM` | An invalid template parameter was provided. Check that all variables, buttons, and parameters are present and correct. | | `ERROR_CODE_BUSINESS_RATE_LIMIT_REACHED` | The business has exceeded the API rate limit. | *** ## Error types Error types represent broad categories. Use `type` to determine the right corrective action before inspecting the specific `code`. | Type | Meaning | | ------------------------------ | ------------------------------------------------------------------------------------------------------- | | `ERROR_TYPE_UNSPECIFIED` | Unexpected error. Contact the Connectly team with your `cntTraceId`. | | `ERROR_TYPE_AUTHENTICATION` | Invalid authentication credentials — missing or invalid API key. | | `ERROR_TYPE_AUTHORIZATION` | No authorization for the resource — e.g. the API key doesn't have access to the specified `businessId`. | | `ERROR_TYPE_RATE_LIMIT` | API rate limit exceeded. Wait before retrying. | | `ERROR_TYPE_INVALID_REQUEST` | Request payload is not valid. Refer to the API docs and fix the request body. | | `ERROR_TYPE_DEADLINE_EXCEEDED` | Request timed out. Contact the Connectly team with your `cntTraceId`. | | `ERROR_TYPE_NOT_FOUND` | The requested resource does not exist. | | `ERROR_TYPE_CONFLICT` | The resource already exists — use an update operation instead of create. | | `ERROR_TYPE_CONTEXT_CANCLED` | The client cancelled the request. Check your HTTP client timeout settings and increase the limit. | | `ERROR_TYPE_BUSINESS_LIMIT` | The business has reached its account-level limits. | # Messaging Overview Source: https://docs.connectly.ai/messaging/overview Send WhatsApp messages to your customers — template messages for proactive outreach, session messages for real-time replies, and campaigns for bulk flows 📨 The Messaging API is the core of Connectly. It lets you send WhatsApp messages to individual customers and trigger campaign flows to bulk recipient lists. There are two fundamental message types, each suited to a different situation. ## Template messages vs session messages | | Template message | Session message | | ------------------------- | -------------------------------------------- | ----------------------------------------------------- | | **Requires pre-approval** | Yes — approved in Meta's WhatsApp Manager | No | | **24-hour window** | Not restricted | Customer must have messaged you first within 24 hours | | **Use for** | Proactive outreach, notifications, campaigns | Replies, real-time support, conversations | | **Endpoint** | `POST …/send/whatsapp_templated_messages` | `POST …/send/messages` | When in doubt: use a **template message** to start a conversation, use a **session message** to continue one. ## How it works Build your message template in [Meta's WhatsApp Manager](https://business.facebook.com) and submit it for approval. Once approved, you can reference it by name in the API. See [Template management](/business-management/template-management) for how to create templates via the API. Use [Send template message](/message-api/send-template-message) to reach out to a customer. This works at any time — no prior interaction needed. The response includes a message ID you can use to track delivery. Once the customer replies, a 24-hour session window opens. Use [Send session message](/message-api/send-session-message) to respond with free-form text, media, location pins, interactive lists, reply buttons, or product catalog messages. Subscribe to [webhooks](/webhooks/overview) to receive real-time delivery status events (`sent`, `delivered`, `read`, `delivery_failed`) and inbound message events. ## Message types at a glance **Template messages** support: * Text, image, video, and document headers * Body variables (`body_1` … `body_15`) * URL buttons with dynamic suffixes * Carousel cards with per-card media, body, and buttons * Multi-language translations **Session messages** support: * Plain text * Image, video, audio, and document attachments * Location pins * Interactive list messages * Interactive reply-button messages * Single and multi-product catalog messages ## Endpoints `POST …/send/whatsapp_templated_messages` — initiate contact or reach customers outside the 24-hour window. `POST …/send/messages` — reply within an active session with any content type. Full reference for error types and codes returned by the messaging endpoints. How to send and receive messages for customers identified by BSUID instead of phone number. # Send Session Messages Source: https://docs.connectly.ai/messaging/session-messages POST /v1/businesses/{businessId}/send/messages Send a free-form text, media, location, or interactive message within an active 24-hour WhatsApp session 🗺️ Session messages are free-form messages with no pre-approval required. You can send text, attachments, location pins, and interactive messages (list menus, reply buttons, WhatsApp Flows). The only constraint is WhatsApp's 24-hour eligibility window. ## The 24-hour rule You can only send a session message if the recipient has messaged your business within the last 24 hours. If the window has closed, use [Send template message](/message-api/send-template-message) to re-engage them instead. | Situation | Endpoint to use | | ----------------------------------------- | ----------------------------------------- | | Customer messaged you within 24 hours | `POST …/send/messages` (this endpoint) | | No message from customer in over 24 hours | `POST …/send/whatsapp_templated_messages` | | First-ever contact with a customer | `POST …/send/whatsapp_templated_messages` | *** ## Endpoint ```json theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/send/messages ``` **Rate limit:** 200 requests/second. Exceeding this returns `429 Too Many Requests`. *** ## Request body | Field | Type | Required | Description | | ------------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `recipient` | object | Yes | The customer to send to. | | `recipient.id` | string | Yes\* | Customer's WhatsApp number in E.164 format (e.g. `+16044441111`), or a BSUID — bare (`US.…`, as received in webhooks) or `bsuid:`-prefixed; both are accepted. \*Not required when `recipient.userId` is set. | | `recipient.userId` | string | No | BSUID (e.g. `US.13491208655302741918`), exactly as received in webhooks, to target a customer by BSUID. Only read when `recipient.id` is empty — if both are set, `id` wins. | | `recipient.channelType` | string | Yes | Set to `whatsapp`. | | `recipient.name` | string | No | Customer's display name. | | `message` | object | Yes | The message content. Provide exactly one content type: `text`, `attachments`, `location`, `listMessage`, `replyButtonMessage`, `flowMessage`, `singleProductMessage`, or `multiProductMessage`. | | `message.id` | string | No | Optional message identifier (e.g. `01FPW47QF69Y5JZH24605VAKQB`). | | `message.text` | string | No | Plain text body. Required when sending `multiProductMessage`; optional for `singleProductMessage`; standalone text for all other messages. | | `message.attachments` | array | No | Up to one attachment per message. Each item has `type` (`image`, `video`, `audio`, `document`), `url` (public URL), optional `caption`, and optional `filename` (document only). | | `message.location` | object | No | A location pin. Fields: `latitude`, `longitude`, `name`, `address`. | | `message.listMessage` | object | No | An interactive list message with sections and selectable rows. See [WhatsApp docs](https://developers.facebook.com/docs/whatsapp/guides/interactive-messages#list-messages). | | `message.replyButtonMessage` | object | No | An interactive message with up to 3 quick-reply buttons. See [WhatsApp docs](https://developers.facebook.com/docs/whatsapp/guides/interactive-messages#reply-buttons). | | `message.flowMessage` | object | No | Send a **published** WhatsApp Flow as an interactive message. See [Send WhatsApp Flows](/messaging/whatsapp-flows) for the full payload, `navigate` vs `data_exchange`, and endpoint setup. | | `message.singleProductMessage` | object | No | Send a single product from your Meta catalog. Fields: `footer.text`, `catalogId`, `productItem.productRetailerId`. See [WhatsApp docs](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/sell-products-and-services/share-products). | | `message.multiProductMessage` | object | No | Send multiple catalog products grouped into sections. Fields: `header`, `footer`, `catalogId`, `sections[].title`, `sections[].productItems[].productRetailerId`. See [WhatsApp docs](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/sell-products-and-services/share-products). | | `message.referral` | object | No | Read-only. Populated on inbound messages triggered by a Click-to-WhatsApp ad. Fields: `sourceUrl`, `sourceId`, `sourceType`, `headline`, `body`, `mediaType`, `imageUrl`, `videoUrl`, `thumbnailUrl`, `ctwaClid`. See [WhatsApp docs](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples). | | `message.isEcho` | boolean | No | Set to `true` if the message was sent from the business itself. | | `sender` | object | No | Specifies which of your WhatsApp numbers to send from. Omit if you only have one number. | | `sender.id` | string | No | Your WhatsApp sender number in E.164 format (e.g. `+14151111234`). | | `sender.channelType` | string | No | Set to `whatsapp`. | | `sender.name` | string | No | Display name for the sender. | | `callbackData` | any | No | Up to 1024 bytes of JSON echoed back in webhook events for this message. See [callbackData](#callbackdata). | | `campaignName` | string | No | Tags this message with a campaign name for analytics (e.g. `test_campaign_2022-03-12`). | | `order` | object | No | Optional config to control event dispatching order. | | `order.parentId` | string | No | ID of the parent message this event depends on. | | `order.strategy` | string | No | Dispatch strategy. One of `independent` or `short_circuit`. Defaults to `strategy_unspecified`. | ## Response ```json theme={null} { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` The `id` uniquely identifies the message. Use it to correlate [delivery status webhook events](/message-api/webhook-api). *** ## callbackData Set `callbackData` to any JSON value up to 1024 bytes. Connectly echoes it back in every webhook event tied to this message: * Delivery status events: `sent`, `delivered`, `read`, `delivery_failed` * Inbound replies that reference this message: quoted replies, button & list replies, reactions Plain-text replies that don't quote the original message do **not** include `callbackData`. Exceeding 1024 bytes returns a `400` error. ```json theme={null} { "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Your order is on its way!" }, "callbackData": { "order_id": "12345" } } ``` *** ## Examples ```bash cURL theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses//send/messages \ --header 'Content-Type: application/json' \ --header 'X-API-Key: ' \ --data '{ "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Hello, how can we help you today?" } }' ``` ```python Python theme={null} import requests payload = { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Hello, how can we help you today?" } } response = requests.post( "https://api.connectly.ai/v1/businesses//send/messages", headers={ "Content-Type": "application/json", "X-API-Key": "" }, json=payload ) print(response.json()) ``` ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "attachments": [ { "type": "image", "url": "https://example.com/image.jpg", "caption": "Check this out!" } ] } } ``` ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "attachments": [ { "type": "video", "url": "https://example.com/video.mp4", "caption": "Watch this!" } ] } } ``` ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "attachments": [ { "type": "audio", "url": "https://example.com/audio.mp3" } ] } } ``` ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "attachments": [ { "type": "document", "url": "https://example.com/invoice.pdf", "caption": "Your invoice", "filename": "invoice.pdf" } ] } } ``` ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "location": { "latitude": 37.7749, "longitude": -122.4194, "name": "Connectly HQ", "address": "123 Main St, San Francisco" } } } ``` Use `listMessage` to present a scrollable menu. See [WhatsApp interactive list docs](https://developers.facebook.com/docs/whatsapp/guides/interactive-messages#list-messages) for full spec. ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "listMessage": { "header": { "text": "How can we help?" }, "footer": { "text": "Choose an option below." }, "button": { "text": "Select" }, "sections": [ { "title": "Support", "rows": [ { "id": "1", "text": "Track my order", "description": "Get your order status" }, { "id": "2", "text": "Returns & refunds", "description": "Start a return" } ] }, { "title": "Sales", "rows": [ { "id": "3", "text": "Talk to sales", "description": "Speak with our team" } ] } ] } } } ``` You can also use a media header instead of text. Both list and reply-button messages share the same header structure: ```json theme={null} "header": { "attachment": { "type": "image", "url": "https://cdn.connectly.ai/your-image.png" } } ``` For a document header with a filename: ```json theme={null} "header": { "attachment": { "type": "document", "url": "https://example.com/menu.pdf", "filename": "menu.pdf" } } ``` Use `replyButtonMessage` for up to three quick-reply buttons. See [WhatsApp reply button docs](https://developers.facebook.com/docs/whatsapp/guides/interactive-messages#reply-buttons) for full spec. ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Would you like to confirm your appointment?", "replyButtonMessage": { "header": { "text": "Appointment reminder" }, "footer": { "text": "Tap a button to respond." }, "buttons": [ { "id": "confirm", "text": "Confirm ✓" }, { "id": "reschedule", "text": "Reschedule" }, { "id": "cancel", "text": "Cancel" } ] } } } ``` Send a **published** WhatsApp Flow with `flowMessage`. The body comes from the top-level `text`. This example uses `navigate` mode; for `data_exchange`, the full field reference, and endpoint setup, see [Send WhatsApp Flows](/messaging/whatsapp-flows). ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Tap below to book your appointment", "flowMessage": { "flowId": "1234567890123456", "flowToken": "appointment-000123", "flowCta": "Book appointment", "flowAction": "navigate", "flowMessageVersion": "3", "flowActionPayload": { "screen": "APPOINTMENT" } } } } ``` Include the `sender` object when your business has more than one WhatsApp number. ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Hello from our support line!" } } ``` *** ## Targeting by BSUID To target a customer identified by a Business-Scoped User ID, set `recipient.userId` to the BSUID exactly as received in webhooks (e.g. `US.13491208655302741918`) and leave `recipient.id` out. `recipient.id` also accepts a BSUID (bare or `bsuid:`-prefixed) and takes precedence over `recipient.userId` whenever it is set. See [Business-scoped user IDs (BSUID)](/message-api/bsuid) for full details. Reply with the same identifier you received in the webhook. If you receive both `userId` and `parentUserId`, pick one and use it consistently — each identifier keeps its own conversation thread, so mixing them splits the customer's history in two. *** ## Error responses | Status | Meaning | | ------ | ----------------------------------------------------------------------------- | | `400` | Malformed body, session window expired, or `callbackData` exceeds 1024 bytes. | | `401` | Missing or invalid `X-API-Key`. | | `429` | Rate limit exceeded (200 req/s). | | `500` | Internal server error. | See [Error codes](/message-api/error-codes) for the full list of error types and codes, including `ERROR_CODE_MESSAGE_OUTSIDE_OF_ELIGIBILITY_WINDOW`. # Send Template Messages Source: https://docs.connectly.ai/messaging/template-messages POST /v1/businesses/{businessId}/send/whatsapp_templated_messages Send a pre-approved WhatsApp template to any customer, including those outside the 24-hour session window ✅ Template messages are pre-approved formats managed in your WhatsApp Manager (inside Facebook Business Manager). Because Meta approves them in advance, you can send them to any customer at any time, including first-time contacts and customers whose 24-hour session has expired. Your template must be created and approved in [WhatsApp Manager](https://business.facebook.com) before you can reference it here. Contact the Connectly team if you need help getting a template approved. Template messages can only be sent to **personal WhatsApp accounts**, not to WhatsApp API (business) accounts. When testing, use your personal WhatsApp number and make sure it has accepted all WhatsApp app and privacy updates. ## Endpoint ```text theme={null} POST /v1/businesses/{businessId}/send/whatsapp_templated_messages ``` ```json theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/send/whatsapp_templated_messages ``` **Rate limit:** 200 requests/second. Exceeding this returns `429 Too Many Requests`. *** ## Request body | Field | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `number` | string | Yes\* | Recipient's WhatsApp number in E.164 format (e.g. `+16502223333`), or a BSUID — bare (`US.…`, as received in webhooks) or `bsuid:`-prefixed. \*Not required when `userId` is set. | | `userId` | string | No | BSUID (e.g. `US.13491208655302741918`), exactly as received in webhooks, to target a customer by BSUID. Only read when `number` is empty — if both are set, `number` wins. | | `templateName` | string | Yes | Exact name of the approved template in your WhatsApp Manager. | | `language` | string | Yes | Language code for the template translation (e.g. `en`, `en_US`, `pt_BR`, `es`). The template must have an approved translation for this language. | | `parameters` | array | No | Variable substitution objects. See [Template parameters](#template-parameters) below. | | `sender` | string | No | Your WhatsApp sender number in E.164 format. Only required when you have more than one WhatsApp number registered with Connectly. | | `callbackData` | any | No | Up to 1024 bytes of JSON echoed back in webhook events for this message. See [callbackData](#callbackdata). | | `campaignName` | string | No | Tags this send with a campaign name for analytics. | ## Response ```json theme={null} { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` The `id` uniquely identifies the message. Use it to correlate [delivery status webhook events](/message-api/webhook-api) (`sent`, `delivered`, `read`, `delivery_failed`). *** ## Template parameters The `parameters` array substitutes variables in your template's header, body, and buttons. Each object has a `name` and `value`, plus an optional `filename` for document headers. ```json theme={null} "parameters": [ { "name": "header_document", "value": "https://example.com/invoice.pdf", "filename": "invoice.pdf" }, { "name": "body_1", "value": "John" }, { "name": "body_2", "value": "Order #98765" } ] ``` **Available `name` values:** | Category | Name | Description | | -------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Header | `header_text` | Substitutes a text variable in the template header. | | Header | `header_image` | Sets a header image. `value` must be a public image URL. | | Header | `header_document` | Sets a header document. `value` must be a public document URL. Pair with `filename` to control the displayed file name. | | Body | `body_1` … `body_15` | Substitutes the 1st through 15th variable in the template body, in order. | | Buttons | `button_1_url_suffix` | Appended to the base URL of button 1. | | Buttons | `button_2_url_suffix` | Appended to the base URL of button 2. | | Buttons | `button_3_url_suffix` | Appended to the base URL of button 3. | | Buttons | `button_N_custom_wa_flow_action_data` | JSON string with the initial screen data for a [WhatsApp Flow](/messaging/whatsapp-flows) button at position N. Optional; only if the flow's entry screen needs starting data. | | Carousel | `card.N.header.image` | Image URL for carousel card N (zero-indexed). | | Carousel | `card.N.body.M` | Body variable M of carousel card N (both zero-indexed). | | Carousel | `card.N.button.M.urlSuffix` | URL suffix for button M of carousel card N. | *** ## callbackData Set `callbackData` to any JSON value up to 1024 bytes. Connectly echoes it back in every webhook event tied to this message: * Delivery status events: `sent`, `delivered`, `read`, `delivery_failed` * Inbound replies that reference this message: quoted replies, button & list replies, reactions Plain-text replies that don't quote the original message do **not** include `callbackData`. Exceeding 1024 bytes returns a `400` error. ```json theme={null} { "number": "+16044441234", "templateName": "order_confirmation", "language": "en", "parameters": [{ "name": "body_1", "value": "John" }], "callbackData": { "order_id": "12345" } } ``` *** ## Examples ```json theme={null} { "number": "+16044441234", "templateName": "my_template_name", "language": "en", "parameters": [ { "name": "header_text", "value": "Hello" }, { "name": "body_1", "value": "John" }, { "name": "body_2", "value": "red" } ] } ``` ```json theme={null} { "number": "+16044441234", "templateName": "external_header_test2", "language": "pt_BR", "parameters": [ { "name": "header_image", "value": "https://i.picsum.photos/id/695/200/300" }, { "name": "body_1", "value": "Connectly.ai" }, { "name": "body_2", "value": "John" }, { "name": "body_3", "value": "a new party" }, { "name": "body_4", "value": "McDonald's" }, { "name": "body_5", "value": "celebrate" } ] } ``` ```json theme={null} { "number": "+16044441234", "templateName": "external_header_document", "language": "en", "parameters": [ { "name": "header_document", "value": "https://example.com/invoice.pdf", "filename": "invoice.pdf" } ] } ``` The `value` is appended to the button's configured base URL, e.g. a base of `www.facebook.com/` becomes `www.facebook.com/connectlyai`. ```json theme={null} { "number": "+16044441234", "templateName": "external_link_button", "language": "es", "parameters": [ { "name": "button_1_url_suffix", "value": "connectlyai" } ] } ``` Create the carousel template first using the [Template Management API](/business-management/template-management), then reference it here with per-card parameters. ```json theme={null} { "number": "+16044441234", "templateName": "carousel_demo_1", "language": "en_US", "parameters": [ { "name": "card.0.header.image", "value": "https://example.com/card1.png" }, { "name": "card.0.body.0", "value": "Summer Sale" }, { "name": "card.0.button.0.urlSuffix", "value": "summer2024" }, { "name": "card.1.header.image", "value": "https://example.com/card2.png" }, { "name": "card.1.body.0", "value": "Up to 50% off" }, { "name": "card.1.button.0.urlSuffix", "value": "sale50" } ] } ``` To receive events when recipients tap carousel buttons, subscribe to the [Webhook API](/message-api/webhook-api). Include `sender` when your business has more than one WhatsApp number registered with Connectly. ```json theme={null} { "sender": "+14151111234", "number": "+16044441111", "templateName": "order_confirmation", "language": "en", "parameters": [] } ``` *** ## Template languages Your template can have multiple approved language translations. Specify the correct code in the `language` field, the template must have an approved translation for the language you request. See [Meta's language list](https://developers.facebook.com/docs/whatsapp/api/messages/message-templates/#message-templates) for all supported codes. | Language | Code | | -------------------- | ------- | | English | `en` | | English (US) | `en_US` | | Brazilian Portuguese | `pt_BR` | | Spanish | `es` | *** ## Targeting by BSUID To target a customer identified by a Business-Scoped User ID, set `userId` to the BSUID exactly as received in webhooks (e.g. `US.13491208655302741918`) and leave `number` out. `number` also accepts a BSUID (bare or `bsuid:`-prefixed) and takes precedence over `userId` whenever it is set. See [Business-scoped user IDs (BSUID)](/message-api/bsuid) for full details. Authentication templates (one-tap, zero-tap, copy-code) cannot use BSUIDs — they require a phone number. This is a permanent Meta restriction. *** ## Error responses | Status | Meaning | | ------ | -------------------------------------------------------------------------- | | `400` | Malformed body, unapproved template, or `callbackData` exceeds 1024 bytes. | | `401` | Missing or invalid `X-API-Key`. | | `429` | Rate limit exceeded (200 req/s). | | `500` | Internal server error. | See [Error codes](/message-api/error-codes) for the full list of error types and codes. # WhatsApp Flow Endpoints Source: https://docs.connectly.ai/messaging/whatsapp-flow-endpoints Implement the data-exchange endpoint that powers dynamic WhatsApp Flows A static flow defines all of its screens up front in the Flow JSON. A **dynamic** flow builds screens at runtime: when the customer opens the flow or submits a screen, WhatsApp calls an HTTPS endpoint you host, and your endpoint answers with the next screen and its data. Use it when screen content depends on live information — available time slots, account details, product lists. This page covers the contract your endpoint implements. For sending flows through the API, see [Send WhatsApp Flows](/messaging/whatsapp-flows). WhatsApp calls your endpoint directly — the traffic does not pass through Connectly. You configure the endpoint URL and its encryption key on the flow in WhatsApp Manager. Meta's reference documentation, including sample endpoint implementations, is at [Implementing your Flow endpoint](https://developers.facebook.com/docs/whatsapp/flows/guides/implementingyourflowendpoint). ## Request lifecycle Your endpoint receives one `POST` per event: | When | `action` | Your endpoint returns | | ---------------------------------------------- | --------------- | ------------------------------------ | | Periodic health check | `ping` | `{ "data": { "status": "active" } }` | | Customer opens the flow (`data_exchange` mode) | `INIT` | The first screen | | Customer submits a screen | `data_exchange` | The next screen and its data | | Customer taps back | `BACK` | The screen to show | A completed flow (a screen whose action is `complete`) does not call your endpoint. The customer's answers are delivered as a flow response message — see [Webhooks](/webhooks/overview). ## Encryption Every request body carries three base64 fields: ```json theme={null} { "encrypted_flow_data": "…", "encrypted_aes_key": "…", "initial_vector": "…" } ``` **To decrypt the request:** 1. Decrypt `encrypted_aes_key` with your RSA private key, using OAEP padding with SHA-256. The result is a 128-bit AES key. 2. Decrypt `encrypted_flow_data` with AES-128-GCM using that key and `initial_vector`. The last 16 bytes of the ciphertext are the GCM authentication tag. 3. The plaintext is the JSON request. **To encrypt your response:** 1. Flip every byte of the request's `initial_vector` (XOR each byte with `0xFF`). 2. Encrypt your response JSON with AES-128-GCM using the **same AES key** from the request and the flipped IV. Append the 16-byte authentication tag to the ciphertext. 3. Return the result base64-encoded as the raw response body — plain text, not JSON — with HTTP status `200`. If you cannot decrypt a request (for example, after rotating keys), return HTTP `421`. WhatsApp then re-fetches your public key and retries. The private key stays on your server. Its public key is registered on your WhatsApp phone number — one key pair per phone number. If you generate a new key pair, the public key must be registered again before your endpoint can decrypt traffic. ## Requests and responses The decrypted request: ```json theme={null} { "version": "3.0", "action": "data_exchange", "screen": "SELECT_SERVICE", "data": { "service": "haircut" }, "flow_token": "ref-000123" } ``` Your decrypted response — the next screen and the data it needs: ```json theme={null} { "version": "3.0", "screen": "SELECT_TIME", "data": { "time_slots": [ { "id": "0900", "title": "9:00" }, { "id": "1030", "title": "10:30" }, { "id": "1500", "title": "15:00" } ] } } ``` Every `screen` your endpoint returns must exist in the **published** Flow JSON. Returning a screen name the flow does not contain is the most common endpoint bug: the customer sees "Something went wrong" when the flow opens, while your logs show successful `200` responses. Keep your Flow JSON and endpoint in sync — when you rename or add screens, update both. ## Dynamic screen data List components in the Flow JSON (`RadioButtonsGroup`, `CheckboxGroup`, `Dropdown`) bind to data your endpoint returns: ```json theme={null} { "type": "RadioButtonsGroup", "name": "slot", "data-source": "${data.time_slots}" } ``` The number of options is the length of the array you return — 2 items render 2 options, 5 items render 5. Each item needs a string `id` and a `title`; optional fields include `description` and `enabled`. Limits: 20 items for radio buttons and checkboxes, 200 for a dropdown. To personalize from the very first screen, use the `flowToken` you set when [sending the flow](/messaging/whatsapp-flows#flow_token): it is echoed to your endpoint on every request, including `INIT`, so it can carry a customer reference your endpoint resolves before building the first screen. ## Example endpoint A complete Node.js implementation of the contract, with the crypto and a two-screen booking handler. The same structure ports directly to other languages. ```javascript theme={null} import crypto from "node:crypto"; import http from "node:http"; import fs from "node:fs"; const PRIVATE_KEY = fs.readFileSync("./private_key.pem", "utf8"); function decryptRequest(body) { const aesKey = crypto.privateDecrypt( { key: PRIVATE_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" }, Buffer.from(body.encrypted_aes_key, "base64"), ); const iv = Buffer.from(body.initial_vector, "base64"); const flowData = Buffer.from(body.encrypted_flow_data, "base64"); const tag = flowData.subarray(flowData.length - 16); const ciphertext = flowData.subarray(0, flowData.length - 16); const decipher = crypto.createDecipheriv("aes-128-gcm", aesKey, iv, { authTagLength: 16 }); decipher.setAuthTag(tag); const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return { payload: JSON.parse(plain.toString("utf8")), aesKey, iv }; } function encryptResponse(responseObj, aesKey, iv) { const flippedIV = Buffer.from(Uint8Array.from(iv, (b) => b ^ 0xff)); const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, flippedIV, { authTagLength: 16 }); const enc = Buffer.concat([cipher.update(JSON.stringify(responseObj), "utf8"), cipher.final()]); return Buffer.concat([enc, cipher.getAuthTag()]).toString("base64"); } async function nextScreen({ action, screen, data, flow_token }) { if (action === "ping") return { data: { status: "active" } }; if (action === "INIT") return { version: "3.0", screen: "SELECT_SERVICE", data: {} }; if (action === "data_exchange" && screen === "SELECT_SERVICE") { const slots = await fetchAvailableSlots(data.service); // your business logic return { version: "3.0", screen: "SELECT_TIME", data: { time_slots: slots.map((s) => ({ id: String(s.id), title: s.label })) } }; } return { version: "3.0", screen: "SELECT_SERVICE", data: {} }; } http.createServer((req, res) => { let raw = ""; req.on("data", (c) => (raw += c)); req.on("end", async () => { try { const { payload, aesKey, iv } = decryptRequest(JSON.parse(raw)); console.log(`in: action=${payload.action} screen=${payload.screen ?? "-"}`); const response = await nextScreen(payload); res.writeHead(200, { "Content-Type": "text/plain" }); res.end(encryptResponse(response, aesKey, iv)); } catch (e) { res.writeHead(421); res.end(); } }); }).listen(3000); ``` ## Production checklist * **Respond fast.** WhatsApp allows only a few seconds per request. Cache slow upstream calls; keep the endpoint close to your data. * **Use a stable HTTPS URL.** Tunnel tools such as ngrok are fine for development, but free tunnels change their URL on every restart — and the flow keeps calling the old one. Host production endpoints on a fixed domain. * **Log every exchange.** Log the incoming `action` and `screen` and the `screen` you return. Most flow issues are diagnosed from exactly these two lines. * **Return `421` on decryption failures** so WhatsApp refreshes your public key instead of retrying blindly. ## Troubleshooting | Symptom | Likely cause | Fix | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | "Something went wrong" when the flow opens | Endpoint unreachable, or `INIT` returns a screen that is not in the published flow | Confirm the endpoint URL on the flow matches your current URL; check the screen name your `INIT` handler returns | | Flow opens, but a specific screen fails | That screen's `data_exchange` handler errors or returns an unknown screen | Check your logs for that screen's request; verify the returned screen name and data shape | | Your logs show `200` everywhere, but the flow still errors | Response encryption is wrong (IV not flipped, tag missing, or body wrapped in JSON), or the returned screen name does not exist in the flow | Verify the encrypt steps above; return the base64 string as a plain-text body | | Every request fails to decrypt | Your private key does not match the public key registered on the phone number | Re-register the current public key, or restore the matching private key; return `421` meanwhile | | Options list renders empty | The bound array is missing from your response data, or item `id`s are not strings | Return the array under the exact property name the screen declares; stringify ids | | Worked yesterday, fails today with no code change | Your endpoint URL changed (common with tunnels) | Update the endpoint URL on the flow, or move to a stable URL | # WhatsApp Flows Source: https://docs.connectly.ai/messaging/whatsapp-flows What WhatsApp Flows are, and how to send one through the API A [WhatsApp Flow](https://developers.facebook.com/docs/whatsapp/flows) is a structured, multi-screen experience that runs inside WhatsApp. The customer taps a button and steps through screens with inputs like text fields, dropdowns, date pickers, and checkboxes, then submits, all without leaving the chat. You design and publish a flow in WhatsApp Manager, then reference it by its `flowId`. This page covers how to send one through the API. ## Sending a flow A flow is delivered as a message whose single call-to-action button opens the flow. Send it with `message.flowMessage`: ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses//send/messages" \ -H "X-API-Key: " -H "Content-Type: application/json" \ -d '{ "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Complete this quick form to get started", "flowMessage": { "flowId": "1234567890123456", "flowToken": "ref-000123", "flowCta": "Get started", "flowAction": "data_exchange", "flowMessageVersion": "3" } } }' ``` | Field | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `flowId` / `flowName` | Identifier or name of your flow. Provide exactly one. | | `flowToken` | Your own reference string. WhatsApp returns it unchanged in the flow response webhook, so you can match the response to this send. See [flow\_token](#flow_token). | | `flowCta` | Text on the button that opens the flow. | | `flowAction` | `navigate` or `data_exchange`. See [navigate vs data\_exchange](#navigate-vs-data_exchange). | | `flowActionPayload` | Screen and data to start with. Required or forbidden depending on `flowAction`, see below. | | `flowMessageVersion` | WhatsApp Flow message version. Currently `"3"`. | WhatsApp only allows a **pre-approved template** as the first message to a customer you have not been messaging. To reach someone new, put the flow on a template as a flow button, see [Opening a conversation with a template](#opening-a-conversation-with-a-template). ## navigate vs data\_exchange A flow opens in one of two modes. Choose based on how the first screen is built. | | `navigate` | `data_exchange` | | ------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- | | **Who builds the first screen** | You. A static screen already defined in the flow. | Your endpoint, at open time. | | **`flowActionPayload.screen`** | **Required.** Must be the flow's entry screen id. | **Must be omitted.** The endpoint decides the first screen. | | **`flowActionPayload.data`** | Optional. Passed to that screen. | Optional starting data for the endpoint. | | **Endpoint needed to open** | No. Renders on the device. | Yes. WhatsApp calls your endpoint. | | **Use it for** | Static flows, or when you already know the entry screen. | Dynamic flows whose content is fetched live. | These two mistakes return a `400` from the API, instead of a `200` that fails silently at WhatsApp: * `data_exchange` **with** a `flowActionPayload.screen`. The endpoint owns the first screen, so no screen may be specified. * `navigate` **without** a `flowActionPayload.screen`, or omitting `flowAction` (it defaults to `navigate`). Navigate always needs the entry screen. A `navigate` example, opening at a specific screen: ```json theme={null} "flowMessage": { "flowId": "1234567890123456", "flowToken": "ref-000123", "flowCta": "Get started", "flowAction": "navigate", "flowMessageVersion": "3", "flowActionPayload": { "screen": "SIGN_UP" } } ``` `flowActionPayload.data` passes starting values to that screen — useful for pre-filling context the flow should carry, like a phone number or your own session reference: ```json theme={null} "flowActionPayload": { "screen": "SIGN_UP", "data": { "phone": "+16044441111", "session_ref": "1001" } } ``` Every property in `flowActionPayload.data` must be declared in the entry screen's `data` schema in the Flow JSON. To correlate the flow **response** back to a specific send, prefer a unique `flowToken` per send — it is echoed unchanged in the response webhook. The `screen` value must exactly match the flow's entry screen id. WhatsApp rejects any other value. ## Endpoints (data\_exchange) A `data_exchange` flow calls an endpoint you host to build each screen. Your endpoint implements the WhatsApp Flows data-exchange contract: WhatsApp calls it (encrypted) when the flow opens and on each screen submit, and it returns the next screen and its data. See [WhatsApp Flow endpoints](/messaging/whatsapp-flow-endpoints) for the full contract: request lifecycle, encryption, dynamic screen data, a complete example implementation, and troubleshooting. Opening a `data_exchange` flow triggers a call to your endpoint. If it is unreachable or errors, WhatsApp shows "Something went wrong" when the customer opens the flow. The send itself still succeeded; only the open failed. ## flow\_token `flowToken` is any string you generate to identify a send, for example an order or record id. It is not a credential and does not need to be secret. WhatsApp stores it and returns it unchanged in the flow response [webhook](/message-api/webhook-api) when the customer completes the flow, so you can tie the response back to the right customer and context. ## Opening a conversation with a template WhatsApp requires the first message to a customer to be a pre-approved template. To start a flow with someone you have not been messaging, attach the flow to a template as a flow button in WhatsApp Manager, where it is bound to your `flowId`, mode, and entry screen. Then send the template: ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses//send/whatsapp_templated_messages" \ -H "X-API-Key: " -H "Content-Type: application/json" \ -d '{ "number": "+16044441111", "templateName": "your_flow_template", "language": "en", "parameters": [ { "name": "button_1_custom_wa_flow_action_data", "value": "{\"first_name\":\"Sam\"}" } ] }' ``` `button_N_custom_wa_flow_action_data` (where `N` is the flow button's position, 1-based) passes starting data for the first screen as a JSON string. It is optional. See [Send template messages](/messaging/template-messages) for the full parameter reference. # WhatsApp Cost Management Source: https://docs.connectly.ai/resources/cost-management Best practices for managing WhatsApp service message costs under Meta's per-message pricing model 💵 Meta's move to per-message pricing for service messages changes the economics of WhatsApp. Most cost increases are avoidable with the right setup. Here's how to stay in control. Questions about your setup? Reach out to your Connectly Account Manager. *** ## 01 — Use WhatsApp Flows for multi-step interactions **Impact: High · Effort: Medium** Every extra message in a multi-step interaction adds up. WhatsApp Flows let you collect input — forms, selections, booking details — inside a single structured experience, instead of a chain of back-and-forth messages. **With Connectly:** Create your flow in Meta Business Manager and call it from any Connectly campaign. Coming soon: build and host flows without leaving Connectly. *** ## 02 — Cap your auto-reply frequency **Impact: High · Effort: Low** Customers often send several messages at once, or check back multiple times a day. Without a cap, your bot fires a reply to every single one — and each reply is a billable service message. A simple limit ensures you respond once per window, not once per ping. **With Connectly:** Set a frequency cap in your settings (e.g. max one auto-reply per customer per day) regardless of how many messages they send. *** ## 03 — Filter out incoming bots and auto-replies **Impact: High · Effort: Low** Out-of-office replies, delivery bots, and automated notifications can trigger your auto-replies and open agent sessions — just like a real customer conversation would. Those interactions count against your bill. **With Connectly:** A detection algorithm identifies and suppresses incoming bot and auto-reply messages before they trigger any action or cost on your side. *** ## 04 — Consolidate messages **Impact: Medium · Effort: Low** Sending three short messages costs three times as much as one. Combine information into a single well-structured message using buttons, quick replies, or lists — and you'll likely get better engagement too. > **Rule of thumb:** if two messages could be sent within seconds of each other, they should be one message. *** ## 05 — Resolve within the conversation window **Impact: Medium · Effort: Low** When a customer messages you, a conversation window opens. Responding and resolving within that window is more cost-effective than letting it lapse and reopening it with a new outbound message later. **With Connectly:** Use conversation status indicators to see which windows are about to close and prioritise accordingly. *** ## 06 — Keep your contact lists clean **Impact: Medium · Effort: Low** Sending to inactive, unresponsive, or opted-out contacts wastes budget with zero return. Regular list hygiene is one of the simplest cost levers available — and requires no product changes to implement. **With Connectly:** Use engagement data to flag contacts who haven't responded in 60–90 days. Re-engage them with a targeted campaign or remove them from regular sends. *** ## 07 — Build a dedicated AI agent for your most important flows Coming soon **Impact: High · Effort: Medium** Design an AI agent that manages entire conversations end-to-end — understanding intent, gathering details, and handing off to a human only when genuinely needed. Where the other practices optimise individual messages, an agent replaces an entire flow. **Coming to Connectly:** A no-code Agent Builder to design, test, and deploy specialised agents for any flow. # FAQ Source: https://docs.connectly.ai/resources/faq Answers to the most common questions about the Connectly API — messaging, templates, webhooks, campaigns, and account setup 🙋‍♀️ ## Messaging Use `/send/whatsapp_templated_messages` to **initiate** a conversation — when the customer hasn't messaged you before, or their 24-hour session window has expired. Templates are pre-approved by Meta and can be sent at any time. Use `/send/messages` to **reply** within an active session — the customer must have messaged you first within the last 24 hours. Session messages are free-form and don't require pre-approval. See Messaging API overview for a full comparison table. This error means the customer's 24-hour session window has expired. You cannot send a session message to them. Switch to the Send template message endpoint instead — templates bypass the 24-hour restriction. If you have multiple WhatsApp numbers registered with Connectly, our backend may be selecting a different number than the one the customer messaged. Specify the `sender` explicitly in your request: ```json theme={null} { "sender": { "id": "+11234567890", "channelType": "whatsapp" } } ``` This ensures the message is sent from the correct number that has an open session with the customer. Include the optional `sender` object in your request body with your chosen phone number in E.164 format. This works on both the session message and template message endpoints: **Session message:** ```json theme={null} { "sender": { "id": "+14151111234", "channelType": "whatsapp" }, "recipient": { "id": "+16044441111", "channelType": "whatsapp" }, "message": { "text": "Hello!" } } ``` **Template message:** ```json theme={null} { "sender": "+14151111234", "number": "+16044441111", "templateName": "my_template", "language": "en", "parameters": [] } ``` WhatsApp template messages can only be sent to **personal WhatsApp accounts** — not to WhatsApp API (business) accounts. When testing, use your personal WhatsApp phone number rather than a business number. Also make sure your personal account has accepted all WhatsApp app and privacy updates. Use the `callbackData` field when sending a message. Set it to any JSON value up to 1024 bytes (e.g. `{ "order_id": "12345" }`) and Connectly echoes it back in every related webhook event — delivery status updates and inbound replies that reference the message. Alternatively, save the `id` returned in the send response and match it against `statusUpdate.id` in delivery status webhook events. *** ## Templates Meta typically approves templates within 5 minutes, but it can take up to 24 hours. After submitting via Create template, poll Get templates periodically to check the status. The template starts as `MESSAGE_TEMPLATE_STATUS_PENDING` and transitions to approved or rejected. Check the `rejectionReason` field in the Get templates response. Common reasons include: * `INVALID_FORMAT` — the template structure doesn't meet Meta's guidelines. Review [Meta's Message Template Guidelines](https://developers.facebook.com/docs/whatsapp/message-templates/guidelines). * Variables not numbered sequentially (e.g. using `{{1}}` and `{{3}}` without `{{2}}`). * Shortened URLs (e.g. bit.ly) in button fields — use the full URL. * Content that violates WhatsApp's commerce or messaging policies. Fix the issues and submit a new template. You cannot edit a rejected template — create a new one with a different name. Meta only allows editing an approved template once within a 24-hour period, and only certain fields. For significant changes, it's safer to create a new template with a different name, get it approved, and then delete the old one once it's no longer in use. `WHATS_APP_MESSAGE_LIMIT_TIER_UNSPECIFIED` is expected for new accounts or accounts whose tier hasn't changed since setup. The field only updates when Meta registers a tier change. This is not an error — your account can still send messages. Check Get quality signals again after sending your first batch of campaigns. *** ## Webhooks Every webhook request Connectly sends includes an `x-connectly-hmac-sha256` header — a Base64-encoded HMAC-SHA256 digest of the raw request body, signed with your webhook secret. Verify it before processing: ```go theme={null} secret := "YOUR_SECRET_VALUE" hash := hmac.New(sha256.New, []byte(secret)) hash.Write(webhookEventBody) isValid := webhookEventHMAC == base64.StdEncoding.EncodeToString(hash.Sum(nil)) ``` Reject any request where the HMAC doesn't match. See Webhooks overview for more detail. `callbackData` is only echoed back on webhook events that are directly linked to the original outbound message: * Delivery status events (`sent`, `delivered`, `read`, `delivery_failed`) — always included. * Inbound replies that **quote** the original message — button replies, list replies, quoted media, reactions. Plain-text replies that don't quote the original message do **not** include `callbackData` — WhatsApp provides no server-side signal linking them to a specific outbound message. * Your endpoint must be publicly accessible over **HTTPS**. Plain HTTP endpoints are not supported. * Your endpoint must return a `2xx` response within a reasonable timeout. Connectly treats non-2xx responses as failures. * Confirm your webhook registration is active using Get webhooks. * Check that you registered for the correct topic (`messages` for inbound, `delivery_status` for outbound delivery events). *** ## Campaigns A `409` means the campaign is not in a state that accepts sendouts — typically because it hasn't been published yet. Go to the Connectly Flow Builder, finalize and publish the campaign, then retry the API call. By default, no — the API prevents duplicate sends. To override this, set `options.if_duplicate_check_unspecified` to `"allow_multiple"` in your request: ```json theme={null} { "options": { "if_duplicate_check_unspecified": "allow_multiple" }, "entries": [...] } ``` Use this only when repeated sends are intentional — for example, recurring service alerts. In the Connectly inbox at [inbox.connectly.ai](https://inbox.connectly.ai), go to the Flow Builder, select **Resend or Edit** next to your campaign, and copy the name exactly as shown. See Campaigns overview for a step-by-step guide. *** ## Account & general Your business ID is a UUID that scopes all your API requests. You can find it in the Connectly Dashboard under your account settings, or ask your Connectly Account Manager. It appears in the path of every API endpoint: `/v1/businesses/{businessId}/...`. API keys can be regenerated at any time from the Connectly Dashboard under **Settings → General → API Key**. Note that the plaintext key is only shown once when created — if you've lost it, you'll need to create a new one. Update your backend configuration with the new key immediately, as the old one will no longer authenticate. Most messaging endpoints are limited to **200 requests per second** with burst capacity up to **1,000 requests per second**. The Assets API is limited to **100 requests per second**. Exceeding these limits returns `429 Too Many Requests`. Implement exponential backoff in your client if you expect sustained high volume. Email us at [contact@connectly.ai](contact@connectly.ai) and include your `cntTraceId` from any error responses — it's the fastest way for our team to diagnose the issue. # Create Webhook Source: https://docs.connectly.ai/webhooks/create-webhook POST /v1/businesses/{businessId}/create/webhooks Register an HTTPS endpoint to receive Connectly webhook events for a given topic 💪 Register a URL that Connectly will POST to whenever a subscribed event occurs. You can register **multiple endpoints per topic** — the only restriction is you cannot register the exact same topic + URL pair twice. ## Endpoint ```json theme={null} POST https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks ``` ## Request body The event topic to subscribe to. | Value | Description | | ----------------- | ------------------------------------------------------------------------ | | `messages` | Inbound WhatsApp messages sent by your customers. | | `delivery_status` | Delivery status updates: `sent`, `delivered`, `read`, `delivery_failed`. | Your publicly accessible HTTPS endpoint URL (e.g. `https://example.com/webhook`). Integration type for this webhook. Defaults to `custom`. | Value | Description | | ----------------- | ------------------------------ | | `custom` | Your own HTTP endpoint. | | `zapier` | Zapier integration. | | `webengage` | WebEngage integration. | | `integromat_make` | Integromat / Make integration. | Advanced configuration options. When `true`, outbound messages sent by your business are echoed back to this webhook in addition to inbound messages. List of channel types to filter events by (e.g. `["whatsapp"]`). When omitted, events from all channels are delivered. List of filter rules to restrict which events are delivered to this endpoint. The event field to match on. Supported values: `campaign_name`, `phoneNumbers`, `botConversation`. Note: `campaign_name` works on **any topic** including `messages` — not just `delivery_status`. It matches events where the conversation is tagged with that campaign name. Glob-style expression to match against the field value. Use `*` to match any campaign. Use a specific name like `summer-promo-*` to match a subset. Required when `type` is `webengage`. Your WebEngage API token. Your WebEngage webhook URL. ## Examples ```bash theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "topic": "messages", "address": "https://example.com/webhook/messages" }' ``` ```bash theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "topic": "delivery_status", "address": "https://example.com/webhook/delivery" }' ``` ```bash theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "topic": "delivery_status", "address": "https://example.com/webhook/delivery", "configuration": { "channelTypes": ["whatsapp"], "filters": [ { "field": "campaign_name", "expression": "summer-promo-*" } ] } }' ``` You can register multiple webhooks on the same topic pointing to different URLs. All registered endpoints receive matching events — creating a second one does not replace the first. ```bash theme={null} # First endpoint — receives all inbound messages curl --request POST \ --url https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "topic": "messages", "address": "https://example.com/webhook/all-messages" }' # Second endpoint — receives only campaign-tagged messages curl --request POST \ --url https://api.connectly.ai/v1/businesses/{businessId}/create/webhooks \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "topic": "messages", "address": "https://example.com/webhook/campaign-messages", "configuration": { "filters": [{ "field": "campaign_name", "expression": "*" }] } }' ``` ## Response ```json theme={null} { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` Save the returned `id` — you'll need it to [update](/webhooks/update-webhook) or [delete](/webhooks/delete-webhook) this registration. ## Error responses | Status | Meaning | | ------ | ------------------------------------------------------------- | | `400` | Malformed body or invalid field values. | | `401` | Missing or invalid `X-API-Key`. | | `409` | The exact same topic + URL combination is already registered. | | `500` | Internal server error. | # Delete Webhook Source: https://docs.connectly.ai/webhooks/delete-webhook DELETE /v1/businesses/{businessId}/delete/webhooks/{webhookId} Permanently remove a webhook registration and stop event delivery 🚥 Permanently remove a webhook registration. Once deleted, Connectly stops sending events for that topic to the registered address. Deletion is immediate and irreversible. If you only need to change the endpoint URL, use [Update webhook](/webhooks/update-webhook) instead to avoid a gap in event delivery. ## Endpoint ```json theme={null} DELETE https://api.connectly.ai/v1/businesses/{businessId}/delete/webhooks/{webhookId} ``` Retrieve the `webhookId` from the [Get webhooks](/webhooks/get-webhooks) response. ## Example request ```bash theme={null} curl --request DELETE \ --url https://api.connectly.ai/v1/businesses/{businessId}/delete/webhooks/{webhookId} \ --header 'X-API-Key: YOUR_API_KEY' ``` ## Response A successful deletion returns HTTP `200` with an empty JSON object. ```json theme={null} {} ``` ## Error responses | Status | Meaning | | ------ | ----------------------------------------- | | `401` | Missing or invalid `X-API-Key`. | | `404` | The specified `webhookId` does not exist. | | `500` | Internal server error. | # Get Webhooks Source: https://docs.connectly.ai/webhooks/get-webhooks GET /v1/businesses/{businessId}/webhooks Retrieve all webhook registrations for your business 🤝 Retrieve all webhook registrations currently configured for your business. Use the returned `id` values when updating or deleting a specific registration. ## Endpoint ```json theme={null} GET https://api.connectly.ai/v1/businesses/{businessId}/webhooks ``` ## Example request ```bash theme={null} curl --request GET \ --url https://api.connectly.ai/v1/businesses/{businessId}/webhooks \ --header 'Accept: application/json' \ --header 'X-API-Key: YOUR_API_KEY' ``` ## Response ```json theme={null} { "entity": { "businessId": "biz_01ARZ3NDEKTSV4RRFFQ69G5FAV", "webhooks": [ { "id": "wh_01ARZ3NDEKTSV4RRFFQ69G5FAV", "topic": "messages", "address": "https://example.com/webhook/messages", "configuration": { "echo": false, "channelTypes": ["whatsapp"], "filters": [] } }, { "id": "wh_02BRY4OEFLTUW5SSGGH70H6GBW", "topic": "delivery_status", "address": "https://example.com/webhook/delivery", "configuration": { "echo": false, "channelTypes": ["whatsapp"], "filters": [ { "field": "campaign_name", "expression": "summer-promo-*" } ] } } ] } } ``` **Response fields:** | Field | Description | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `entity.businessId` | The business ID these registrations belong to. | | `entity.webhooks` | Array of webhook registration objects. Empty array if none are configured. | | `webhooks[].id` | Unique identifier for this registration. Use with [Update webhook](/webhooks/update-webhook) and [Delete webhook](/webhooks/delete-webhook). | | `webhooks[].topic` | The subscribed topic: `messages` or `delivery_status`. | | `webhooks[].address` | The endpoint URL receiving events for this topic. | | `webhooks[].configuration.echo` | Whether outbound messages are echoed back to this webhook. | | `webhooks[].configuration.channelTypes` | Channel types this webhook filters on. | | `webhooks[].configuration.filters` | Active filter rules — each has a `field` and `expression`. | | `webhooks[].configuration.webengageConfiguration` | Present only for WebEngage-type webhooks. Contains `webengageToken` and `webengageWebhookUrl`. | ## Error responses | Status | Meaning | | ------ | ------------------------------------------ | | `401` | Missing or invalid `X-API-Key`. | | `404` | The specified `businessId` does not exist. | | `500` | Internal server error. | # Webhooks Overview Source: https://docs.connectly.ai/webhooks/overview Register an HTTPS endpoint to receive real-time WhatsApp events from Connectly — inbound messages, delivery status updates, and HMAC-SHA256 verification 🛜 Connectly webhooks deliver real-time WhatsApp events — inbound customer messages and outbound delivery status updates — directly to an HTTPS endpoint you control. Once you register your endpoint for a topic, Connectly POSTs event payloads to it as they occur. ## Available topics | Topic | Description | | ----------------- | ------------------------------------------------------------------------------------------------- | | `messages` | Inbound WhatsApp messages sent to your business number by customers. | | `delivery_status` | Delivery status updates for messages you sent: `sent`, `delivered`, `read`, or `delivery_failed`. | You can register **multiple endpoint addresses per topic** — for example, two different URLs both subscribed to `messages`. The only restriction is you cannot register the exact same topic + URL combination twice. Use [filters](#register-your-endpoint) to route specific events to specific endpoints. *** ## Register your endpoint Before you start receiving events, register your endpoint for the topic you want: ```bash theme={null} curl --request POST \ --url https://api.connectly.ai/v1/businesses//create/webhooks \ --header 'Content-Type: application/json' \ --header 'X-API-Key: ' \ --data '{"topic": "messages", "address": "https://example.com/webhook"}' ``` Change `"topic"` to `"delivery_status"` and update `"address"` to register for delivery status events instead. See [Create webhook](https://docs.connectly.ai/api-reference/create-webhook) for the full reference. *** ## Verify HMAC signatures Every webhook request Connectly sends includes an `x-connectly-hmac-sha256` header — a Base64-encoded HMAC-SHA256 digest of the raw request body, signed with your webhook secret. Always verify this before processing the payload. ```go theme={null} secret := "YOUR_SECRET_VALUE" data := string(webhookEventBody) hash := hmac.New(sha256.New, []byte(secret)) hash.Write([]byte(data)) isValid := webhookEventHMAC == base64.StdEncoding.EncodeToString(hash.Sum(nil)) ``` Reject any request where the computed HMAC does not match the `x-connectly-hmac-sha256` header. Never process unauthenticated webhook payloads. *** ## Acknowledge receipt Your endpoint must return HTTP `200` for every event to acknowledge successful delivery. Connectly treats any non-200 response as a failure and may retry the request. *** ## Delivery status event flow When you send a message via the Connectly API, the response includes a message ID: ```json theme={null} { "id": "01FRRVK645V350357FGV2Y1B16" } ``` Delivery status events include this same ID in `statusUpdate.id`, so you can correlate each status update back to the original outbound message. The typical progression for a successfully delivered and read message is: The message was dispatched to the recipient. Delivery is not yet confirmed. The message reached the recipient's device. It may not have been opened yet. The recipient opened the message in their WhatsApp app. The message could not be delivered. The event includes an `error` object with a `cntTraceId` you can share with Connectly support. Store the message ID returned when you send a message so you can match it against incoming `delivery_status` events via the `statusUpdate.id` field. *** ## Next steps Every payload shape with full JSON examples — text, media, button replies, referrals, and delivery status. Register an endpoint for a topic, with filtering and configuration options. Change the destination URL or configuration of an existing registration. Permanently remove a webhook registration. # Webhook Payload Reference Source: https://docs.connectly.ai/webhooks/payload-types Every webhook payload shape Connectly sends — inbound text, referrals, media, button replies, and delivery status events — with full JSON examples 💻 This page covers every type of webhook payload Connectly can POST to your endpoint. Currently, only WhatsApp events are supported. ## Common fields ### `callbackData` When you set the optional `callbackData` field on an outbound message, Connectly echoes it back at the top level of related webhook events: * Delivery status events — `sent`, `delivered`, `read`, `delivery_failed` * Inbound replies that reference the original message — quoted replies, button & list replies, reactions `callbackData` is only present when it was set on the original outbound message. Plain-text inbound messages that don't quote a prior message will not include it. ### Business-scoped user IDs (BSUID) On WhatsApp numbers with BSUID support enabled, the customer identifier carries two extra fields: `userId` and `phoneNumber`. For phone-less customers, `id` holds the BSUID and `phoneNumber` is `""`. The customer appears as `sender` on inbound message webhooks and as `recipient` on delivery-status webhooks. See [BSUID](https://docs.connectly.ai/messaging/bsuid) for full details. *** ## Inbound message payloads ### Plain text ```json theme={null} { "timestamp": "1639083206", "sender": { "id": "+16315555500", "channelType": "whatsapp", "name": "Customer Name" }, "recipient": { "id": "+16044441234", "channelType": "whatsapp", "name": "connectlyai" }, "message": { "text": "Hello!" } } ``` ### Message with referral (Click-to-WhatsApp ad) When a customer messages you directly from a Meta ad, the payload includes a `referral` object with ad attribution data. ```json theme={null} { "timestamp": "1639083206", "sender": { "id": "+16315555500", "channelType": "whatsapp", "name": "Customer Name" }, "recipient": { "id": "+16044441234", "channelType": "whatsapp", "name": "connectlyai" }, "message": { "text": "Hello!", "referral": { "ctwaClid": "ARAkLkA8nM...", "sourceUrl": "AD_OR_POST_FB_URL", "sourceId": "ADID", "sourceType": "ad", "headline": "AD_TITLE", "body": "AD_DESCRIPTION", "mediaType": "image", "imageUrl": "RAW_IMAGE_URL", "videoUrl": "RAW_VIDEO_URL", "thumbnailUrl": "RAW_THUMBNAIL_URL" } } } ``` | Field | Description | | -------------- | -------------------------------------------------------- | | `ctwaClid` | Click ID generated by Meta for the click-to-WhatsApp ad. | | `sourceUrl` | URL of the Facebook ad or post. | | `sourceId` | ID of the ad. | | `sourceType` | `"ad"` or `"post"`. | | `headline` | Title of the ad. | | `body` | Description text of the ad. | | `mediaType` | `"image"` or `"video"`. | | `imageUrl` | Raw URL of the ad image. | | `videoUrl` | Raw URL of the ad video. | | `thumbnailUrl` | Raw URL of the video thumbnail. | ### Media attachment When a customer sends an image, video, audio, or document, the payload includes an `attachments` array. Only one attachment is sent per webhook event. ```json theme={null} { "timestamp": "1640204070", "sender": { "id": "+16315555500", "channelType": "whatsapp", "name": "Customer Name" }, "recipient": { "id": "+16044441234", "channelType": "whatsapp", "name": "connectlyai" }, "message": { "attachments": [ { "type": "audio", "url": "https://cdn.connectly.ai/46b6/46b6725c-a821-480c-901b-8a76b990a25c" } ] } } ``` The `type` field indicates the media type. The file is accessible at `url` — when downloading programmatically, read the MIME type from the response `Content-Type` header. **Supported media types and MIME types:** | Type | Supported MIME types | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `image` | `image/jpeg`, `image/png` | | `video` | `video/mp4`, `video/3gpp`*(coming soon)* | | `audio` | `audio/aac`, `audio/mp4`, `audio/mpeg`, `audio/amr`, `audio/ogg`, `audio/ogg; codecs=opus` | | `document` | `text/plain`, `application/pdf`, `application/msword`, `application/vnd.ms-excel`, `application/vnd.ms-powerpoint`, `application/vnd.openxmlformats-officedocument.*`, `application/vnd.android.package-archive` | ### Button response When a customer taps an interactive button, the payload includes a `buttonResponse` object and a `context` object referencing the original message. If you set `callbackData` on the original outbound message, it is echoed back here. ```json theme={null} { "timestamp": "1639083206", "sender": { "id": "+16315555500", "channelType": "whatsapp", "name": "Customer Name" }, "recipient": { "id": "+16044441234", "channelType": "whatsapp", "name": "connectlyai" }, "context": { "replyToId": "018d62f2-2524-10fa-f857-091ab99c14c8" }, "buttonResponse": { "text": "Yes", "payload": "1" }, "callbackData": { "order_id": "12345" } } ``` *** ## Delivery status payloads Delivery status events are sent to your `delivery_status` topic endpoint. The `statusUpdate.id` field matches the message ID returned when you originally sent the message — use it to correlate events back to outbound messages. ### Delivered ```json theme={null} { "topic": "delivery_status", "timestamp": "1641513098", "sender": { "id": "+16044441234", "channelType": "whatsapp", "name": "connectlyai" }, "recipient": { "id": "+16315555500", "channelType": "whatsapp", "name": "Customer Name" }, "statusUpdate": { "id": "01FRRWW2ZAHD9GJ1SWY4VF2GBD", "status": "delivered", "error": null, "metadata": { "campaign_name": "my_campaign" } }, "callbackData": { "order_id": "12345" } } ``` The `status` field progresses through `sent` → `delivered` → `read`. Each transition generates a separate event. ### Delivery failed When delivery fails, `status` is `delivery_failed` and the `error` object provides details including a `cntTraceId` to share with Connectly support. ```json theme={null} { "topic": "delivery_status", "timestamp": "1641512856", "sender": { "id": "+16044441234", "channelType": "whatsapp", "name": "connectlyai" }, "recipient": { "id": "+16315555500", "channelType": "whatsapp", "name": "Customer Name" }, "statusUpdate": { "id": "01FRRWMSAMKNFZAMBPQ65CA7DD", "status": "delivery_failed", "error": { "message": "Message template inputs invalid", "type": "ERROR_TYPE_INVALID_REQUEST", "code": "ERROR_CODE_MESSAGE_TEMPLATE_INPUT_INVALID", "userTitle": "Message template inputs invalid", "userMessage": "Pass along the connectly trace id 'cnct_trace_id' to the team for more information.", "cntTraceId": "10827968052975079261", "details": {} }, "metadata": { "campaign_name": "my_campaign" } } } ``` | Error field | Description | | ------------- | ------------------------------------------------------------------------------ | | `message` | Description of the error. | | `type` | High-level error category (e.g. `ERROR_TYPE_INVALID_REQUEST`). | | `code` | Specific error code (e.g. `ERROR_CODE_MESSAGE_TEMPLATE_INPUT_INVALID`). | | `userTitle` | Short human-readable error title. | | `userMessage` | Actionable guidance for resolving the error. | | `cntTraceId` | Connectly trace ID — provide this to support when reporting delivery failures. | Log the `cntTraceId` from every `delivery_failed` event. It's the fastest way to diagnose a delivery failure with Connectly support. For the full list of error types and codes, see [Error codes](https://docs.connectly.ai/messaging/error-codes). # Update Webhook Source: https://docs.connectly.ai/webhooks/update-webhook PUT /v1/businesses/{businessId}/update/webhooks/{webhookId} Change the destination URL or configuration of an existing webhook registration 🔀 Update the destination URL or configuration of an existing webhook registration. Only the fields you include in the request body are changed — omitting a field keeps its current value. You cannot change the `topic` of an existing registration. To switch topics, [delete](/webhooks/delete-webhook) the registration and [create](/webhooks/create-webhook) a new one. ## Endpoint ```json theme={null} PUT https://api.connectly.ai/v1/businesses/{businessId}/update/webhooks/{webhookId} ``` Retrieve the `webhookId` from the [Get webhooks](/webhooks/get-webhooks) response. ## Request body New destination URL. Must be a publicly accessible HTTPS endpoint. Updated configuration. Any fields you provide overwrite the existing values. When `true`, outbound messages sent by your business are echoed back to this webhook. List of channel types to filter events by (e.g. `["whatsapp"]`). Updated filter rules. Replaces the existing filters entirely. The event field to match on (e.g. `campaign_name`). Glob-style expression to match against the field value (e.g. `prod-*`). Required when the webhook type is `webengage`. Your WebEngage API token. Your WebEngage webhook URL. ## Examples ```bash theme={null} curl --request PUT \ --url https://api.connectly.ai/v1/businesses/{businessId}/update/webhooks/{webhookId} \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "address": "https://example.com/webhook-v2" }' ``` ```bash theme={null} curl --request PUT \ --url https://api.connectly.ai/v1/businesses/{businessId}/update/webhooks/{webhookId} \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "configuration": { "echo": true, "channelTypes": ["whatsapp"], "filters": [ { "field": "campaign_name", "expression": "prod-*" } ] } }' ``` ```bash theme={null} curl --request PUT \ --url https://api.connectly.ai/v1/businesses/{businessId}/update/webhooks/{webhookId} \ --header 'Content-Type: application/json' \ --header 'X-API-Key: YOUR_API_KEY' \ --data '{ "address": "https://example.com/webhook-v2", "configuration": { "echo": false, "channelTypes": ["whatsapp"], "filters": [ { "field": "campaign_name", "expression": "prod-*" } ] } }' ``` ## Response ```json theme={null} { "id": "wh_01ARZ3NDEKTSV4RRFFQ69G5FAV" } ``` ## Error responses | Status | Meaning | | ------ | ----------------------------------------- | | `400` | Malformed body or invalid field values. | | `401` | Missing or invalid `X-API-Key`. | | `404` | The specified `webhookId` does not exist. | | `500` | Internal server error. | # Webhook Keys Source: https://docs.connectly.ai/webhooks/webhook-keys Webhook keys are HMAC signing secrets used to verify that webhook payloads come from Connectly. One active key per business at a time 🔑 Webhook keys are separate from webhook registrations. A webhook registration defines *where* Connectly sends events. A webhook key is the **HMAC signing secret** used to verify that those events actually came from Connectly — it's included in every request as the `x-connectly-hmac-sha256` header. There is only ever **one active webhook key per business** at a time. Creating a new key automatically revokes the previous one. Old revoked keys are retained in the system (soft-deleted) but are no longer valid — this is why you may see multiple keys listed when you call the list endpoint. *** ## Endpoints | Method | Endpoint | Description | | ------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `GET` | `/v1/businesses/{businessId}/business_keys` | List all keys — shows type, status (`active`/`expired`/`revoked`), masked key value, and creation timestamps. | | `POST` | `/v1/businesses/{businessId}/business_keys` | Create a new webhook signing key. Automatically revokes the currently active key. | | `POST` | `/v1/businesses/{businessId}/business_keys/{keyId}/revoke` | Revoke a specific key by ID. | *** ## List your keys Use this to see the status of all keys associated with your business — useful if you're seeing unexpected keys in your account: ```bash theme={null} curl -X GET "https://api.connectly.ai/v1/businesses/{businessId}/business_keys" \ -H "X-API-Key: YOUR_API_KEY" ``` The response shows each key's `status` (`active`, `expired`, or `revoked`) and `created_at` timestamp. Only one key will have `status: active` — the rest are historical rotations. *** ## Rotate your key Creating a new key immediately revokes the current active one: ```bash theme={null} curl -X POST "https://api.connectly.ai/v1/businesses/{businessId}/business_keys" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" ``` After rotating, update your webhook handler with the new secret immediately — any requests verified against the old key will fail. *** ## Verify webhook payloads Use the active key to verify the `x-connectly-hmac-sha256` header on every incoming webhook request. See [Webhooks overview](/webhooks/overview#verify-hmac-signatures) for the verification code example.