# Diverge Docs > Everything needed to integrate the Diverge chatbot, track chatbot analytics, and work against the chatbot API. import { ApiReference } from "../components/ApiReference"; ## Chatbot Analytics This document covers the analytics events emitted by the chatbot script and how to consume them in your own tracking setup, for example Google Tag Manager. ### Prerequisites The chatbot script must be loaded on the page: ```html ``` No additional scripts or configuration are needed. Analytics events are emitted automatically. ### How It Works The chatbot dispatches `CustomEvent`s on the global `window` object whenever key interactions occur. These events are intentionally decoupled from any specific analytics platform, so you keep full control over how to forward and map the data. ### Available Events | Event Name | Fired When | Extra Detail Fields | | ------------------------------ | ----------------------------------------------- | --------------------- | | `chat_open` | The user opens the chat window | None | | `chat_response` | The chatbot returns a response | `response_latency_ms` | | `chat_recommendation` | The chatbot returns product recommendations | None | | `chat_product_click` | The user clicks a product link in the chat | None | | `chat_contact_form_shown` | The chatbot shows the built-in contact form | None | | `chat_conversation_classified` | The conversation is classified after a response | `classification` | All events include `chat_type: "shopping_assistant"` in their `detail` payload. ### Listening to Events ```js window.addEventListener("chat_open", (e) => { console.log("Chat opened", e.detail); }); window.addEventListener("chat_response", (e) => { console.log("Response latency:", e.detail.response_latency_ms, "ms"); }); window.addEventListener("chat_recommendation", (e) => { console.log("Recommendation shown", e.detail); }); window.addEventListener("chat_product_click", (e) => { console.log("Product clicked", e.detail); }); window.addEventListener("chat_contact_form_shown", (e) => { console.log("Contact form shown", e.detail); }); window.addEventListener("chat_conversation_classified", (e) => { console.log("Classification:", e.detail.classification); }); ``` ### Google Tag Manager Integration Push events into the GTM `dataLayer` by listening to the chatbot events and mapping them: ```html ``` Then create corresponding triggers in GTM using **Custom Event** with the event names above. ### Event Detail Payloads Every event carries a `detail` object accessible via `e.detail`: **`chat_open`** ```json { "chat_type": "shopping_assistant" } ``` **`chat_response`** ```json { "chat_type": "shopping_assistant", "response_latency_ms": 1234 } ``` **`chat_recommendation`** ```json { "chat_type": "shopping_assistant" } ``` **`chat_product_click`** ```json { "chat_type": "shopping_assistant" } ``` **`chat_contact_form_shown`** ```json { "chat_type": "shopping_assistant" } ``` **`chat_conversation_classified`** ```json { "chat_type": "shopping_assistant", "classification": "Produktinformation" } ``` ### postMessage from Chatbot Iframe The chatbot iframe sends `postMessage` events to the parent window. You can listen to these instead of, or in addition to, the `CustomEvent`s above. Use this when you need raw access to iframe messages or when the chatbot script is not loaded on your page. **Origin:** `https://chatbot.dialogintelligens.dk` (or `http://localhost:3002` in development) **Payload format:** Each message has an `action` field. Some actions include extra fields. | Action | Extra Fields | When | | ------------------------ | ------------------------------------- | ------------------------------------- | | `productClick` | None | User clicks a product in the chat | | `navigate` | `url` | Parent should navigate to product URL | | `userMessageSubmitted` | None | User submitted a message | | `firstMessageSent` | `chatbotID` | User sent their first message | | `assistantFirstToken` | None | First token of AI response arrived | | `productRecommendation` | Optional `urls` | Product recommendations shown | | `contactFormShown` | None | Built-in contact form shown | | `conversationClassified` | `emne` | Conversation classified by AI | | `purchaseReported` | `chatbotID`, `totalPrice`, `currency` | User reported a purchase | | `expandChat` | None | Chat expanded | | `collapseChat` | None | Chat collapsed | | `closeChat` | None | Chat closed | | `toggleSize` | None | User toggled chat size | **Example listener:** ```js const CHATBOT_ORIGIN = "https://chatbot.dialogintelligens.dk"; window.addEventListener("message", (event) => { if (event.origin !== CHATBOT_ORIGIN) return; const data = event.data ?? {}; if (data.action !== "productClick") return; window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "chat_product_click", chat_type: "shopping_assistant", }); }); ``` **Mapping to CustomEvents:** The chatbot script on the parent translates some postMessage actions into CustomEvents. If the script is loaded, you get both the raw `postMessage` and the `CustomEvent`. Actions that map to CustomEvents: `productClick` -> `chat_product_click`, `assistantFirstToken` -> `chat_response`, `productRecommendation` -> `chat_recommendation`, `contactFormShown` -> `chat_contact_form_shown`, `conversationClassified` -> `chat_conversation_classified`. ### Notes * Events are dispatched automatically. No initialization or opt-in is required. * The `response_latency_ms` value is the round-trip time in milliseconds from when the user sends a message until the chatbot response arrives. * The `chat_conversation_classified` event fires a few seconds after each response, once the backend finishes classifying the conversation. The `classification` value is the topic or category assigned by the AI, for example `"Ordre"`, `"Produktinformation"`, or `"Reklamation"`, or `null` if classification failed. * The implementation is decoupled from GTM on purpose. You are responsible for listening, mapping, and pushing data to your analytics platform. * Events are standard `CustomEvent`s, so they work with any analytics tool that can listen to DOM events. * These events cover the front-end side of a session. To load the conversations themselves — topic, rating, and helpdesk escalation — into your own data warehouse, see [Conversation Export](/guides/conversation-export). ## Chatbot API Errors The chatbot API has two error channels: * Non-2xx HTTP responses use the standard `ApiError` envelope. * `POST /api/v1/chat/messages` can also return a `200` SSE stream that later ends with a terminal `error` event. Handle these separately. `ApiError.error.code` and `StreamErrorEvent.code` are different enums. ### HTTP API errors Before a stream starts, endpoints return non-2xx HTTP responses with this shape: ```json { "error": { "code": "validation", "message": "Validation failed", "params": [{ "field": "message.parts.0.text", "message": "Text is required" }] } } ``` `params` is only present when the API can point to specific invalid fields. | Code | Typical status | Meaning | Client handling | | -------------- | -------------- | ------------------------------------------------------------- | ------------------------------------------------------------------- | | `unauthorized` | `401` | Missing, expired, or invalid authentication credentials. | Request a new visitor token or ask the user to restart the session. | | `forbidden` | `403` | Authenticated, but not allowed to access the resource. | Stop the action and show a permission error. | | `not_found` | `404` | The target resource does not exist or is unavailable. | Show a not-available state; do not retry automatically. | | `rate_limited` | `429` | Too many requests in a short period. | Back off before retrying. | | `validation` | `422` | The request body or query parameters failed validation. | Fix the highlighted fields from `params` before retrying. | | `internal` | `500` | Unexpected server failure before the response stream started. | Show a generic error and allow retry if the action is safe. | ### Stream errors `POST /api/v1/chat/messages` is different because it streams the assistant response. Once the HTTP response has started, the server cannot switch to a non-2xx HTTP error. Instead, the stream ends with an SSE `error` event: ```text event: error data: {"code":"generation_failed","message":"Failed to generate a response. Please try again.","retryable":true} ``` The parsed event data has this shape: ```json { "code": "generation_failed", "message": "Failed to generate a response. Please try again.", "retryable": true } ``` `message` is localized using the chatbot's configured UI language when the stream error is generated by the chatbot API. Keep displaying the returned value instead of hardcoding client-side copy. | Code | When it happens | Client handling | | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------ | | `generation_failed` | Response generation failed after the stream started. | Show `message`; offer retry only when `retryable` is `true`. | After receiving a stream `error`, treat the stream as complete and disconnect. It will not be followed by a `done` event. ### Handling both channels Check the HTTP response first. Only start reading stream events after `response.ok` is true. ```ts const response = await fetch("https://api.dialogintelligens.dk/api/v1/chat/messages", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { const apiError = await response.json(); handleApiError(apiError.error.code, apiError.error.message, apiError.error.params); return; } await readSse(response.body, (event) => { if (event.event === "error") { handleStreamError(event.data.code, event.data.message, event.data.retryable); return; } handleStreamEvent(event); }); ``` Keep a generic fallback for unknown future error codes, but do not treat stream error codes as `ApiError` codes. ## Chatbot API Flow This guide explains how the public chatbot API fits together from an integrator's point of view. It focuses on the flow and data model so you can build a client without guessing how responses should be interpreted. For the full schema, field-level validation, and complete endpoint reference, see the [API reference](/api). ### Flow at a glance 1. Your backend calls `POST /api/v1/chat/auth` with the chatbot API key and a `chatbot_id`. 2. The API returns a visitor Bearer token. This creates a visitor/session, not a conversation. 3. Your client calls `GET /api/v1/chat/config` to load branding and startup configuration. 4. Your client sends user input to `POST /api/v1/chat/messages` and receives a streamed response. 5. The stream emits structured content as `part_delta` and `part` events, then ends with `done` or `error`. 6. If the assistant returns an actionable marker, your client collects the required data and sends it to `POST /api/v1/chat/actions`. 7. To start a fresh chat for the same visitor/session, your backend calls `POST /api/v1/chat/auth/reset` with the current visitor token, then your client replaces its stored token with the returned token. 8. You can optionally read message history, save a conversation rating, export data, or delete the visitor's data. ### Auth and config The API has two authentication layers: | Step | Who calls it | Purpose | | ------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------ | | `POST /api/v1/chat/auth` | Your backend | Exchange the API key for a visitor token | | `POST /api/v1/chat/auth/reset` | Your backend | Start a new empty conversation for the same visitor/session and return a replacement token | | Visitor-scoped endpoints | Your client or trusted backend | Use the returned Bearer token for config, messages, actions, history, rating, and deletion | Keep the API key on your server. Do not expose it in browser code, mobile apps, or public frontend bundles. `/auth` uses HTTP Basic Auth with: * the API key as the username * an empty password When a visitor wants a new chat without becoming a new visitor, call `/auth/reset` from your backend and replace the client-side token with the returned token. The reset token is scoped to the new empty conversation, so subsequent messages, actions, ratings, and livechat calls do not reuse the previous conversation. Older visitor tokens that do not contain a conversation scope remain compatible and use the latest-conversation fallback. If the token is too old for reset, call `/auth` again to create a new visitor. There is no refresh token flow. #### Management statistics Trusted backends can fetch dashboard-style statistics without a dashboard session: ```bash curl "https://api.dialogintelligens.dk/api/v1/chat/statistics?chatbot_id=shop-bot§ions=chatbot,livechat&start_date=2026-06-01T00:00:00Z&end_date=2026-06-30T23:59:59Z" \ -u "$CHATBOT_API_KEY:" ``` The API key environment controls whether the response reads `live` or `test` data. Use the `sections` query parameter to limit the payload: | Section | Contains | | ---------- | ----------------------------------------------------------------------------- | | `chatbot` | Consolidated conversation statistics, ratings, sources, conversions, and CSAT | | `livechat` | Handover/livechat sessions, response times, feedback, and CSAT | | `ttft` | Time-to-first-token metrics | | `tags` | Aggregated conversation tags, optionally filtered by `emne` | Omit `sections`, pass an empty value, or pass `all` to fetch every section. `start_date` and `end_date` must be supplied together. Metrics that depend on data without an environment dimension, such as purchase totals and engagement rate, return `N/A` or `null` instead of mixing live and test data. #### Fetching individual CSAT records Use the management CSAT endpoint when external agents need to follow up on low scores. It returns individual ratings with public conversation identifiers (`conversation_42`), not just aggregated CSAT totals: ```bash curl "https://api.dialogintelligens.dk/api/v1/chat/csat?chatbot_id=shop-bot&max_rating=2&source=all&limit=50" \ -u "$CHATBOT_API_KEY:" ``` Each item includes `conversation_id`, `rating`, optional `feedback`, `submitted_at`, `visitor_id`, and contact hints such as `livechat_email`. The response merges: | Source | Origin | | ---------- | ------------------------------------------------------------------- | | `chatbot` | Bot conversation ratings submitted through `POST /api/v1/chat/rate` | | `livechat` | Post-session feedback submitted through livechat close flows | Filter with `min_rating`, `max_rating`, `source`, and paired `start_date`/`end_date`. Paginate with `cursor` and `next_cursor`. The API key environment scopes results to `live` or `test` data. #### Example: create a visitor token ```bash curl -X POST "https://api.dialogintelligens.dk/api/v1/chat/auth" \ -u "$CHATBOT_API_KEY:" \ -H "Content-Type: application/json" \ -d '{ "chatbot_id": "shop-bot" }' ``` The response contains a JWT token: ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` After that, your client can load config with the Bearer token. `GET /api/v1/chat/config` returns the public UI configuration for the chatbot: display identity, resolved theme values, livechat controls, popup messages, and dynamic form references. For field names, null semantics, server defaults, and dashboard label mapping, see [Chatbot Config & Theming](/guides/chatbot-config). #### Example: reset to a new chat ```bash curl -X POST "https://api.dialogintelligens.dk/api/v1/chat/auth/reset" \ -u "$CHATBOT_API_KEY:" \ -H "Content-Type: application/json" \ -d '{ "visitor_token": "eyJhbGciOiJIUzI1NiIs..." }' ``` The response shape is the same as `/auth`: ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` Store this token in place of the previous visitor token, clear the visible transcript state, then hydrate the new chat from `GET /api/v1/chat/messages`. The first reset response is empty because the reset-created conversation has no messages yet. ### How message data is structured Every conversation item is a `message`. A message contains ordered `parts`. For user input, `message.parts` is how you send text and attachments in one request. For assistant output, `message.parts` is also how you render the response. Parts are already structured for you, so the client should render them directly instead of trying to parse raw text. #### Mental model | Term | Meaning | | --------- | -------------------------------------------------------- | | `message` | One user, assistant, or system entry in the conversation | | `part` | A top-level renderable unit inside a message | | `block` | A section inside a `rich_text` part or table cell | | `span` | Inline formatted text inside a paragraph or bullet item | ```text message `-- parts[] |-- rich_text | `-- blocks[] | |-- paragraph | | `-- spans[] -> text | bold | strike | link | `-- bullet_list |-- image |-- table |-- products `-- show_contact_form ``` #### Parts Common assistant part types are: * `rich_text` for formatted text * `image` for image content * `table` for structured tables * `products` for product cards * marker parts such as `show_contact_form` or `request_image_upload` The important detail is that all of these are siblings in the same `parts` array. That means a single assistant message can look like: 1. Some text 2. A marker telling the client to open a form 3. More text after the marker #### Blocks and spans Inside a `rich_text` part: * `blocks` describe larger sections such as paragraphs and bullet lists * `spans` describe inline formatting inside those blocks, such as plain text, bold text, struck text, and links This lets a client render formatted content without having to parse markdown or custom marker syntax. #### Prompt suggestions Product cards always open `url` when the visitor clicks the product CTA. If the assistant wants the client to send a follow-up prompt instead, it emits a separate `suggestions` part: ```json { "type": "suggestions", "part_id": "part_suggestions_1", "suggestions": [ { "id": "outfit_2", "title": "Outfit 2", "image_url": "https://cdn.example.com/outfits/outfit-2.jpg", "prompt_text": "Show me outfit 2" } ] } ``` When the visitor selects a suggestion, the client should send `prompt_text` as the next user message. Legacy `XXX...YYY` product text can express the same behavior with an inline marker inside the section: ```text XXX https://cdn.example.com/outfits/outfit-2.jpg **Outfit 2** {{product_prompt:Show me outfit 2}} YYY ``` #### Example: a message with text and an action ```json { "message_id": "msg_123", "role": "assistant", "parts": [ { "type": "rich_text", "part_id": "part_1", "blocks": [ { "type": "paragraph", "spans": [{ "type": "text", "text": "Need help with your order?" }] } ] }, { "type": "show_contact_form", "part_id": "part_2", "fields": [ { "key": "name", "label": "Your name", "required": true }, { "key": "email", "label": "Email address", "type": "email", "required": true } ] }, { "type": "rich_text", "part_id": "part_3", "blocks": [ { "type": "paragraph", "spans": [{ "type": "text", "text": "Fill in the form and we will follow up." }] } ] } ] } ``` In this example, the form is not separate from the message. It is one ordered part of the message. ### How streaming works `POST /api/v1/chat/messages` returns a Server-Sent Events stream. Because this is a `POST` endpoint, the browser's built-in `EventSource` API is not a good fit. Use `fetch()` directly or a helper such as `@microsoft/fetch-event-source`. The stream uses five public event types: | Event | Meaning | | ------------ | ------------------------------------------------- | | `status` | Lifecycle updates such as connected or processing | | `part_delta` | Incremental updates for a streamed part | | `part` | A finalized structured part | | `done` | The final assembled assistant message | | `error` | A terminal stream error | Typical flow: ```text status(connected) status(processing) part_delta / part ... done or error ``` `part_delta` is useful when you want progressive rendering for content such as: * rich text * products * tables `part` gives you a finalized structured part. Marker parts typically arrive this way because they do not need progressive updates. `done.message` is the final source of truth for the assistant response. If you rendered draft content while streaming, replace or reconcile it with the message from `done`. #### Example: consume the message stream ```ts import { fetchEventSource } from "@microsoft/fetch-event-source"; await fetchEventSource("https://api.dialogintelligens.dk/api/v1/chat/messages", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ message: { parts: [{ type: "text", text: "What is your return policy?" }], }, context: { page: window.location.href, }, }), onmessage(event) { const data = JSON.parse(event.data); switch (event.event) { case "status": updateStatus(data.status); break; case "part_delta": applyPartDelta(data.part_id, data.delta); break; case "part": renderFinalPart(data.part); break; case "done": replaceDraftMessage(data.message); break; case "error": showError(data.message, { code: data.code, retryable: data.retryable, }); break; } }, }); ``` ### Error handling `POST /api/v1/chat/messages` has two different error channels, depending on whether the SSE stream has started. Non-2xx HTTP responses use `ApiError`; terminal SSE `error` events use `StreamErrorEvent`. For the full error-code catalog and handling examples, see [Chatbot API Errors](/guides/chatbot-api-errors). ### How markers and actions work together Markers are assistant parts that tell the client to do something beyond rendering text. The key link is `part_id`: * the assistant sends a marker part with a `part_id` * your client renders the related UI * when the user completes the action, your client sends that same `part_id` to `/actions` #### Marker behavior | Marker part | What the client does | Follow-up | | ---------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------- | | `show_contact_form` | Render the fields from the marker | Submit `action.type = "contact_form"` to `/actions` | | `show_support_ticket` | Render the ticket form and attachment rules from the marker | Submit `action.type = "support_ticket"` to `/actions` | | `request_image_upload` | Prompt the user to upload an image that matches the marker constraints | Send a later `/messages` request with an `image` part | | `request_human_agent` | Show a handoff option in your UI | Request livechat handover | | `custom` | Handle your own `key` and optional `payload` | Client-defined flow | Only `show_contact_form` and `show_support_ticket` are submitted to `POST /api/v1/chat/actions`. #### Example: submit an action ```json { "part_id": "part_2", "action": { "type": "contact_form", "fields": { "name": "Jane Doe", "email": "jane@example.com", "message": "I need help with my order" } } } ``` The same pattern applies to `support_ticket`, but with the support ticket payload shape defined in the API reference. ### Livechat end-to-end Livechat is the handover path from the AI conversation to a human agent. The public API keeps the visitor experience, your backend setup, outbound webhooks, and agent-system writes separated by authentication type. There are three livechat actors: | Actor | Authentication | What it does | | ------------------ | --------------------------------------------- | ---------------------------------------------------------------------------- | | Setup backend | Chatbot API key with HTTP Basic Auth | Configures livechat, webhooks, agent profiles, and queue/list views | | Visitor client | Visitor Bearer token from `/api/v1/chat/auth` | Requests handover, polls state, sends customer messages, closes, and rates | | External agent app | Livechat session Bearer token from webhook | Reads context and sends agent messages, typing updates, and agent-side close | The livechat endpoints in this guide are the public chatbot API endpoints under `/api/v1/chat`. The full request and response schemas are in the [API reference](/api). #### Setup before handover Before visitors can request a human agent, configure livechat and webhook delivery from a trusted backend using the chatbot API key. Keep this key server-side. At minimum: * Enable livechat for the chatbot. * Configure webhook delivery with an HTTPS endpoint and signing secret. * Subscribe to the required livechat webhook events. * Create or update each agent profile with `PUT /api/v1/chat/livechat/agents/{agent_id}`. * Use a stable, valid UUID for each agent. UUID v4 is recommended, where the third group starts with `4`; values such as `b2c3d4e5-f6a7-8901-bcde-f12345678901` are rejected because the UUID version digit is `8`. * List configured transfer targets with `GET /api/v1/chat/livechat/agents?chatbot_id=...`. * Set attachment retention and availability rules for the chatbot. Availability is what the visitor sees in `GET /api/v1/chat/config` under `livechat`. | Config field | How the client should use it | | --------------------------- | ------------------------------------------------------------------------------------- | | `enabled` | Show the handover UI only when this is true. | | `configured` | Indicates livechat can be made available for this chatbot. | | `availability_status` | `live` means the visitor can request handover now; `offline` means do not start it. | | `availability_reason` | Explains why livechat is live or offline, such as working hours or a manual override. | | `attachments_enabled` | Whether visitor and agent livechat messages can include attachments. | | `max_attachment_size_bytes` | Per-attachment size limit for livechat uploads. | If working hours are empty, livechat is treated as always on while enabled. If working hours exist, availability is calculated in the configured timezone. Manual overrides can force livechat live or offline without changing the weekly schedule. If you use waiting-room forms, `GET /api/v1/chat/config` returns form references in `forms`. Fetch the referenced form, submit values before or during the waiting state, and the materialized session values are included in livechat webhooks and agent context responses. #### Visitor handover flow A visitor can request a human handover after your client has a visitor Bearer token. ```bash curl -X POST "https://api.dialogintelligens.dk/api/v1/chat/livechat/handover" \ -H "Authorization: Bearer $VISITOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "platform": "website", "source": "manual_button", "part_id": "part_human_1" }' ``` `part_id` is optional. Include it when the handover came from a `request_human_agent` marker so you can connect the livechat session back to the assistant part that prompted it. On success, the visitor enters `waiting`: ```json { "livechat_session_id": "livechat_session_42", "status": "waiting", "platform": "website", "language": "english", "requested_at": "2025-06-15T14:30:10Z", "closed_at": null, "closed_by": null, "closed_by_agent_id": null, "close_reason": null } ``` At the same time, Diverge creates a `livechat.handover.requested` webhook event for your configured endpoint. That event contains the visitor, session values, detected language, prior conversation context, livechat session id, and a short-lived livechat session token for the agent system. While the visitor is waiting, your client should: * Poll `GET /api/v1/chat/livechat/state` to detect assignment, agent typing, closure, and feedback availability. * Poll `GET /api/v1/chat/livechat/messages` to merge new livechat messages into the transcript. * Continue using the normal AI message stream if you want the assistant to keep helping while the visitor waits. Once state becomes `active`, stop sending customer input to `POST /api/v1/chat/messages`. Send customer livechat messages to `POST /api/v1/chat/livechat/messages` instead: ```json { "message": { "parts": [ { "type": "text", "text": "Here is the invoice you asked for." }, { "type": "file", "data": "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC...", "mime": "application/pdf", "filename": "invoice.pdf" } ] }, "context": { "page": "https://shop.example.com/orders/123" } } ``` Customer typing indicators use `POST /api/v1/chat/livechat/typing` with `{ "is_typing": true }` and later `{ "is_typing": false }`. Send typing events only while the livechat state is `active`. The visitor can close the livechat session with `POST /api/v1/chat/livechat/close`: ```json { "reason": "Conversation completed" } ``` Closing is idempotent. After closure, livechat message and typing writes are rejected. If the state response shows `feedback.status: "pending"`, submit the visitor's post-session rating once with `POST /api/v1/chat/livechat/feedback`. #### External agent flow The handover webhook is the bridge into your agent system. Handle `livechat.handover.requested`, verify its signature, then store the `event_id` as your idempotency key. Automatic retries and manual replays use the same `event_id`, so processing the same event twice should not create a second ticket or duplicate assignment. The webhook payload includes `livechat_session.token`. Use that token as: ```text Authorization: Bearer ``` for agent-side calls: | Endpoint | Purpose | | ------------------------------------------- | -------------------------------------------------------------- | | `GET /api/v1/chat/livechat/agent/context` | Fetch latest session values and full transcript for the agent. | | `POST /api/v1/chat/livechat/agent/messages` | Send a human-agent message to the visitor. | | `POST /api/v1/chat/livechat/agent/typing` | Show or clear the agent typing indicator for the visitor. | | `POST /api/v1/chat/livechat/agent/transfer` | Transfer the active session to another configured agent. | | `POST /api/v1/chat/livechat/agent/close` | Close the session as the agent. | The livechat session token is not the chatbot API key and not the visitor token. It is scoped to one livechat session and currently lives for 900 seconds. Treat it as a secret for the active assignment only. If the token expires while the livechat session is still open, refresh it from your backend with the chatbot API key: ```bash curl -X POST "https://api.dialogintelligens.dk/api/v1/chat/livechat/sessions/$LIVECHAT_SESSION_ID/token" \ -u "$CHATBOT_API_KEY:" ``` The refresh response contains a new bearer token and expiry metadata: ```json { "livechat_session_id": "livechat_session_42", "token_type": "Bearer", "token": "eyJhbGciOiJIUzI1NiIs...", "expires_in": 900, "expires_at": "2025-06-15T14:45:10Z" } ``` The refresh endpoint does not require the old livechat session token, so it also works after the old token has expired. Refresh is rejected once the livechat session is closed by either the agent or the customer. Agent messages include the same configured UUID as `agent_id`: ```json { "agent_id": "00000000-0000-4000-8000-0000000000a1", "message": { "parts": [{ "type": "text", "text": "Hi, I'm Alice. I can help from here." }] } } ``` The first successful agent message or typing update assigns that agent to the session when no agent is already active. If another agent is already assigned, the API returns a conflict. Agent-authored messages appear in history with `role: "agent"` and include the configured display name and avatar. In the widget, an agent's `avatar_url` is shown next to their messages when one is registered. When `avatar_url` is omitted (or the image cannot be loaded), the widget renders the agent's initials derived from `display_name` instead (for example `AM` for "Andrii Medvedskyi"), so register agents without an `avatar_url` if you prefer initials over pictures. To pass a session to another agent, call `POST /api/v1/chat/livechat/agent/transfer` with the current `agent_id` and `target_agent_id`. The target must be an existing livechat agent profile for the same chatbot and API key environment; after transfer, writes from the previous agent are rejected and the target agent can continue the conversation. External systems that maintain their own queue can also list sessions with the public livechat session endpoints using the chatbot API key. Use the `turn` filter to find sessions where the agent system should act next, and use the notification count endpoint for lightweight badge counts. #### Webhook events and signing Livechat webhooks are **outbound**: Diverge POSTs signed JSON events to your HTTPS endpoint when livechat state changes. That is separate from [Universal API Integration](/guides/universal-api-integration), where Diverge **calls your API** to fetch live data during a conversation. Livechat webhooks are signed JSON `POST` requests. Verify the signature against the raw request body before trusting the payload: ```ts import { createHmac, timingSafeEqual } from "node:crypto"; function verifyDivergeWebhook({ rawBody, timestamp, signature, signingSecret, }: { rawBody: string; timestamp: string; signature: string; signingSecret: string; }) { const expected = createHmac("sha256", signingSecret) .update(`${timestamp}.${rawBody}`) .digest("hex"); const expectedBuffer = Buffer.from(expected, "hex"); const signatureBuffer = Buffer.from(signature, "hex"); return ( expectedBuffer.length === signatureBuffer.length && timingSafeEqual(expectedBuffer, signatureBuffer) ); } ``` The relevant headers are: | Header | Meaning | | ------------------ | -------------------------------------------------- | | `x-di-event` | Event type, such as `livechat.handover.requested`. | | `x-di-timestamp` | Timestamp used in the HMAC input. | | `x-di-signature` | Lowercase hex HMAC-SHA256 digest. | | `x-di-delivery-id` | Unique HTTP delivery attempt id. | | `x-di-attempt` | 1-based delivery attempt number. | | `x-di-environment` | API key environment, `live` or `test`. | Important livechat events: | Event type | When it is emitted | | -------------------------------------------- | ------------------------------------------------------- | | `livechat.handover.requested` | Visitor requests a human agent. | | `livechat.waiting.customer.message.created` | Visitor sends a message while waiting for an agent. | | `livechat.waiting.assistant.message.created` | Assistant sends a message while the visitor is waiting. | | `livechat.customer.message.created` | Visitor sends a message during active livechat. | | `livechat.customer.typing.started` | Visitor starts typing during active livechat. | | `livechat.customer.typing.stopped` | Visitor stops typing during active livechat. | | `livechat.session.closed` | Visitor or agent closes the livechat session. | | `livechat.feedback.submitted` | Visitor submits post-session livechat feedback. | | `visitor.data.deleted` | Visitor requests deletion of their chatbot data. | Only some events are optional subscriptions. Required livechat events are always kept in the subscription set when webhook delivery is enabled so the downstream system can maintain a coherent session state. Every livechat webhook payload carries the visitor's session state as `session`, including `session.metadata` and, when configured, `session.verified_metadata`. Metadata can be set server-side through `PATCH /api/v1/chat/session/metadata` (API key + visitor token), or — for chatbots embedded with the widget script — client-side via [`window.DialogIntelligens.setMetadata()`](/guides/chatbot-integration#attaching-session-metadata). Note the trust difference: server-set metadata is controlled by your backend, while widget-set metadata originates in the visitor's browser and must be treated as untrusted input. `session.verified_metadata` bridges the two for widget embeds: claims from a backend-signed RS256 JWT submitted via [`window.DialogIntelligens.setSignedMetadata()`](/guides/chatbot-integration#verified-signed-metadata) are verified against a configured public key and stored separately, so its contents are as trustworthy as server-set metadata. #### Attachments Livechat message parts can include `image` and `file` inputs when attachments are enabled. The API stores the attachment for the configured retention period and returns message parts with signed download URLs: ```json { "type": "file", "attachment_id": "attachment_123", "filename": "invoice.pdf", "mime_type": "application/pdf", "size_bytes": 48231, "url": "https://api.dialogintelligens.dk/api/v1/chat/livechat/attachments/attachment_123?token=...", "url_expires_at": "2025-06-15T14:46:00Z" } ``` Do not store signed attachment URLs as permanent file links. Store `attachment_id` and refetch the message or agent context when you need a fresh URL. Expired or deleted attachments return an API error instead of file bytes. #### Livechat state model Livechat sessions move through this lifecycle: ```text inactive -> waiting -> active -> closed ``` `inactive` means the visitor has no current livechat session. `waiting` means handover was requested and no agent has joined yet. `active` means a human agent is assigned and customer input belongs on the livechat message endpoint. `closed` is terminal for that session. Use `state_version` and `updated_at` from state/session responses to reconcile polling updates. Use `closed_by`, `closed_by_agent_id`, and `close_reason` to explain why a session ended. Use `feedback.status` to decide whether the visitor can rate the session. A chatbot can close inactive chats automatically. When the visitor has not answered an agent for the configured time, a `system` message warns them and `auto_close_at` says when the session will close unless the visitor or an agent writes first. Any visitor or agent message clears it. An automatic close reports `closed_by: "agent"`, `closed_by_agent_id: null` and `close_reason: "inactivity"`, in state responses and in the `livechat.session.closed` webhook. ### Webhook event replay Outbound webhook events are persisted before delivery. API-key callers can list delivery state, inspect attempts, and replay unexpired events through the `/api/v1/chat/events` endpoints. Stored webhook events expire 30 days after creation. After expiry, events are no longer listable, fetchable, or replayable. Expired events that were never delivered are marked `dead` before cleanup. ### History, feedback, and deletion Once a visitor is authenticated, you can also use: * `GET /api/v1/chat/messages` to load paginated message history * `POST /api/v1/chat/rate` to save a `1`-`5` conversation rating and optional feedback * `DELETE /api/v1/chat` to remove all data for the current visitor These endpoints use the same Bearer token as `config`, `messages`, and `actions`. Deleting removes the visitor's conversations, messages, livechats and other personal data. So that the chatbot's statistics and conversation counts still include those conversations, Diverge keeps a de-identified statistical record in place of each one. It holds dates, ratings, categories such as the resolution and a starter topic, and one placeholder per message, but no message content and nothing that identifies the visitor. In the dashboard it shows as a conversation whose messages read "\[DELETED FOR GDPR COMPLIANCE]". When webhook delivery is configured, `DELETE /api/v1/chat` also emits a signed `visitor.data.deleted` webhook after Diverge has removed the visitor data. The payload includes `chatbot.id`, `visitor.id`, `visitor.session_id`, `deletion.reason: "user_request"`, and deletion counts. Use this event to remove matching visitor/session data in your own systems. Like other outbound webhooks, deletion events are persisted, signed, retried, and visible through the webhook event listing and replay endpoints until they expire. ## Chatbot Config & Theming `GET /api/v1/chat/config` returns the public UI configuration for the chatbot tied to the visitor token. Fetch it after `POST /api/v1/chat/auth` and before rendering the first chat screen. The response contains display identity, fully resolved theme values, livechat controls, URL-triggered popup messages, and dynamic form references. For endpoint authentication and the full conversation flow, see [Chatbot API Flow](/guides/chatbot-api-flow). ### Naming and resolution rules The config response is designed so clients can render without guessing: 1. Colors are always suffixed `_color` and scoped by their parent object. Use `theme.input.background_color`, not `input_background_color`. 2. The theme is fully resolved. Every color and size the widget needs is required and non-null. The server resolves defaults. Clients should apply values directly and never write fallback logic. 3. `| null` appears only when null is meaningful: optional assets such as logos, launcher images, and avatars, or optional effects such as gradient color and borders where `null` means no border. 4. Sizes are CSS length strings, such as `"40px"` or `"2.5em"`. Multipliers are unitless floats with a documented base. 5. Image assets are objects, for example `{ "url": "https://cdn.example.com/avatar.png" }`. 6. Booleans are `*_enabled` toggles or adjective flags with server defaults. 7. Discriminated unions only appear for genuinely different shapes, with one `type` field. ### Complete response example ```json { "display": { "name": "Customer Service Bot", "avatar": { "url": "https://cdn.example.com/bot-avatar.png" }, "welcome_message": "Hi! How can I help you?", "subtitle": { "text": "You are chatting with an AI assistant.", "link": { "text": "Contact support", "url": "https://shop.example.com/support" } }, "privacy_policy_url": "https://shop.example.com/privacy" }, "livechat": { "enabled": true, "configured": true, "availability_status": "live", "availability_reason": "always_on", "show_livechat_logo": true, "attachments_enabled": true, "max_attachment_size_bytes": 5242880 }, "theme": { "brand": { "primary_color": "#4F46E5" }, "surface": { "background_gradient_color": "#FFFFFF", "background_color": "#FFFFFF", "muted_text_color": "#6B7280" }, "header": { "alignment": "center", "logo": { "url": "https://cdn.example.com/logo.png" }, "button": { "background_color": "#F3F4F6", "icon_color": "#111827" } }, "messages": { "assistant": { "background_color": "#F3F4F6", "text_color": "#111827", "avatar_size": "40px", "border_color": null, "thinking_border_gradient": ["#FFFFFF", "#4F46E5", "#FFFFFF"] }, "user": { "background_color": "#4F46E5", "text_color": "#FFFFFF", "border_color": null } }, "input": { "text_color": "#333333", "placeholder_color": "#a9a9a9", "background_color": "#FFFFFF", "border_color": "#000000", "height": "2.5em", "send_button": { "icon_color": "#FFFFFF" } }, "launcher": { "adaptive_icon_color_enabled": false, "image": { "desktop_url": null, "mobile_url": null } }, "product_card": { "button": { "background_color": "#4F46E5", "bold": true }, "discount_price_color": "#cc0000" }, "layout": { "border_radius_multiplier": 1 }, "font": { "web": { "family": "'Inter', sans-serif", "asset_url": "https://fonts.askdiverge.ai/system/fonts/default/web/inter-regular.woff2", "format": "woff2", "sha256": "e06f6b1bc553aaea4e4668023ed0ab0a147129c3107f511bc7d03d361b0ae085" }, "ios": { "asset_url": "https://fonts.askdiverge.ai/system/fonts/default/native/inter-regular.ttf", "format": "ttc", "sha256": "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82" }, "android": { "asset_url": "https://fonts.askdiverge.ai/system/fonts/default/native/inter-regular.ttf", "format": "ttc", "sha256": "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82" } } }, "dark_theme": { "brand": { "primary_color": "#4F46E5" }, "surface": { "background_gradient_color": null, "background_color": "#1c1c1e", "muted_text_color": "#9ba1a6" }, "header": { "alignment": "center", "logo": { "url": "https://cdn.example.com/logo.png" }, "button": { "background_color": "rgba(44, 44, 46, 0.85)", "icon_color": "#ffffff" } }, "messages": { "assistant": { "background_color": "#2c2c2e", "text_color": "#f2f2f7", "avatar_size": "40px", "border_color": null, "thinking_border_gradient": ["#FFFFFF", "#4F46E5", "#FFFFFF"] }, "user": { "background_color": "#4F46E5", "text_color": "#ffffff", "border_color": null } }, "input": { "text_color": "#f2f2f7", "placeholder_color": "#8e8e93", "background_color": "#2c2c2e", "border_color": "#3a3a3c", "height": "2.5em", "send_button": { "icon_color": "#ffffff" } }, "launcher": { "adaptive_icon_color_enabled": false, "image": { "desktop_url": null, "mobile_url": null } }, "product_card": { "button": { "background_color": "#4F46E5", "bold": true }, "discount_price_color": "#ff6b6b" }, "layout": { "border_radius_multiplier": 1 }, "font": { "web": { "family": "'Inter', sans-serif", "asset_url": "https://fonts.askdiverge.ai/system/fonts/default/web/inter-regular.woff2", "format": "woff2", "sha256": "e06f6b1bc553aaea4e4668023ed0ab0a147129c3107f511bc7d03d361b0ae085" }, "ios": { "asset_url": "https://fonts.askdiverge.ai/system/fonts/default/native/inter-regular.ttf", "format": "ttc", "sha256": "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82" }, "android": { "asset_url": "https://fonts.askdiverge.ai/system/fonts/default/native/inter-regular.ttf", "format": "ttc", "sha256": "40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82" } } }, "popup_messages": [ { "message": "Need help finding the right size? Ask me!", "url_pattern": "https://shop.example.com/products/*" } ], "forms": [ { "trigger": "livechat_waiting", "form_id": "livechatWaitingContact", "name": "Waiting contact", "override_targets": [], "submit_actions": [] } ] } ``` ### Display | Field | Purpose and render location | Null semantics | | ---------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | | `display.name` | Header/title bar name. Server falls back from header title to chat window title to `chatbot_id`. | Never null. | | `display.avatar.url` | Assistant avatar image used beside assistant messages and in the header area when present. | `null` means no custom avatar. | | `display.welcome_message` | Initial greeting shown at the start of a new conversation. | `null` means no configured greeting. | | `display.subtitle.text` | Header subtitle text below the display name. | `null` means no subtitle text. | | `display.subtitle.link` | Optional link rendered in the header subtitle area. | `null` means no subtitle link. | | `display.subtitle` | Combined subtitle object. When present, at least one of `text` or `link` is non-null. | `null` means no configured subtitle or link. | | `display.privacy_policy_url` | Footer privacy link. | Never null; server falls back to the default Diverge privacy policy. | ### Theme #### `theme.brand` | Field | Purpose and render location | Null semantics | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | | `primary_color` | Primary accent for title/link accents, send button background, product CTA fallback, and user message background fallback. Default `#1a1d56`. | Never null. | #### `theme.surface` | Field | Purpose and render location | Null semantics | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | `background_color` | Main chat window background behind messages, livechat panels, and composer. Default `#ffffff`. | Never null. | | `background_gradient_color` | Top title-bar and bottom footer fade overlay color. | `null` means derive the fade from `background_color` or white. | | `muted_text_color` | Header subtitle, footer links, livechat helper copy, close-rating copy, and session form helper text. Default `#707070`. | Never null. | #### `theme.header` | Field | Purpose and render location | Null semantics | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- | | `alignment` | Header title/subtitle alignment. Default `center`. | Never null. | | `logo.url` | Header logo above the title. | `null` means no custom logo. | | `button.background_color` | Header utility button group background for livechat, reset, minimize, and close controls. Default `rgba(242, 240, 239, 0.85)`. | Never null. | | `button.icon_color` | Header utility icon color. Default `#000000`. | Never null. | #### `theme.messages` | Field | Purpose and render location | Null semantics | | ------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `assistant.background_color` | Assistant message bubble background. Default `#e5eaf5`. | Never null. | | `assistant.text_color` | Assistant message text. Default `#262641`. | Never null. | | `assistant.avatar_size` | Assistant avatar width and height as a CSS length. Default `1.3em`. | Never null. | | `assistant.border_color` | Assistant message bubble border. | `null` means no border. | | `assistant.thinking_border_gradient` | Color stops for the assistant thinking-state border animation. | Empty array means the widget uses its built-in primary/neutral fallback. | | `user.background_color` | User message bubble background. Default is `theme.brand.primary_color`. | Never null. | | `user.text_color` | User message text. Default `#ffffff`. | Never null. | | `user.border_color` | User message bubble border. | `null` means no border. | #### `theme.input` | Field | Purpose and render location | Null semantics | | ------------------------ | ---------------------------------------------------------------------- | -------------- | | `text_color` | Composer textarea text. Default `#333333`. | Never null. | | `placeholder_color` | Composer placeholder text. Default `#a9a9a9`. | Never null. | | `background_color` | Composer field background before widget opacity. Default `#ffffff`. | Never null. | | `border_color` | Composer field outline color before widget opacity. Default `#000000`. | Never null. | | `height` | Composer minimum textarea height as a CSS length. Default `2.5em`. | Never null. | | `send_button.icon_color` | Send button arrow/icon color. Default `#ffffff`. | Never null. | #### `theme.launcher` | Field | Purpose and render location | Null semantics | | ----------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ | | `adaptive_icon_color_enabled` | Whether the launcher icon adapts to the host page background. Default `false`. | Never null. | | `image.desktop_url` | Desktop launcher image. | `null` means use the default launcher icon. | | `image.mobile_url` | Mobile launcher image. | `null` means use the desktop launcher image or default icon. | #### `theme.product_card` | Field | Purpose and render location | Null semantics | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | | `button.background_color` | Product card CTA and add-to-cart button background. Default is `theme.brand.primary_color`. | Never null. | | `button.bold` | Whether product CTA text is bold. Default `true`. | Never null. | | `discount_price_color` | Intended color for discount or previous-price text in product cards. The current web widget does not render this field yet. Default `#cc0000`. | Never null. | #### `theme.layout` | Field | Purpose and render location | Null semantics | | -------------------------- | ------------------------------------------------------------------ | -------------- | | `border_radius_multiplier` | Multiplies the widget's built-in default radii. `1` means default. | Never null. | #### `theme.font` | Field | Purpose and render location | Null semantics | | ------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------- | | `web.family` | CSS font-family value used by web clients. | Never null. | | `web.asset_url` | Hosted web font asset or stylesheet. | Never null. | | `web.format` | Web font asset format, or `stylesheet`. | Never null. | | `web.sha256` | SHA-256 hex digest of the web font file bytes, used as the client cache key. Not an integrity proof on its own. | Never null. | | `ios.asset_url` | Native iOS font asset. | Never null. | | `ios.format` | iOS font asset format. | Never null. | | `ios.sha256` | SHA-256 hex digest of the iOS font file bytes, used as the client cache key. Not an integrity proof on its own. | Never null. | | `android.asset_url` | Native Android font asset. | Never null. | | `android.format` | Android font asset format. | Never null. | | `android.sha256` | SHA-256 hex digest of the Android font file bytes, used as the client cache key. Not an integrity proof on its own. | Never null. | Prefer `sha256` over `asset_url` alone when deciding whether to reuse a cached font file. Defaults use the hosted Inter digest; a custom font without a stored hash falls back to the default asset + digest. Clients that need verification must hash the downloaded bytes and compare to the config value. ### Dark mode `dark_theme` is a required sibling of `theme` with the same shape. Choose the active theme once from the visitor's color scheme and apply it directly: ```ts const activeTheme = prefersDark ? config.dark_theme : config.theme; ``` Do not merge `dark_theme` with `theme`, and do not add null checks or fallback logic. When dark mode is not configured for a chatbot, the server returns `dark_theme` equal to `theme`, so existing chatbots keep their light appearance for dark-scheme visitors. Dashboard users configure this in the Chatbot page under Branding → Dark mode. In v1, assets and mode-independent fields are shared across modes: header logo, launcher images, assistant avatar size, input height, product button boldness, layout, and font assets. | Dark field fallback | Default behavior | | --------------------------------------------- | --------------------------------------------------------- | | `brand.primary_color` | Light `theme.brand.primary_color` | | `surface.background_gradient_color` | `null` | | `surface.background_color` | `#1c1c1e` | | `surface.muted_text_color` | `#9ba1a6` | | `header.button.background_color` | `rgba(44, 44, 46, 0.85)` | | `header.button.icon_color` | `#ffffff` | | `messages.assistant.background_color` | `#2c2c2e` | | `messages.assistant.text_color` | `#f2f2f7` | | `messages.assistant.border_color` | Light `theme.messages.assistant.border_color` | | `messages.assistant.thinking_border_gradient` | Light `theme.messages.assistant.thinking_border_gradient` | | `messages.user.background_color` | Dark `brand.primary_color` | | `messages.user.text_color` | `#ffffff` | | `messages.user.border_color` | Light `theme.messages.user.border_color` | | `input.text_color` | `#f2f2f7` | | `input.placeholder_color` | `#8e8e93` | | `input.background_color` | `#2c2c2e` | | `input.border_color` | `#3a3a3c` | | `input.send_button.icon_color` | `#ffffff` | | `product_card.button.background_color` | Dark `brand.primary_color` | | `product_card.discount_price_color` | `#ff6b6b` | ### Livechat | Field | Purpose and render location | Null semantics | | --------------------------- | ------------------------------------------------------------------------------------------------------ | -------------- | | `enabled` | Whether a visitor can request a live agent now. Default `false`. | Never null. | | `configured` | Whether livechat is configured and can become available. Default `false`. | Never null. | | `availability_status` | Current live/offline state for livechat UI. Default `offline`. | Never null. | | `availability_reason` | Why livechat is live or offline. Default `disabled`. | Never null. | | `show_livechat_logo` | Controls whether the livechat header button/logo is shown before a handover is active. Default `true`. | Never null. | | `attachments_enabled` | Whether livechat attachments are allowed. Default `false` unless livechat is configured. | Never null. | | `max_attachment_size_bytes` | Attachment upload limit in bytes. Default `5242880`. | Never null. | ### Popup messages | Field | Purpose and render location | Null semantics | | ------------------------------ | ----------------------------------------------------------------- | -------------- | | `popup_messages[].message` | Popup text shown when the current page URL matches `url_pattern`. | Never null. | | `popup_messages[].url_pattern` | URL pattern that triggers the popup. | Never null. | An empty array means no URL-triggered popups are configured. ### Forms | Field | Purpose and render location | Null semantics | | -------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------ | | `forms[].form_id` | Stable form identifier. Fetch the full definition with `GET /api/v1/chat/forms/{form_id}`. | Never null. | | `forms[].name` | Optional operator-facing form name. | `null` means unnamed. | | `forms[].trigger` | When the form can be used, such as `livechat_waiting` or `session_start`. | Never null. | | `forms[].override_targets` | Built-in action targets this form replaces. | Empty array means no overrides. | | `forms[].submit_actions` | Post-submit actions such as saving a lead or starting livechat. | Empty array means no extra submit actions. | An empty array means no dynamic forms are available to this visitor/session. ### Dashboard label to API field | Dashboard label | API field | | --------------------------- | -------------------------------------------- | | Primary color | `theme.brand.primary_color` | | Avatar | `display.avatar.url` | | Header logo | `theme.header.logo.url` | | Header Title | `display.name` | | Header Subtitle | `display.subtitle.text` | | Privacy Policy URL | `display.privacy_policy_url` | | Background gradient | `theme.surface.background_gradient_color` | | Chat background | `theme.surface.background_color` | | Muted text | `theme.surface.muted_text_color` | | Header button background | `theme.header.button.background_color` | | Header button icon | `theme.header.button.icon_color` | | Assistant message | `theme.messages.assistant.background_color` | | Assistant message text | `theme.messages.assistant.text_color` | | Assistant message border | `theme.messages.assistant.border_color` | | Avatar size | `theme.messages.assistant.avatar_size` | | User message | `theme.messages.user.background_color` | | User message text | `theme.messages.user.text_color` | | User message border | `theme.messages.user.border_color` | | Input text | `theme.input.text_color` | | Input placeholder | `theme.input.placeholder_color` | | Input background | `theme.input.background_color` | | Input border | `theme.input.border_color` | | Input field height | `theme.input.height` | | Send button icon | `theme.input.send_button.icon_color` | | Button background | `theme.product_card.button.background_color` | | Discount price | `theme.product_card.discount_price_color` | | Border Radius | `theme.layout.border_radius_multiplier` | | Custom Chat Button Image | `theme.launcher.image.desktop_url` | | Mobile Chat Button Image | `theme.launcher.image.mobile_url` | | Adaptive start button color | `theme.launcher.adaptive_icon_color_enabled` | import { EmbedGallery } from "../../components/embed-gallery"; ## Chatbot Integration The standard Diverge chatbot is a **floating widget**: a launcher on your site that opens a chat window over the page. This guide is the starting point — load the script, open the chat, optionally attach session metadata.
A website with a round chat button in the corner and a chat window open over the page.
Want the chatbot to look like a search bar, sit inside the page, or fill the screen? Pick a layout on [UI embeddings](/guides/ui-embeddings) instead:
### Add the script Load this on every page where the widget should appear: ```html ``` `window.DialogIntelligens` is available as soon as the script loads. You can call it before the widget has finished initializing. ### Cookie consent By default the script keeps a visitor key in a first-party cookie (`di_visitor_`) and in `localStorage`. It uses the key for analytics, A/B experiments, chat-open and purchase tracking, Shopify cart attributes and [Orders API](/guides/orders-api) stamps. It also keeps the date of the visitor's last visit in `localStorage`, so the daily visitor count includes each visitor once a day. If the visitor has not accepted non-essential cookies, add `data-consent="essential"` to the script tag. The chatbot still works, but in essential mode: * the script reads, creates and sends no visitor key and sets no `di_visitor_*` or `di_stamp_*` cookie; * nothing is sent for analytics, experiments, chat opens or purchases, and the Shopify cart gets no visitor attributes; * the page view is still counted without any identity (only the device type and whether the page shows a product), but no visit date is kept, so the visitor is not in the daily visitor count; * the chat window keeps the conversation for the browser tab only. It survives a reload in the same tab, but not closing the tab, and other tabs start their own conversation. ```html ``` When your consent tool reports the visitor's choice, pass it on. A tool that already knows a stored choice on page load should do so as soon as the chatbot API is ready: ```js function reportConsent(accepted) { window.DialogIntelligens.setConsent(accepted ? "all" : "essential"); } if (window.DialogIntelligens) reportConsent(visitorAcceptedCookies()); else window.addEventListener("di-chatbot-api-ready", () => reportConsent(visitorAcceptedCookies())); ``` * `setConsent("all")` starts the visitor key and tracking at once. The conversation of the current tab is kept from then on, unless the browser already keeps one from an earlier visit, which is shown instead. * `setConsent("essential")` stops tracking at once and removes the visitor key, its cookies and the Shopify cart attributes the script stored earlier. Use it when the visitor refuses or withdraws consent. The chat window then starts a conversation for the tab only; a conversation it kept earlier stays in the browser unread, and is shown again if the visitor consents later. `data-consent="essential"` alone removes nothing, so a visitor who consented earlier keeps their identity once you call `setConsent("all")`. A chat window that has already loaded reloads when consent changes, so report a stored choice before the visitor opens the chat. Consent applies to the page where you set it: report it on every page, and put the attribute on every Diverge script tag, including the layouts on [UI embeddings](/guides/ui-embeddings). An unrecognized value is treated as `"essential"`. ### Availability and service health Use availability and health subscriptions for every custom entry point, whether or not your site uses Shopify or currently runs an on/off experiment. #### Custom buttons, links, and search forms There are two independent signals: * **Availability** decides whether this visitor may use chat. Its reasons remain `normal`, `experiment`, or `unavailable`. The subscription reports the current value once configuration is ready, then any changes. On/off experiment assignments persist through outages. * **Service health** reports `checking`, `available`, or `unavailable`. It checks database availability and whether the API-owned AI runtime is ready to accept conversations. It does not guarantee the next model response will succeed. The getter waits for the initial check; subscriptions immediately report `checking` while it runs. Destroying the integration settles a pending health getter as `unavailable`. Start your chat entry point hidden, then enable it only when availability has `enabled: true` and health has `status: "available"`. You own this UI: you can hide it, disable it, or offer your normal search/contact experience. Diverge never modifies your custom buttons or forms. This complete example preserves a contact link if the loader fails to download, configuration is still loading, the visitor is in Bot off, or the service is unavailable: ```html Contact us ``` To send a pre-filled question, replace `api.open()` with `api.open("I need help with my order")`. Use the same subscriptions around an anchor, a custom search form, or an image-upload interface. For a search form, keep its normal search route as the fallback. Hide the complete chat wrapper so loading or Bot off leaves no empty space. The example loads the script synchronously before binding the subscriptions. If you use `async` or inject the script later, bind in its `load` handler and preserve the fallback on `error`. Keep the loader installed for both experiment groups so assignment and measurement continue. Do not infer assignment from cookies, the visible launcher, or whether someone starts a chat. When caching, self-hosting, or pinning script versions, update the chatbot loader and its UI scripts together and refresh both caches. The updated search bar stays hidden if an older loader does not expose service health; refresh the loader too rather than enabling the search manually. The built-in launcher, popup, inline search bar, and unopened inline embed handle both signals automatically, including hidden-mode installations. An outage leaves already-open conversations in place with their existing messages, errors, and retry controls. Experiment Bot off continues to suppress chat. Legacy custom code keeps its existing `open()`/`show()` behaviour; it gains health-aware entry points by adopting the subscriptions above. Health follows the loader's own configuration requests, so it adds no requests to your pages. It is checked every 30 seconds while the chat is open or an experiment runs. While the service is unavailable, the loader retries at growing intervals of up to five minutes, or a few seconds after the visitor's connection returns. A visitor who returns to the tab checks it again after five minutes. The loader allows eight seconds for each request. Transient configuration failures retry automatically; a successful refresh restores eligibility without a page reload. For your own monitoring, the public, cross-origin `GET /api/v1/chatbot-public/health` endpoint on your chatbot backend returns HTTP 200 with `{ "status": "available" }` or HTTP 503 with `{ "status": "unavailable" }`, with `Cache-Control: no-store`. The subscriptions above handle network failures and timeouts for your integration. If the chat is already open, `open(message)` still delivers the message. ### Open the chat `open()` opens the widget if it is closed and never toggles it shut. #### From a URL Append `?chat=open` to any page where the script is loaded. The chat opens on page load, then the parameter is stripped from the address bar. ```text https://example.com/page?chat=open ``` Include a pre-filled question with `chatbot_message`. Using that parameter alone also opens the chat: ```text https://example.com/page?chat=open&chatbot_message=I+need+help+with+my+order ``` ```text https://example.com/page?chatbot_message=What+are+your+opening+hours%3F ``` These parameters run once per page load. They work well for email campaigns, QR codes, and FAQ links. #### With an image Your site owns the upload UI. Pass the visitor's `File`; Diverge opens the chatbot and sends the image as the first message. Text is optional. ```html ``` Images must be valid image files no larger than 5 MB. Diverge resizes large images before they enter the normal chatbot image flow. Invalid or oversized files are rejected. #### From your own widget Use the supported public API from your availability- and health-controlled component. It opens and initializes the chat; no iframe selector or fixed delay is needed. ```js if (eligible && healthy) { window.DialogIntelligens.open("I need help with my order", "custom-widget"); } ``` Ready-made search bar and in-page layouts live on [UI embeddings](/guides/ui-embeddings). ### JavaScript API | Method | Description | | --------------------------------- | ---------------------------------------------------------------------------------- | | `open()` | Opens the chat window | | `open(message)` | Opens the chat window and sends a pre-filled message | | `open({ image, message? })` | Opens the chat and sends an image with optional text | | `hide()` | Hides the chat button and iframe completely | | `show()` | Shows the chat button (closed state) | | `setMetadata(metadata)` | Attaches metadata to the visitor's session (deep-merged) | | `setSignedMetadata(token)` | Attaches **verified** metadata from a backend-signed RS256 JWT | | `destroy()` | Removes the chatbot from the page entirely | | `getAvailability()` | Resolves to the visitor's current eligibility: `{ enabled, reason }` | | `onAvailabilityChange(callback)` | Subscribes to eligibility changes; returns an unsubscribe function | | `getServiceHealth()` | Resolves after the initial health check, then returns current health: `{ status }` | | `onServiceHealthChange(callback)` | Immediately reports current health, then changes; returns an unsubscribe function | | `getVisitorStamp()` | Resolves to the visitor stamp for [Orders API](/guides/orders-api) orders, or null | | `setConsent(mode)` | `"all"` or `"essential"`; see [Cookie consent](#cookie-consent) | `setMetadata(metadata)` is safe to call at any time after the script loads; patches are queued until the widget is ready and retried until delivered. `destroy()` drops patches that have not been delivered yet. ### Attaching session metadata Use `setMetadata()` to attach context from your page to the visitor's chat session — for example a customer id, plan, or cart state: ```html ``` **Semantics:** * Safe to call as soon as the chatbot script has loaded — even before the widget finishes initializing. Patches are queued and delivered once the widget is ready. * Repeated calls **deep-merge** into the existing session metadata: nested objects merge, scalars and arrays are replaced. There is no way to delete a key. * The merged metadata is limited to **32 KiB** (JSON-encoded) and 20 levels of nesting. The keys `__proto__`, `prototype`, and `constructor` are rejected. * The top-level keys `verified_metadata` and `signed_metadata` are reserved for [verified metadata](#verified-signed-metadata): a patch that uses either is rejected and not retried, and none of its keys are stored. * `destroy()` drops patches that have not been delivered yet; metadata that already reached the server stays on the session. **Delivery:** the session metadata appears as `session.metadata` in every livechat webhook event and in the livechat agent context (including the Intercom bridge), exactly like metadata set through the server-side Chatbot API (`PATCH /api/v1/chat/session/metadata`). **Dashboard:** open a conversation's **Session context** to see these values under **Session metadata · Unverified**. Nested objects and arrays can be expanded. This section is separate from verified customer data, and is hidden when there is no metadata. On the **Livechat** page, these values also appear in a separate **Session metadata · Unverified** row beneath the conversation header, alongside signed metadata, without expanding Session context. To have Diverge **call your API** during a conversation (for example order or delivery lookups), see [Universal API Integration](/guides/universal-api-integration). Metadata is for session context on webhooks and livechat; runtime variables on that guide are what forward browser tokens into outbound API requests. :::warning Metadata set through `setMetadata()` comes from the visitor's browser — any visitor can call it from the developer console. Treat it as untrusted input: never use it for authorization, entitlement, or billing decisions downstream. If metadata must be trustworthy, use [verified (signed) metadata](#verified-signed-metadata) instead. ::: #### Verified (signed) metadata When downstream systems must be able to **trust** metadata — for example an account id used to look up orders — have your backend sign it as an RS256 JWT and pass the token through the widget with `setSignedMetadata()`. Our API verifies the signature against a public key you configure and stores the claims separately as `verified_metadata`, so consumers can always distinguish trusted from untrusted data. **1. Generate a keypair** (keep the private key on your backend; you will paste only the public key into the dashboard): ```bash openssl genrsa -out signed-metadata-private.pem 2048 openssl rsa -in signed-metadata-private.pem -pubout -out signed-metadata-public.pem ``` **2. Configure the public key** in the dashboard under **Developer → Signed metadata**, per chatbot and environment. Signed metadata is enabled as soon as a key is saved and disabled when the key is removed. **3. Sign a token on your backend** when the visitor is authenticated. The token's claims become the verified metadata: ```js import jwt from "jsonwebtoken"; const token = jwt.sign( { sub: "account-123", logged_in: true, store: "sweden", }, privateKeyPem, { algorithm: "RS256", expiresIn: "10m" }, ); ``` **4. Pass the token through the widget** on the page: ```html ``` **Token requirements:** * Signed with **RS256** using the private key matching the configured public key. * Must include `exp` and `iat`, with a lifetime of **15 minutes or less** (`exp - iat ≤ 900`). * Registered claims (`iss`, `aud`, `exp`, `iat`, `nbf`, `jti`) are stripped; `sub` and all custom claims are kept and deep-merged into `verified_metadata`. * The same 32 KiB / 20-level / blocked-key limits as `setMetadata()` apply to the merged result. **Semantics:** queueing and retry behave like `setMetadata()` — safe to call before the widget is ready. A token that fails verification (bad signature, expired, oversized) is rejected once and **not retried**; sign a fresh token and call `setSignedMetadata()` again. **Delivery:** verified claims appear as `session.verified_metadata` — alongside but separate from `session.metadata` — in every livechat webhook event and the livechat agent context. In the Intercom bridge, a verified `sub` (or `user_id`) claim becomes the Intercom contact `external_id` so the conversation attaches to the contact you already have for that user, verified `email`/`name` take precedence for the contact identity, and top-level scalar claims are exposed as `di_verified_*` attributes. Client-side `setMetadata()` calls can never write into `verified_metadata`, and `session.metadata` never contains a `verified_metadata` or `signed_metadata` key. **Dashboard:** verified claims are shown to agents on the livechat conversation and in the Conversations session context, and the free-text search on both pages matches claim values (for example a customer id or email), so agents can find every chat from the same signed-in customer. ### Related reading * [UI embeddings](/guides/ui-embeddings) — search bar, inline chat, fullscreen, and React components * [Chatbot Config & Theming](/guides/chatbot-config) — colors, launcher, and display settings * [Universal API Integration](/guides/universal-api-integration) — call your APIs from a conversation ## Conversation Export The chatbot analytics events give you the front-end side of a chatbot session. This guide covers the other half: pulling the conversations themselves — the stored rating, topic, and where they were escalated to — into your own data warehouse, so chatbot journeys can be joined with traffic, order, and helpdesk data on your side. Two endpoints do the work: | Endpoint | Returns | | ------------------------------------- | --------------------------------------------------------- | | `GET /api/v1/chat/conversations` | Conversation-level metadata and stored signals, paginated | | `GET /api/v1/chat/conversations/{id}` | One conversation including the full transcript | Both live under the management API and are documented in full in the [API Reference](/api). ### Authentication Use a chatbot API key with Basic auth, the same key used for the statistics and CSAT endpoints. The key prefix decides the environment: a `live_` key only ever returns live conversations, a `test_` key only test conversations. ```bash curl https://api.dialogintelligens.dk/api/v1/chat/conversations?chatbot_id=YOUR_CHATBOT_ID \ -u 'live_yourkey_yoursecret:' ``` ### What one conversation looks like ```json { "conversation_id": "conversation_42", "chatbot_id": "shop-bot", "environment": "live", "visitor_id": "visitor_123", "created_at": "2026-07-10T14:02:00.000Z", "updated_at": "2026-07-10T14:22:00.000Z", "message_count": 8, "topic": "Returnering", "tags": ["retur"], "customer_rating": 2, "rating_feedback": "Fik ikke svar på mit spørgsmål", "quality_score": 4, "fallback": true, "lacking_info": false, "escalation": { "livechat": false, "livechat_session_id": null, "livechat_requested_at": null, "support_ticket": true, "support_ticket_provider": "zendesk", "support_ticket_id": "48211", "support_ticket_status": "completed" }, "source": "website", "split_test_id": null } ``` ### Signals for good and bad journeys Every field is stored as-is — the API does not classify a conversation as good or bad, so the definition stays yours. The fields worth building on: | Field | Meaning | | ----------------- | --------------------------------------------------------------- | | `customer_rating` | The visitor's own 1-5 rating, or `null` if they did not rate | | `rating_feedback` | Free-text the visitor left with the rating | | `quality_score` | The automatic 1-10 conversation score, or `null` if not scored | | `fallback` | The bot answered with its fallback response at least once | | `lacking_info` | The knowledge base was missing information for the question | | `escalation` | Whether and where the visitor was handed to a human (see below) | | `topic` | The automatically classified topic, matching the dashboard | ### Following journeys into Zendesk or Freshdesk When the visitor is handed over, `escalation` records where the journey continued: * `livechat` and `livechat_session_id` for conversations taken over by an agent in Diverge livechat. * `support_ticket_provider` and `support_ticket_id` for conversations that created a helpdesk ticket. `support_ticket_id` is the ticket number in Zendesk or Freshdesk, so it joins directly against your helpdesk data. `support_ticket_id` is `null` while a ticket is still queued (`support_ticket_status` is `pending` or `processing`) and for tickets created before ticket linking was introduced; `support_ticket` still tells you the visitor submitted the form. Narrow the list to escalated journeys with `escalated=true`, or to the ones the bot handled on its own with `escalated=false`. ### Incremental syncs `updated_at` is the export watermark. It is the most recent of conversation creation, rating submission, livechat session activity, and helpdesk ticket delivery — so a conversation that gets rated two days after it started shows up again in the next sync. Store the highest `updated_at` you have loaded and pass it back as `updated_since`: ```bash curl -G https://api.dialogintelligens.dk/api/v1/chat/conversations \ -u 'live_yourkey_yoursecret:' \ --data-urlencode 'chatbot_id=YOUR_CHATBOT_ID' \ --data-urlencode 'updated_since=2026-07-09T00:00:00Z' \ --data-urlencode 'limit=100' ``` When back-filling a large history, combine `updated_since` with a `start_date`/`end_date` window and walk the range month by month. `start_date` and `end_date` filter on `created_at` and must be supplied together. ### Pagination Results are ordered newest first and paginated with an opaque cursor. Keep requesting until `next_cursor` is `null`: ```js async function fetchAll(chatbotId, updatedSince) { const auth = Buffer.from(`${process.env.DIVERGE_API_KEY}:`).toString("base64"); const conversations = []; let cursor = null; do { const url = new URL("https://api.dialogintelligens.dk/api/v1/chat/conversations"); url.searchParams.set("chatbot_id", chatbotId); url.searchParams.set("limit", "100"); if (updatedSince) url.searchParams.set("updated_since", updatedSince); if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers: { Authorization: `Basic ${auth}` } }); if (!response.ok) throw new Error(`Conversation export failed: ${response.status}`); const page = await response.json(); conversations.push(...page.items); cursor = page.next_cursor; } while (cursor); return conversations; } ``` ### Reading a transcript Fetch a single conversation when you need the messages themselves — for example to review the low-rated conversations the list surfaced: ```bash curl https://api.dialogintelligens.dk/api/v1/chat/conversations/conversation_42 \ -u 'live_yourkey_yoursecret:' ``` The response is the same record plus `form_data` (the stored contact or support ticket form) and `history`, the full message list in the same shape the chatbot API uses everywhere else. Transcripts contain whatever visitors typed, so treat them as personal data: pull them on demand for the conversations you actually need rather than mirroring every transcript into your warehouse. ### Related * [Chatbot Analytics](/guides/chatbot-analytics) — front-end events for Google Tag Manager. * [API Reference](/api) — every parameter and response field. ## Intercom Livechat Use the Intercom integration when you want human livechat requests from Diverge to become conversations in Intercom. When a visitor requests livechat, Diverge creates or updates the Intercom contact, opens an Intercom conversation, adds the chat history, and keeps the visitor chat in sync when an Intercom teammate replies. ### What gets synced | Action in Diverge or Intercom | What happens | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Visitor requests livechat | A new Intercom conversation is created with the title `Diverge livechat request`. | | Visitor sends another message | The message is sent to the same Intercom conversation. This includes messages the visitor sends to the AI assistant while waiting in the livechat queue, so the Intercom conversation stays active while the visitor waits. | | Intercom teammate or operator replies | The reply appears in the visitor chat as an agent message. | | Intercom conversation is closed | The visitor livechat session is closed in Diverge. | | Visitor leaves a rating | Diverge sends the rating to Intercom where the workspace supports it. | The first Intercom message includes the recent chat history, so the teammate can see what the visitor already asked before livechat started. ### Before you start You need: * Access to the Diverge dashboard for the organization. * Admin access to the Intercom workspace. * Livechat enabled on at least one Diverge chatbot. * The Intercom workspace region: Default / US, Europe, or Australia. ### Connect Intercom 1. Open the Diverge dashboard. 2. Go to **Integrations**. 3. Find **Intercom** and click **Connect**. 4. Approve the Intercom app when Intercom asks for permission. 5. Return to the Diverge dashboard. 6. Turn on **Sync to Intercom**. 7. Select the chatbots that should send livechat requests to Intercom. 8. Choose the Intercom API region that matches your Intercom workspace. 9. Save the livechat sync settings. #### Default Intercom admin ID The default Intercom admin ID is only needed when Diverge sends an agent reply or close action into Intercom. You do not need it for normal Intercom-handled livechat where the teammate replies inside Intercom and the visitor sees the reply in the Diverge chat. ### Default Intercom People fields Diverge sends the customer context to Intercom contact fields when Intercom supports the field in your workspace. | Intercom People field | Value from Diverge | | --------------------- | -------------------------------------------------------------------------------------------------------------------------- | | External ID | Your own user id when the page passes it as verified metadata (see below). Otherwise the anonymous Diverge widget user id. | | Current Page URL | The page the visitor was viewing when livechat was requested. | | Browser | Visitor browser name. | | Browser version | Visitor browser version. | | Browser language | Visitor browser language. | | OS | Visitor operating system. | | Language | Visitor or chatbot language. | | Signed up | Visitor signup time, if available. | | Last seen | When the visitor requested livechat. | | Conversation Rating | The visitor's last submitted livechat rating, where Intercom supports it. | These are standard Intercom fields. You should not create custom attributes with the same names. #### Matching your own users If your visitors are logged in on your site and you want the Intercom conversation to attach to the contact you already have for that user, pass the user id as **verified (signed) metadata** through the widget. Sign a token on your backend with the user id as the `sub` claim and call `window.DialogIntelligens.setSignedMetadata(token)` when the user is logged in. Diverge then uses that `sub` value as the Intercom `external_id` when the livechat is handed over, so the conversation lands on the same contact as a direct Intercom chat. A `user_id` claim is accepted as an alias for `sub`. Setup instructions and token requirements are in the [chatbot integration guide](/guides/chatbot-integration#verified-signed-metadata). Unsigned `setMetadata()` values are never used as the Intercom identity. ### Optional custom attributes Custom attributes are optional. Livechat works even if you do not create any custom attributes in Intercom. Create custom attributes only if your Intercom team wants extra fields for filtering, reporting, or showing context directly in the inbox sidebar. Custom attributes must be created in Intercom before Diverge can update them. #### People custom attributes Create these under **Settings > Data > People** if you want the values stored on the Intercom contact. | Attribute name | Suggested type | What it means | | ------------------------ | -------------- | ------------------------------------------------------ | | `di_chatbot_id` | Text | The Diverge chatbot that created the livechat request. | | `di_environment` | Text | The chatbot environment, such as `live` or `test`. | | `di_livechat_session_id` | Text | The Diverge livechat session ID. | | `di_visitor_session_id` | Text | The visitor session ID. | | `di_visitor_user_id` | Text | The anonymous Diverge widget user ID. | #### Conversation custom attributes Create these under **Settings > Data > Conversations** if you want the values visible on each Intercom conversation. | Attribute name | Suggested type | What it means | | ------------------------------------- | -------------- | ------------------------------------------------------------- | | `di_current_page_url` | Text or URL | The page the visitor was viewing when livechat was requested. | | `di_os` | Text | Visitor operating system. | | `di_browser` | Text | Visitor browser name. | | `di_browser_version` | Text | Visitor browser version. | | `di_browser_language` | Text | Visitor browser language. | | `di_language` | Text | Visitor or chatbot language. | | `di_chatbot_id` | Text | The Diverge chatbot that created the livechat request. | | `di_environment` | Text | The chatbot environment, such as `live` or `test`. | | `di_livechat_session_id` | Text | The Diverge livechat session ID. | | `di_visitor_session_id` | Text | The visitor session ID. | | `di_visitor_user_id` | Text | The anonymous Diverge widget user ID. | | `di_conversation_rating` | Number | The visitor's submitted livechat rating. | | `di_conversation_rating_remark` | Text | The visitor's optional rating comment. | | `di_conversation_rating_submitted_at` | Text | When the visitor submitted the rating. | If an attribute is missing in Intercom, Diverge skips that field and still creates the livechat conversation. ### Notes for Intercom admins * Use exact attribute names. Intercom custom attribute names are case sensitive. * Do not create custom attributes that duplicate Intercom standard People fields. * Keep custom attributes simple. Text, number, boolean, date, and URL values work best. * If a custom attribute is archived or not writable through the API, Diverge will skip it. ### Intercom references * [Tracking user data in Intercom](https://www.intercom.com/help/en/articles/320-tracking-user-data-in-intercom) * [Create and track custom data attributes](https://www.intercom.com/help/en/articles/179-send-custom-user-attributes-to-intercom) ## Orders API Stores that are not on a platform Diverge connects to directly send their orders through the Orders API. Your backend sends the whole current order every time it changes; your checkout copies one opaque value, the **visitor stamp**, into each order. Diverge never receives money from the browser: order totals, refunds and returns come only from your backend. ### Set up in the dashboard 1. Open **Integrations → Custom store (Orders API)** and choose **Add store**. 2. Name the store and select the chatbots that run on its storefronts. Each chatbot is one market of the store; a chatbot belongs to one store at a time. 3. Create a **live** key and a **test** key. The full key is shown once, so store it in your backend's secret store right away. 4. Note the stamp cookie of each chatbot, shown on the store: `di_stamp_`. Only organization owners can manage stores and keys. ### Step 1: capture the visitor stamp at checkout The Diverge script keeps a stamp for each visitor on your own domain. Save it on the order when the order is created. Treat it as opaque: store and send it unchanged, never parse it. Its format can change without you changing anything. Diverge signs every stamp. An edited or made-up stamp is reported as `unrecognized` and the order is not linked to a visitor. A linked order records which visitor's stamp it carried; it does not prove who placed the order. A chatbot starts getting stamps on its next page load after it joins a store (allow up to a minute for caches); a page that was already open gets one when the script next loads its settings. **Server-rendered checkout on the same domain**: read the cookie in your checkout handler. ```js const stamp = req.cookies[`di_stamp_${DIVERGE_CHATBOT_ID}`] ?? null; order.divergeStamp = stamp; ``` **Headless storefront, or checkout on another domain**: read it in the browser and send it with the checkout request. ```js const stamp = (await window.DialogIntelligens?.getVisitorStamp?.()) ?? null; await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ ...cart, divergeStamp: stamp }), }); ``` The stamp is missing when the Diverge script is not on the page. Leave the field out in that case: the order still counts in store totals, it is just not linked to a visitor. ### Step 2: send order snapshots Send the whole current order whenever it changes: placed, paid, cancelled, edited, refunded or returned. Authenticate with HTTP Basic: the commerce key is the username and the password is empty. ```bash curl -X PUT https://api.dialogintelligens.dk/api/v1/commerce/orders/100234 \ -u "live_cs…:" -H "Content-Type: application/json" -d @order.json ``` ```json { "order_number": "100234", "placed_at": "2026-10-01T14:03:11Z", "updated_at": "2026-10-01T14:05:42.120Z", "currency": "DKK", "total": "1299.00", "subtotal": "1199.00", "discount_total": "100.00", "shipping_total": "49.00", "tax_total": "259.80", "payment_status": "paid", "cancelled_at": null, "sales_channel": "web", "test": false, "visitor_stamp": "v0.eyJj…", "line_items": [ { "line_id": "1", "product_id": "8812", "variant_id": "8812-42", "sku": "TS-RED-42", "quantity": 2, "unit_price": "599.50", "total": "1199.00" } ], "refunds": [ { "refund_id": "r1", "created_at": "2026-10-05T09:12:00Z", "amount": "599.50", "line_items": [{ "line_id": "1", "quantity": 1 }] } ], "returns": [ { "return_id": "ret1", "received_at": "2026-10-04T16:40:00Z", "line_items": [{ "line_id": "1", "quantity": 1 }] } ] } ``` | Field | Rule | | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `placed_at`, `updated_at`, `currency`, `total`, `payment_status` | Required. `updated_at` must increase with every change to the order. | | Amounts | Decimal strings in the shop's own currency, with no more decimals than the currency allows (DKK 2, JPY 0). | | `total` | What the customer pays, including VAT and shipping. Diverge calculates the net value from `refunds`. | | `payment_status` | `pending`, `authorized`, `paid`, `failed` or `voided`. | | `refunds` and `returns` | A refund is money back; a return is goods received back. Send whichever happened. | | `product_id`, `sku` | Use the ids of the product feed the chatbot uses. | | `chatbot_id` | Only for stores with several chatbots, to credit a market when the order has no stamp. The stamp wins. | | Customer data | There are no name, email or address fields. Requests with unknown fields are refused, so none slip in. | #### Responses | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------- | | `200` | `result` is `applied` (stored), `stale` (a newer version is stored) or `unchanged` (identical). | | `409` | `conflict`: same `updated_at` but different content. Increase `updated_at` and resend. | | `410` | `gone`: the order was erased and cannot be recreated. | | `413` | `payload_too_large`: the body is over 4 MB. Send fewer orders per batch. | | `422` | `validation`: `params` names each failing field. | | `429` | `rate_limited`: wait the number of seconds in `Retry-After`, then retry. | | `401` | `unauthorized`: the key is missing, wrong or revoked. | Errors share one shape: `{ "error": { "code": "…", "message": "…", "params": [...] } }`. ### Other operations | Method | Path | Purpose | | -------- | ------------------------------------ | ---------------------------------------------------------------------------- | | `POST` | `/api/v1/commerce/order-batches` | Up to 100 snapshots, each with its `order_id`. See [batches](#batches). | | `GET` | `/api/v1/commerce/orders/{order_id}` | The order as Diverge stores it, to check an integration. | | `DELETE` | `/api/v1/commerce/orders/{order_id}` | Erases the order, e.g. for a deletion request. Later snapshots answer `410`. | | `GET` | `/api/v1/commerce/connection` | The key's store, environment, chatbots and when the last order arrived. | The full schema is in the [API reference](/api). #### Batches A batch is checked as a whole first: if any snapshot is malformed (a missing field, an unknown field, an amount that is not a decimal string), the whole request answers `422` and lists the fields, for example `orders.2.total`, and nothing is stored. Otherwise each order is stored on its own and the response has one result per order: `applied`, `unchanged` or `stale`, or an error (`validation`, `conflict` or `gone`) for that order only, such as a JPY amount with decimals or an erased order. ### Step 3: keep it correct * Send from a queue or outbox so checkout never waits for Diverge. Retry on `5xx` and `429`. * Once a night, resend every order updated in the last 7 days through `/order-batches`. Sending the same snapshot again is safe, and it repairs anything a failed request missed. * Each key can make 600 requests a minute. A batch counts as one request. ### Step 4: test 1. Use the `test_` key. Test orders are kept apart from live data. 2. Place an order after chatting, refund one line and cancel another order. 3. On the store in **Integrations**, the test line shows the orders, and how many are linked to a visitor. ### Privacy * Order snapshots contain no customer contact or address data. * When a customer asks you to delete their data, call `DELETE /orders/{order_id}` for their orders. Diverge keeps only a keyed digest of the order id, to refuse later snapshots of it. * When a visitor deletes their chatbot data, Diverge removes their link from every order, and later snapshots of those orders are stored without it. Their browser then starts over as a new visitor with a new stamp, so only what they do afterwards can be linked again. ## Product Feed A product feed gives Diverge the catalogue information it needs to answer product questions and recommend the right products. In most cases, the same feed you already use for Google Shopping is a good starting point. ### Accepted formats We can work with a feed that is available at a stable URL in any of these formats: * XML (including Google Shopping feeds) * JSON * CSV The URL should be accessible without an interactive login. If access must be restricted, share the authentication requirements with your Diverge contact. ### Required product information Every product should have the following fields. Field names do not need to match these names exactly, as long as their meaning is clear. | Field | Description | | ------------ | ---------------------------------------------------------------- | | Product ID | A unique, stable identifier for the product. | | Title | The customer-facing product name. | | Description | A useful description of the product. | | Product URL | The canonical page where a customer can view or buy the product. | | Image URL | A publicly accessible URL for the main product image. | | Price | The current selling price, including its currency. | | Availability | Whether the product is currently available to buy. | ### Recommended product information Include as much of the following information as your catalogue supports. More complete data helps the chatbot compare products and give customers useful, accurate answers. * Brand * Product type or category * Additional images * Sale price and original price * Colour, size, material, and other product attributes * SKU, GTIN, EAN, MPN, or other identifiers * Tags or collections * Stock status or quantity * Shipping weight or dimensions Do not include private customer information, internal credentials, or any data that should not be shown to customers. ### Variants If a product is sold in variants, include the relationship between each variant and its parent product. Each variant should have its own stable identifier and the values that distinguish it, such as size or colour. Where price, availability, URL, or image differs by variant, provide the variant-specific value. ### Languages and markets Tell us if your catalogue covers more than one language, currency, or market. Ideally, each feed entry should make its language and target market clear. You can provide separate feeds per market or include market-specific values in one feed. Prices must always include a currency. Product URLs should lead to the appropriate language and market storefront. ### Keeping the feed up to date Provide a stable feed URL that Diverge can fetch repeatedly. Update the file at that URL whenever product details, prices, or availability change instead of creating a new URL. Tell your Diverge contact: * How often the feed is updated * How frequently it should be imported * Whether access requires authentication or IP allowlisting * Who to contact if the feed becomes unavailable ### Before you send the feed Use this checklist before sharing your feed: * [ ] Every product has a unique and stable ID * [ ] Titles and descriptions are suitable for customers * [ ] Product and image URLs are absolute and accessible * [ ] Prices include the correct currency * [ ] Availability values are present and up to date * [ ] Variants are linked to their parent products * [ ] Language and market are identifiable * [ ] The feed is available at a stable URL * [ ] No private or sensitive information is included When it is ready, send the feed URL and any access instructions to your Diverge contact. import { EmbedGallery } from "../../components/embed-gallery"; ## UI Embeddings Use these layouts when the standard [floating widget](/guides/chatbot-integration) is not the right fit. Click a picture to jump to its setup.
The floating widget script is still required for the search bar. Inline and fullscreen use a separate inline script. If you cache, self-host, or pin these scripts, update the loader and search script together and refresh both caches. The updated search bar stays hidden with an older loader that lacks health subscriptions. This applies to all websites, whether or not they use Shopify. All these Diverge-owned entry points handle visitor availability and service health automatically, on every website, including sites without Shopify. They collapse while loading, for Bot off, or while the service is unavailable, and recover when it is ready. Already-open conversations keep their messages and existing retry controls during outages. Experiment assignments never change because of service health. For custom buttons or fallback search/contact UI, follow [availability and service health](/guides/chatbot-integration#availability-and-service-health). ### Search bar An inline search bar can sit anywhere on the page. When the visitor types a question and presses Enter or clicks send, the **floating chatbot** opens and receives the message. The whole search widget hides during an outage and preserves unsubmitted text. It works with `hidden=true` on the universal loader; a visible launcher is not required. Use a dedicated, initially hidden container: the script owns its contents and visibility. Put any normal search fallback outside that container. #### Setup 1. Load the search bar script **after** the chatbot script: ```html ``` 2. Add a container where the search bar should appear: ```html ``` Use the same class for more than one search bar on a page: ```html ``` #### Wrap the query in a sentence By default the search bar sends exactly what the visitor typed. Use `data-message-template` to wrap the query in a fixed sentence so the chatbot gets context. `{query}` is replaced with whatever they typed: ```html ``` A visitor searching for `wine` makes the chatbot receive: ```text I am looking for sizing help with "wine", can you help me? ``` If you only need text before and/or after the query, use `data-message-prefix` and `data-message-suffix` instead: ```html ``` A visitor searching for `wine` makes the chatbot receive `What is the delivery time on wine?`. Notes: * Attributes are per widget, so different search bars on the same page can use different sentences. * `{query}` may appear multiple times in the template — every occurrence is replaced. * `data-message-template` takes precedence over `data-message-prefix`/`data-message-suffix` when both are set. * If `data-message-template` is set but does not contain `{query}`, the raw search word is sent and an invalid-template diagnostic is recorded. * All three attributes are optional. With none set, the search bar sends the raw search word. #### Manual initialization ```html ``` `messagePrefix` and `messageSuffix` are available here too. Data attributes on the container take precedence over the config object. `sendLabel` (or `data-send-label`) customizes the send button's accessible name. By default it uses the page language. Keyboard focus remains visible and reduced-motion preferences are honored. ### Inline chat Use the inline embed when the chatbot should render **in the page** instead of as a floating widget. Visitors chat without leaving the surrounding content. #### Setup ```html
``` If `#chatbot-placeholder` is missing, the script appends the chatbot next to the script tag. The unopened embed collapses while unavailable and recovers automatically. Once a conversation is open, an outage preserves it. The inline embed exposes the same public API and visitor assignment as the floating loader. Use one chatbot ID per page; duplicate scripts share a runtime. By default the inline embed starts as a collapsed search-bar landing state. The visitor types a first question, then the in-page chat expands. #### Welcome text Set `window.__CHATBOT_INLINE_OVERRIDES__` **before** loading `chatbotinline.js` to override inline-only behavior for that embed instance. ```html
``` `inlineWelcomeMessage` is the text shown above the input before the first question in the collapsed landing state. ### Fullscreen Fullscreen is the same inline embed, started already in the expanded chat view — with the chatbot's first message shown, instead of the collapsed search field. ```html
``` `fullscreenStartOpen` defaults to `false`, so existing inline embeds keep the search-bar-first behavior. You can combine it with a custom welcome line: ```html
``` ### React components React components for placing Diverge in your own UI are **coming soon**. They will let you customize where the chatbot sits in a React app, without dropping in the script layouts above. Until those components ship: * Use the [floating widget](/guides/chatbot-integration) for the standard launcher. * Use the [search bar](#search-bar), [inline chat](#inline-chat), or [fullscreen](#fullscreen) snippets on this page. ### Related reading * [Chatbot Integration](/guides/chatbot-integration) — floating widget, `open()`, and session metadata * [Chatbot Config & Theming](/guides/chatbot-config) — colors and display settings ## Universal API Integration Universal API Integration lets Diverge **call your HTTP endpoints** during a conversation — for example to look up order status, delivery times, or account details — and use the response in the assistant's reply. This guide covers how the feature works, how to configure it in the dashboard, and the authentication patterns your API can use. ### What this is (and what it is not) | Direction | Feature | Auth | Guide | | ----------------------------------- | -------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Diverge calls **your** API | Universal API Integration | Bearer, Basic, OAuth 1.0a, pre-request OAuth2, browser tokens | This guide | | **You** receive events from Diverge | Outbound livechat webhooks | HMAC signature (`x-di-signature`) | [Chatbot API Flow — webhooks](/guides/chatbot-api-flow#webhook-events-and-signing) | | Attach context to a chat session | Session metadata (`setMetadata`) | Untrusted browser input; optional signed JWT | [Chatbot Integration — metadata](/guides/chatbot-integration#attaching-session-metadata) | Universal API Integration is configured in the **Diverge dashboard**. It is not available through the public Chatbot API for self-service setup, and it is not yet exposed on the customer-facing MCP surface. Contact your Diverge representative if you need help configuring an integration. ### Flow at a glance 1. A visitor asks a question that needs live data from your systems (for example "Where is my order?"). 2. The chatbot widget sends the message to Diverge, optionally including **runtime variables** read from the visitor's browser (cookie, localStorage, and so on). 3. Diverge's planner selects the right integration and collects any required fields (order number, email, and so on) from the conversation. 4. Diverge's servers call your HTTP endpoint with the configured authentication. 5. Your API returns data (typically JSON). Diverge uses it to compose the reply. **Request path:** Visitor → chatbot widget → Diverge planner → your API → reply to visitor. The widget may attach **runtime variables** read from the browser (cookie, `localStorage`, etc.) when it sends each message. #### Limits | Limit | Value | | -------------------------------------------------- | ---------- | | Timeout per external HTTP request | 15 seconds | | Total time for pre-request chain plus main request | 40 seconds | | Maximum pre-request steps per integration | 5 | | Calls run at the same time in one planner step | 5 | Access tokens obtained in a pre-request step are fetched **fresh on each invoke** — there is no long-lived token cache shared across conversations. Token response bodies are not injected into the LLM context; only a compact per-step status log (label, HTTP status, ok) is recorded. ### Before you start You need: * Access to the Diverge dashboard for the chatbot. * One or more HTTP endpoints that return data your bot can use (JSON is typical). **HTTPS is strongly recommended** for production; HTTP may work but is not advised. * An authentication method chosen from the sections below. * A clear list of **required fields** the bot must collect before calling your API (for example `order_number`, `email`). * For per-user auth: a stable place in the visitor's browser to read a session token (cookie, `localStorage`, or `sessionStorage`). Calls to your API originate from **Diverge's servers**, not from the visitor's browser. CORS on your endpoint does not apply to Universal API requests. ### Configure in the dashboard #### Enable the feature Universal API Integration is disabled until you turn it on: 1. Open the Diverge dashboard and edit the chatbot. 2. Go to **Extra Settings**. 3. Find **Universal API Integration** and turn on **Enable Universal API Integration**. 4. Save Extra Settings. Until this is enabled, the **Universal API Integration** and **Universal API Planner** tabs do not appear. After you enable the feature and save, those tabs show up in the chatbot editor. If the feature is turned off while you are already on that tab, the dashboard prompts you to go to Extra Settings first. #### Create an integration 1. Open the **Universal API Integration** tab. 2. Create a new integration (or edit an existing one). 3. Set the **integration key** and **routing description**. 4. Configure **Authentication** (see [Authentication](#authentication)). 5. Configure the **main request** (method, URL, headers, body templates). 6. Optionally add **pre-request steps**, **runtime variables**, **required fields**, and **response shaping**. 7. Save. | Area | Purpose | | ------------------- | ------------------------------------------------------------------------------------- | | Integration key | Stable identifier the planner uses to invoke this integration | | Routing description | Plain-language hint so the planner knows when to use this integration | | Authentication | Server-stored credentials (see below) | | Request config | HTTP method, URL, headers, query parameters, and body templates | | Response config | Optional JSON path and transform that reshape the response before the planner uses it | | Pre-request steps | Optional chained requests before the main call (for example OAuth2 token fetch) | | Runtime variables | Values read from the visitor's browser before each message | | Required fields | Fields the planner must collect from the conversation before invoking | **Security:** Secrets stored in the **Authentication** credential panel (bearer tokens, passwords, OAuth 1.0a secrets, and so on) stay on Diverge's servers and are **never** sent to the visitor's browser. Runtime variable configuration is different: the public widget config includes the variable definitions — keys, storage sources, and **static values**. Do **not** put API keys or other secrets in a static runtime variable. Prefer the Authentication panel for shared secrets. **Routing description** is also included in the public widget runtime config when the integration exposes runtime variables. Treat it as customer-visible hint text, not a place for secrets or internal-only notes. ### How the planner uses integrations When a message may need live data, Diverge runs a Universal API planner. The planner returns one of these actions: | Action | What happens | | ----------------- | ----------------------------------------------------------------------------------------- | | `invoke` | Call the chosen integration with collected variables (for example order number, email) | | `invoke_parallel` | Make several independent calls at the same time (for example one lookup per order number) | | `clarify` | Ask the visitor for missing information before calling your API | | `finish` | Stop looking up data and continue with what is already known | **Required fields** (configured per integration) must be present before an `invoke` runs. Values bound from pre-request steps are supplied by the chain and are not treated as fields the planner must collect from the visitor. Routing descriptions and the planner catalog help the model choose among multiple enabled integrations. When a question needs several calls that do not depend on each other — one search per product type, one details call per product, or one lookup per order — the planner can request them in one `invoke_parallel` step. Up to five calls run at the same time, to the same or different integrations, so the step takes as long as its slowest call. Each call is checked and run like an `invoke`, including its own pre-request chain, whose steps still run in order. A call that needs another call's result runs in a later planner step. ### Authentication Diverge supports four common patterns when calling your endpoints. Pick the one that matches how your API gateway or platform expects credentials. #### Integration-level vs per-step auth There are two places auth can come from: | Level | Where you configure it | Applies to | | ----------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------- | | **Integration Authentication panel** | Credential panel on the integration | Every pre-request step **and** the main request (unless overridden) | | **Request-config Auth / Token Placement** | Inline on a **pre-request step** (Auth / Token Placement UI) | That pre-request step only | An explicit `Authorization` header (including one you add on the **main** request under **Headers**) or enabled Auth / Token Placement on a pre-request step **wins** for that call — credential-panel injection is skipped for that request. There is no separate Authentication panel per pre-request step. On the **main** request editor, Auth / Token Placement is not shown. Use the integration Authentication panel, or add headers, query parameters, and body fields directly on the main request. #### 1. Static API key or token The simplest case: a shared secret attached to every request. ##### Option A — Credential panel (recommended for secrets) In the dashboard **Authentication** panel, choose a type and enter the secret once. Diverge automatically adds an `Authorization` header on every request (including pre-request steps unless overridden): | Auth type | Header sent | | ------------------------------ | -------------------------------------------------------------------------------------------- | | Bearer token | `Authorization: Bearer ` | | Raw Authorization header value | `Authorization: ` — use for custom schemes such as `ApiKey abc123` | | Basic (username + password) | `Authorization: Basic ` | This is usually enough for delivery-info or order-lookup endpoints protected by a static token. ##### Option B — Custom header, query parameter, or body field When your API expects the secret somewhere other than a standard `Authorization` scheme from the credential panel: **On the main request** (Auth / Token Placement is not available here), add rows under **Headers**, **Query Params**, or body fields: | Placement | Example | | -------------------- | ------------------------------------------------ | | Authorization header | Header `Authorization`, value `Bearer {{token}}` | | Custom header | Header name `X-API-Key`, value your key | | Query parameter | Query name `api_key`, value your key | | Body field | Field name `token`, value your key | **On a pre-request step**, you can instead enable **Auth / Token Placement** on that step's request config for the same placements (Authorization header, custom header, query, or body). Value templates support `{{variableName}}` substitution (see [Template variables](#template-variables)). :::tip Store shared secrets in the **Authentication** credential panel when possible (Option A). Do **not** use a static runtime variable for secrets — those values are included in the public widget config. For a custom header name with a server-side secret on the main request, put the value in a **Headers** row on the server-side request editor (not as a static runtime variable), or use the credential panel's raw Authorization header type when the full header value is the secret. ::: **Example — Bearer token via credential panel:** 1. Authentication type: **Bearer token** 2. Enter the token in the dashboard (stored server-side). 3. Request URL: `https://api.example.com/orders/{{order_number}}` Diverge sends `Authorization: Bearer ` automatically; no auth block needed on the request config. **Example — custom header on the main request:** 1. Authentication type: **No auth** (credential panel). 2. On the main request **Headers** list, add: * Key: `X-API-Key` * Value: your key (entered in the request editor on the server, not as a static runtime variable) #### 2. OAuth2 client\_credentials If your API sits behind an OAuth token server, configure a **pre-request step** that fetches an access token before the main data request. There is no separate "OAuth2" auth type — the chain handles token exchange automatically on each invoke. **Setup:** 1. Set **Authentication** (integration credential panel) to **No auth** so credential headers do not conflict with the token step on every call. 2. Add a pre-request step: * Method: `POST` * URL: your token endpoint (for example `https://auth.example.com/oauth/token`) * Body (JSON): include `grant_type: client_credentials` and any client id/secret your server requires (often as form fields or Basic auth on the token endpoint itself). 3. Add an **output binding**: map response field `access_token` → variable `access_token`. 4. On the **main request**, add header: `Authorization: Bearer {{access_token}}` **Example pre-request step (JSON body):** ```json { "label": "Get access token", "requestConfig": { "method": "POST", "url": "https://auth.example.com/oauth/token", "bodyMode": "json", "bodyTemplate": { "grant_type": "client_credentials", "client_id": "your-client-id", "client_secret": "your-client-secret" } }, "outputBindings": [{ "variable": "access_token", "jsonPath": "access_token" }] } ``` Client secrets placed in pre-request JSON are stored server-side but remain visible in the dashboard to anyone who can edit the chatbot. Prefer Auth / Token Placement (or a header row) on the **token pre-request step** when the provider supports Basic auth on the token endpoint — do **not** set Basic on the integration Authentication panel just for the token step, because that panel applies to every request in the chain unless overridden. **Example main request headers:** ```json [{ "key": "Authorization", "value": "Bearer {{access_token}}" }] ``` If the token endpoint itself requires Basic auth, add it on that **pre-request step's request config** (Auth / Token Placement or a header row) — not a separate Authentication panel for the step. An explicit header on that step wins over integration-level credential injection for that call only. Keep the integration Authentication panel on **No auth** when the main request uses `Authorization: Bearer {{access_token}}`. Token refresh is **automatic per invoke**: each time the planner calls the integration, the chain runs from the beginning and obtains a fresh token. #### 3. OAuth 1.0a signed requests For gateways that require OAuth 1.0a request signing (HMAC-SHA1 or HMAC-SHA256), use the **OAuth 1.0a** auth type in the credential panel: | Field | Maps to | | ---------------- | ---------------------------- | | Consumer Key | OAuth consumer key | | Consumer Secret | OAuth consumer secret | | Access Token | OAuth access token | | Token Secret | OAuth token secret | | Signature method | `HMAC-SHA1` or `HMAC-SHA256` | Diverge signs each request and sets a signed `Authorization` header. Magento 2.4.4 and later typically require **HMAC-SHA256**; older integrations may use HMAC-SHA1. Configure the main request URL and method as usual — signing is applied automatically. #### 4. Per-user session token (browser) When the data is user-specific and your API should authorize the request as if the logged-in visitor called it themselves, configure **Runtime variables** to read a token from the browser. **Data path:** Browser storage or a same-origin browser request → chatbot script on your site → chatbot iframe → Diverge servers → your API (for example as `Authorization: Bearer {{customer_token}}` or another header you configure). Supported sources: | Source | Use when | | -------------- | -------------------------------------------------------------------------------- | | Cookie | Session token stored in a named cookie | | localStorage | Token or JSON profile in `localStorage` | | sessionStorage | Token scoped to the browser tab session | | Fetch | Token minted by a same-origin GET or POST using the visitor's browser session | | Static | Same value for every visitor (not for secrets — values are public to the widget) | For JSON stored in cookie or storage, set an optional **JSON path** (for example `customer.id`) to extract a nested field. For tokens available only from an endpoint on your site, choose **Fetch (same-origin request)**, set the response JSON path (for example `accessToken`), and enter a fetch configuration: ```json { "url": "/api/token/context-token", "method": "POST", "credentials": "include", "body": "{}", "headers": { "Content-Type": "application/json", "X-CSRF-Token": "{{cookie:CSRF-TOKEN}}", "X-Tab-Id": "{{sessionStorage:tabId}}" }, "expiresAtPath": "expiresAt", "ttlSeconds": 150 } ``` Use a root-relative path starting with `/`; absolute URLs, protocol-relative URLs, and redirects are rejected. GET and POST are supported; a body is allowed only with POST. Headers and body accept `{{cookie:NAME}}`, `{{localStorage:KEY}}`, and `{{sessionStorage:KEY}}`; JSON-encoded string values are unquoted. The browser supplies HttpOnly session cookies with the request; JavaScript does not read or forward those cookies. The loader prefetches and shares identical requests across HTTP and Universal API integrations. Responses stay in memory only (`cacheKey`, if supplied, does not create a storage entry). It refreshes about 45 seconds before the ISO timestamp at `expiresAtPath`, while the tab is visible. A missing expiry falls back to `ttlSeconds` (60–3600 seconds; default 150). Opening the widget also refreshes near-expiry values. Cached values add no message delay; a missing value waits at most 800 ms, within the widget’s existing one-second refresh deadline. Requests time out after five seconds and failures back off from one minute up to fifteen minutes. An expired value is omitted; a still-valid cached value survives a failed refresh. Preview never runs these requests. This browser request is distinct from server-side pre-request steps: use Fetch when the token endpoint requires the visitor’s browser session. It is also available in generic HTTP integrations. **Example runtime variable definitions:** | Variable key | Source | Key / path | | ---------------- | ------------ | -------------------------------------- | | `customer_id` | localStorage | Key `profile`, JSON path `customer.id` | | `customer_token` | cookie | Key `session_token` | | `locale` | static | Value `da-DK` | Use `{{customer_token}}` (or your chosen key) in the request URL, headers, query parameters, body fields, or (on a pre-request step) Auth / Token Placement. The widget reads these values on each message and sends them to Diverge. Values the planner extracts from the **current user message** override browser runtime values for the same key. :::warning Browser-sourced tokens are only as trustworthy as the visitor's browser — the same caution applies as for [`setMetadata()`](/guides/chatbot-integration#attaching-session-metadata). Do not rely on them alone for high-stakes entitlement decisions. [Verified (signed) metadata](/guides/chatbot-integration#verified-signed-metadata) is for trusted **session context** on webhooks and livechat. It is **not** automatically mapped into Universal API request variables or authentication. For Universal API entitlement, use a session token your site controls in cookie/`localStorage`/`sessionStorage` (and that your API validates), or server-stored credentials from the Authentication panel. ::: ### Template variables Request configs support `{{variableName}}` placeholders in: * URL * Headers * Query parameters * JSON or form body fields * Auth value templates (pre-request Auth / Token Placement) Rules: * Variable names may contain letters, numbers, `_`, `.`, or `-`. * Missing variables become an empty string. * Planner-collected fields (for example `order_number`) and pre-request output bindings (for example `access_token`) are ordinary template variables once bound. ### Pre-request chaining Pre-request steps run in order before the main request. Each step is a full HTTP request config. After a step succeeds, **output bindings** copy fields from the JSON response into the shared variable map using a JSON path, so later steps and the main request can reference them. Use chaining when: * You need OAuth2 `client_credentials` (see above). * A lookup depends on an earlier call (for example token → account id → order list). **Example — three-step chain:** 1. **Get access token** — `POST` token URL → bind `access_token` 2. **Resolve account** — `GET` account endpoint with `Authorization: Bearer {{access_token}}` → bind `account_id` 3. **Main request** — `GET` `https://api.example.com/accounts/{{account_id}}/orders/{{order_number}}` with the same bearer header Additional rules: * Up to **5** pre-request steps per integration. * Total wall-clock time for all steps plus the main request is capped at **40 seconds** (each external request made by Universal API also times out after **15 seconds**). * If a step or the main request already sets an `Authorization` header, that explicit header is used for that call; set credential auth to **No auth** when the chain provides tokens. * Variables produced by output bindings are not listed as planner "required fields" — the chain supplies them after the planner's field check. ### Response shaping On the main request, you can optionally configure: | Option | Purpose | | ------------------ | ------------------------------------------------------------------------------------------------------------------ | | Response path | JSON path into the response body (for example `data` or `orders.0`) | | Response transform | Short JavaScript that receives `response`, `variables`, and `context` and returns the value the planner should see | Use transforms when your API returns a large or nested payload and you only want a compact shape passed into the conversation context. Leave them empty when the raw JSON is already suitable. ### What your endpoint should implement As the API owner, make sure your endpoint: * Accepts the auth method you configured in Diverge. * Returns structured data (JSON is typical) with stable field names. Non-JSON success responses are accepted as plain text, but JSON path selection and most planner usage work best with JSON. * Handles read-style lookups idempotently where possible. * Responds within the timeout limits above. You do not need to allow browser CORS for these calls — they are server-to-server from Diverge. ### Troubleshooting | Symptom | Likely cause | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Universal API tabs missing in the dashboard | Feature not enabled under **Extra Settings → Enable Universal API Integration** | | Your API returns 401 | Wrong auth type; token in wrong header, query, or body; OAuth 1.0a signature method mismatch (try HMAC-SHA256) | | Your API returns another non-2xx status | Diverge treats non-success HTTP statuses as a failed request and stops that invoke | | Response is not JSON | Success responses without `application/json` are kept as plain text; JSON path / output bindings that expect objects will not work as expected — prefer JSON | | Pre-request step fails | Token or lookup URL wrong; auth missing on that step's request config; non-2xx from the step endpoint | | `{{variable}}` is empty in the outgoing request | Runtime variable missing in browser storage; typo in cookie/storage key or JSON path | | Token step succeeds but main request fails | Main request missing `Authorization: Bearer {{access_token}}` header | | Unexpected double auth | Credential panel auth plus explicit `Authorization` on the same call — use **No auth** when the chain or request config supplies the header | | Chain times out / chain budget exceeded | Too many slow steps; reduce steps or optimize your API; maximum 40 seconds total for all pre-steps plus the main request | ### Related reading * [Chatbot Integration](/guides/chatbot-integration) — opening the floating widget, session metadata, and signed metadata (session context — not Universal API auth) * [UI embeddings](/guides/ui-embeddings) — search bar, inline chat, and fullscreen layouts * [Chatbot API Flow](/guides/chatbot-api-flow) — visitor auth, streaming messages, and outbound livechat webhooks