# 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.
## 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.
#### 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, including:
* name
* avatar
* welcome message
* optional header subtitle text
* optional subtitle link text and URL
* theme colors
* popup messages
#### 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, Dialoge 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.
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 signed JSON `POST` requests. Verify the signature against the raw request
body before trusting the payload:
```ts
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyDialogeWebhook({
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.
#### 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.
### 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`.
When webhook delivery is configured, `DELETE /api/v1/chat` also emits a signed
`visitor.data.deleted` webhook after Dialoge 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 Integration
This document covers how to open and interact with the chatbot from your own custom elements,
buttons, links, search bars, or any other UI.
### Prerequisites
The chatbot script must be loaded on the page:
```html
```
### Option 1: JavaScript API (`window.DialogIntelligens`)
The chatbot script exposes a global `window.DialogIntelligens` object with the following methods:
| Method | Description |
| --------------- | ---------------------------------------------------- |
| `open()` | Opens the chat window |
| `open(message)` | Opens the chat window and sends a pre-filled message |
| `hide()` | Hides the chat button and iframe completely |
| `show()` | Shows the chat button (closed state) |
| `destroy()` | Removes the chatbot from the page entirely |
#### Examples
**Custom button:**
```html
```
**Link/anchor:**
```html
Need help?
```
**Open with a pre-filled message:**
```html
```
### Option 2: URL Parameters
Append `?chat=open` to any page URL where the chatbot is loaded. The chatbot will automatically
open on page load and the parameters are stripped from the URL.
```text
https://example.com/page?chat=open
```
You can also include a `chatbot_message` parameter to send a pre-filled message when the chat opens:
```text
https://example.com/page?chat=open&chatbot_message=I+need+help+with+my+order
```
Using `chatbot_message` alone (without `chat=open`) also works. The chat will open automatically:
```text
https://example.com/page?chatbot_message=What+are+your+opening+hours%3F
```
This is useful for email campaigns, QR codes, FAQ links, or any link where you want the chat to
open with a specific question.
### Option 3: Inline Search Bar Widget
An inline search bar can be embedded anywhere on the page. When the user types a question and
presses Enter or clicks send, the chatbot opens and receives the message.
#### Setup
1. Load the search bar script **after** the chatbot script:
```html
```
2. Add a container element where you want the search bar to appear:
```html
```
Or use a class for multiple instances:
```html
```
#### Manual initialization with custom config
```html
```
### Option 4: Send a Message via `postMessage`
You can open the chatbot and send a pre-filled message by posting a message directly to the
chatbot iframe. This is how the inline search bar works internally.
```html
```
### Option 5: Inline Chatbot Embed
Use the inline embed when the chatbot should render directly in the page instead of as a floating widget.
#### Setup
```html
```
If `#chatbot-placeholder` is missing, the script appends the chatbot next to the script tag.
#### Inline Options
Set `window.__CHATBOT_INLINE_OVERRIDES__` before loading `chatbotinline.js` to override inline-only behavior for that embed instance.
```html
```
Supported inline override keys:
* `inlineWelcomeMessage`: text shown above the input before the first question in inline/minimal mode
### Notes
* `window.DialogIntelligens` is available as soon as the chatbot script loads.
* The `open()` method is safe to call even if the chatbot is already open. It will not toggle it
closed.
* `open(message)` opens the chat and sends the message. If the chat is already open, the message
is still delivered.
* The `?chat=open` and `?chatbot_message=` URL parameters only trigger once per page load and are
removed from the URL bar.
* When using `postMessage` to send a message, use a timeout of roughly `1000ms` to let the iframe
initialize after opening.
## 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. |
| 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 |
| --------------------- | ------------------------------------------------------------------------- |
| 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.
### 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. |
#### 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_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)