Developer Integration Guide v1.0
Complete reference for integrating with the Chatbot Platform — covering the REST API, Socket.IO real-time layer, web embed widget, and mobile SDK channel setup.
Authentication
All agent-facing REST endpoints (under /api/conversations/) require a valid browser session established by logging in at /auth/login. The session cookie (session) is set automatically by the browser. These endpoints use @login_required (Flask-Login) and cannot be called with an API key.
API Keys can be generated under Company → API Keys. They are stored for future use and upcoming webhook/integration endpoints — they do not currently authenticate any REST route.
/chat-widget/) do not require a session. They authenticate end users via external_id matching the conversation's end user record.
Base URL & Common Headers
Base URL: https://<your-domain>
# Agent REST calls — requires active browser session
GET /api/conversations/
Cookie: session=<flask-session-cookie>
X-CSRFToken: <token from <meta name="csrf-token">>
# Widget public endpoints — no auth header needed
GET /chat-widget/history/<conversation_id>?external_id=<sid>
All request bodies are JSON. All responses are JSON unless noted.
Send Message
Messages are sent over Socket.IO, not HTTP. This applies to both end users (from the widget) and agents (from the inbox). The server persists the message, broadcasts it to the conversation room, and optionally triggers a bot response.
Payload
| Field | Type | Required | Description |
|---|---|---|---|
conversation_id | string (UUID) | yes | Target conversation. |
content | string | yes * | Text body of the message. May be empty when rich_media is provided. |
content_type | string | no | One of text, image, file, quick_button, carousel, dropdown. Default: text. |
rich_media | object | no | Structured payload for non-text types. See Rich Media Types. |
sender_type | string | yes | end_user or agent. |
external_id | string | yes for end_user | The session identifier that identifies the end user. Must match the value used when the conversation was opened. |
End-User Example (widget)
// JavaScript — inside the widget iframe
socket.emit("send_message", {
conversation_id: "3a2b1c...",
content: "Hello, I need help",
content_type: "text",
sender_type: "end_user",
external_id: "browser-session-abc123"
});
Agent Example (inbox)
// JavaScript — inside the agent inbox page
socket.emit("send_message", {
conversation_id: "3a2b1c...",
content: "Hi! How can I help you today?",
content_type: "text",
sender_type: "agent"
// external_id not needed — agent is identified by the session cookie
});
Server Response Event
On success, the server broadcasts message_received to everyone in the conversation room (see Event Reference). On failure, a error event is emitted only to the sender.
external_id. Excess messages are silently dropped.
Message History
Two endpoints provide message history depending on who is calling.
Agent — REST endpoint
| Query param | Default | Description |
|---|---|---|
limit | 50 | Maximum number of messages to return. |
offset | 0 | Pagination offset. |
End User (Widget) — public endpoint
Returns the last 100 messages. Authorization is via external_id matching the conversation's end user — no session cookie required.
Response shape (both endpoints)
[
{
"id": "uuid",
"session_id": "uuid", // omitted in widget endpoint
"sender_type": "end_user", // end_user | agent | bot
"sender_id": "external-id or user-uuid",
"content_type": "text",
"content": "Hello",
"rich_media": null,
"read_by": {}, // omitted in widget endpoint
"created_at": "2025-01-15T08:23:10.000000"
}
]
Rich Media Types
Pass content_type and a matching rich_media object in the send_message event.
Image
{
"content_type": "image",
"content": "Optional caption",
"rich_media": {
"url": "https://example.com/photo.jpg",
"alt": "A scenic view"
}
}
File
{
"content_type": "file",
"content": "invoice.pdf",
"rich_media": {
"url": "https://example.com/files/invoice.pdf",
"filename": "invoice.pdf",
"size": 204800, // bytes
"mime_type":"application/pdf"
}
}
Quick Buttons
{
"content_type": "quick_button",
"content": "Choose an option:",
"rich_media": {
"buttons": [
{ "label": "Yes", "value": "yes" },
{ "label": "No", "value": "no" }
]
}
}
Carousel
{
"content_type": "carousel",
"content": "",
"rich_media": {
"cards": [
{
"image_url": "https://...",
"title": "Product A",
"subtitle": "Short description",
"buttons": [{ "label": "Buy now", "value": "buy_A" }]
}
]
}
}
Dropdown
{
"content_type": "dropdown",
"content": "Select your region:",
"rich_media": {
"placeholder": "-- pick one --",
"options": [
{ "label": "Asia Pacific", "value": "apac" },
{ "label": "Europe", "value": "eu" }
]
}
}
List Conversations
| Query param | Values | Description |
|---|---|---|
filter | unassigned (default), mine, all | unassigned — open sessions with no agent. mine — sessions assigned to the calling agent. all — every conversation in the org. |
Response
[
{
"id": "uuid",
"end_user": {
"id": "uuid",
"display_name": "John Doe",
"external_id": "browser-session-abc"
},
"channel_id": "uuid",
"active_session": {
"id": "uuid",
"status": "open",
"assigned_agent_id": null,
"tags": []
},
"created_at": "2025-01-15T08:00:00",
"updated_at": "2025-01-15T08:23:00"
}
]
Get Conversation
Returns a single conversation with the same shape as the list items above.
Assignment Process
An agent claims an unassigned conversation either through the REST API or via Socket.IO. Both paths call the same service method (ConversationService.assign_conversation) which sets assigned_agent_id and assigned_at on the open session.
Option A — REST (HTTP POST)
No request body required. The calling agent's identity is read from the session cookie.
# curl example
curl -X POST https://<host>/api/conversations/3a2b1c.../assign \
-H "Cookie: session=..." \
-H "X-CSRFToken: ..."
Response
{
"id": "3a2b1c...",
"assigned_agent_id": "user-uuid",
"assigned_at": "2025-01-15T08:25:00"
}
Option B — Socket.IO event
socket.emit("claim_conversation", {
conversation_id: "3a2b1c..."
});
On success, the server emits conversation_assigned to the entire org queue room so all connected agents see the conversation leave the unassigned list.
Assignment Flow
End user sends first message
Widget emits send_message via Socket.IO. Server creates the conversation and an open session, then persists the message.
Server broadcasts to org queue
conversation_updated is emitted to org:{org_id}:queue. All agents connected to the inbox receive the event and update their conversation list.
Agent claims the conversation
Agent clicks Claim in the inbox. The browser emits claim_conversation. The server sets assigned_agent_id and joins the agent to the conversation room conv:{id}.
All agents updated
conversation_assigned is broadcast to org:{org_id}:queue. The conversation disappears from the unassigned filter for all agents.
Agents and user exchange messages
Both sides emit send_message. Messages are broadcast to everyone in conv:{id}.
Resolve Conversation
Closes the open session. Optionally apply final tags at the same time.
Request body
{
"tags": ["billing", "resolved-no-action"] // optional, defaults to []
}
Response
{
"id": "conversation-uuid",
"session_id": "session-uuid",
"status": "resolved",
"tags": ["billing", "resolved-no-action"],
"resolved_at": "2025-01-15T09:10:00"
}
Error states
| HTTP | Condition |
|---|---|
404 | Conversation not found or belongs to a different org. |
409 | No open session — the conversation is already resolved. |
Real-Time Layer (Socket.IO)
The platform uses Socket.IO v4 for real-time messaging. Two categories of client connect:
- End users — the widget iframe. No login required. Authenticated via
external_id. - Agents — the inbox SPA. Authenticated via the Flask session cookie. On connect, the server marks the agent as
onlinein Redis.
// Connect (browser)
const socket = io(window.location.origin);
// or with explicit URL and options
const socket = io("https://chat.example.com", {
transports: ["websocket"],
withCredentials: true
});
Socket.IO Event Reference
Client → Server
| Event | Payload fields | Description |
|---|---|---|
send_message |
conversation_id, content, content_type, rich_media, sender_type, external_id |
Send a message. See Send Message. |
claim_conversation |
conversation_id |
Agent claims an unassigned conversation. |
join_conversation |
conversation_id, external_id (end user) or session cookie (agent) |
Join the Socket.IO room for a conversation to receive messages. |
leave_conversation |
conversation_id |
Leave the conversation room. |
join_queue |
none | Agent joins the org-wide queue room to receive new conversation notifications. |
join_org_presence |
none | Join the org presence room to receive agent online/offline events. |
user_typing |
conversation_id, sender_type |
Broadcast typing indicator to the other side. |
mark_read |
conversation_id, message_ids[] |
Mark messages as read (not yet implemented server-side — reserved). |
Server → Client
| Event | Emitted to | Payload |
|---|---|---|
message_received |
conv:{id} room |
id, conversation_id, session_id, sender_type, sender_id, content_type, content, rich_media, created_at |
conversation_updated |
org:{org_id}:queue room |
conversation_id, sender_type, content_type, content |
conversation_assigned |
org:{org_id}:queue room |
conversation_id, assigned_to (agent user ID) |
typing_indicator |
conv:{id} room (skip sender) |
conversation_id, sender_type |
typing_cleared |
conv:{id} room |
conversation_id |
agent_status_changed |
org:{org_id}:presence room |
agent_id, status (online | offline) |
error |
sender only | message (string) |
Socket.IO Rooms
| Room | Who joins | Events received |
|---|---|---|
conv:{conversation_id} | End user (widget) and assigned agent | message_received, typing_indicator, typing_cleared |
org:{org_id}:queue | Agents listening to the inbox queue | conversation_updated, conversation_assigned |
org:{org_id}:presence | Agents tracking team presence | agent_status_changed |
Web Embed Widget
The web embed delivers a fully contained chat window as an <iframe> injected into the host page by a small JavaScript loader. No SDK installation is needed — just a <script> tag.
Step 1 — Create an Embed Token
- Go to Integration → Web Chat in the platform dashboard.
- Select the bot you want to attach to this embed.
- Give it a name and (optionally) list allowed origins one per line.
- Click Create. Copy the generated token (shown once).
https://yourapp.com). Requests from other origins return 403. Leave blank to allow all origins during development.
Step 2 — Add the Script Snippet
Paste this snippet just before the closing </body> tag on any page where you want the chat widget to appear:
<!-- Chat widget loader -->
<script
src="https://<your-domain>/chat-widget/embed.js"
data-embed-token="ew_xxxxxxxxxxxxxxxxxxxx"
></script>
The loader script:
- Reads
data-embed-tokenfrom the script tag. - Generates or retrieves a persistent
session_idfromlocalStorage(key:cw_sid_{token}). This is the end user'sexternal_id. - Injects an
<iframe>pointing to/chat-widget?embed_token=...&session_id=.... - The iframe is positioned fixed, bottom-right, 380×560 px.
Custom Parameters
Pass extra query params in the iframe src URL (not via the <script> tag — you would need to build the iframe manually in that case):
| Param | Description |
|---|---|
title | Header title shown in the widget. Default: Chat with us. |
customer_name | Pre-fill the end user's display name. |
session_id | Your own user/session identifier. Use this to tie the chat to a logged-in account. |
Passing a known user identity
If your site has authenticated users, pass their unique identifier as session_id so conversations persist across browsers and devices:
<!-- Example: inject with a server-rendered user ID -->
<script>
(function() {
var iframe = document.createElement('iframe');
iframe.src = 'https://<your-domain>/chat-widget'
+ '?embed_token=ew_xxxx'
+ '&session_id=' + encodeURIComponent('user_' + USER_ID)
+ '&customer_name=' + encodeURIComponent(USER_DISPLAY_NAME)
+ '&title=Support+Chat';
iframe.style.cssText = 'position:fixed;bottom:0;right:0;width:380px;height:560px;border:none;z-index:2147483647;border-radius:12px 12px 0 0;box-shadow:0 8px 32px rgba(0,0,0,0.18);';
document.body.appendChild(iframe);
})();
</script>
Communication between host page and iframe
The widget iframe is sandboxed. Use window.postMessage if you need to pass runtime data from your page into the widget after load. The widget does not currently emit postMessage events back.
Mobile SDK Integration
Mobile apps connect through the same Socket.IO and REST APIs as the web widget. The platform additionally manages push notification credentials (FCM for Android, APNs for iOS) per mobile channel.
Step 1 — Provision a Mobile Channel
- Go to Integration → Mobile SDK in the dashboard.
- Select the bot and click Create Channel. Each bot gets one mobile channel.
- Configure FCM and/or APNs credentials (see Push Notifications).
- Note the Channel ID — you will need it when opening conversations from the app.
Step 2 — Open a Conversation from the App
/chat-widget) is not exposed as a standalone API call. Two integration paths are available today:
Option A — WebView (simplest, works today)
Load the widget URL in a native WebView. The widget handles conversation creation, Socket.IO connection, and message rendering automatically. Use the embed token generated in the dashboard and pass a stable user identifier as session_id:
https://<your-domain>/chat-widget
?embed_token=ew_xxxxxxxxxxxxxxxxxxxx
&session_id=mobile-user-12345
&customer_name=Alice+Smith
&title=Support+Chat
Option B — Native Socket.IO (requires pre-existing conversation_id)
If you already have a conversation_id from a prior WebView session or from a server-side process that called ConversationService.create_or_get_conversation() directly, you can connect natively via Socket.IO. The join_conversation event verifies external_id against the stored end user record — it does not create a new conversation.
Step 3 — Connect via Socket.IO
Using any Socket.IO client library (available for iOS, Android, Flutter, React Native):
// build.gradle: implementation 'io.socket:socket.io-client:2.1.0'
val opts = IO.Options.builder()
.setTransports(arrayOf("websocket"))
.build()
val socket = IO.socket("https://your-domain.com", opts)
socket.on(Socket.EVENT_CONNECT) {
socket.emit("join_conversation", JSONObject().apply {
put("conversation_id", conversationId)
put("external_id", externalId)
})
}
socket.on("message_received") { args ->
val data = args[0] as JSONObject
// render data.getString("content") in your UI
}
socket.connect()
// Send a message
socket.emit("send_message", JSONObject().apply {
put("conversation_id", conversationId)
put("content", userText)
put("content_type", "text")
put("sender_type", "end_user")
put("external_id", externalId)
})
// Package.swift: .package(url: "https://github.com/socketio/socket.io-client-swift", ...)
let manager = SocketManager(
socketURL: URL(string: "https://your-domain.com")!,
config: [.log(false), .compress]
)
let socket = manager.defaultSocket
socket.on(clientEvent: .connect) { _, _ in
socket.emit("join_conversation", [
"conversation_id": conversationId,
"external_id": externalId
])
}
socket.on("message_received") { data, _ in
guard let msg = data.first as? [String: Any] else { return }
// render msg["content"] in your UI
}
socket.connect()
// Send a message
socket.emit("send_message", [
"conversation_id": conversationId,
"content": userText,
"content_type": "text",
"sender_type": "end_user",
"external_id": externalId
])
// pubspec.yaml: socket_io_client: ^2.0.0
import 'package:socket_io_client/socket_io_client.dart' as IO;
final socket = IO.io('https://your-domain.com', <String, dynamic>{
'transports': ['websocket'],
'autoConnect': false,
});
socket.onConnect((_) {
socket.emit('join_conversation', {
'conversation_id': conversationId,
'external_id': externalId,
});
});
socket.on('message_received', (data) {
// render data['content'] in your widget tree
});
socket.connect();
// Send a message
socket.emit('send_message', {
'conversation_id': conversationId,
'content': userText,
'content_type': 'text',
'sender_type': 'end_user',
'external_id': externalId,
});
Push Notifications
Configure push credentials under Integration → Mobile SDK → {channel}.
FCM (Android / Firebase Cloud Messaging)
| Field | Where to find it |
|---|---|
| Server Key | Firebase Console → Project settings → Cloud Messaging → Server key |
| Sender ID | Firebase Console → Project settings → Cloud Messaging → Sender ID |
APNs (iOS / Apple Push Notifications)
| Field | Where to find it |
|---|---|
| Team ID | Apple Developer → Membership details |
| Bundle ID | Your app's bundle identifier (e.g. com.example.myapp) |
| Key ID | Apple Developer → Certificates → Keys → your APNs Auth Key |
| Private Key | Contents of the downloaded .p8 file (include the full PEM block) |
| Environment | sandbox (development) or production |
Data Models
Conversation
The top-level entity. One conversation per end user per channel. Multiple sessions can exist over time (one per support interaction).
| Field | Type | Notes |
|---|---|---|
id | UUID string | Primary key. |
end_user | object | Nested: id, display_name, external_id. |
channel_id | UUID string | The channel (webchat or mobile_sdk) this conversation belongs to. |
organization_id | UUID string | Owning org — used for all access-control checks. |
active_session | object | null | The current open session, or null if resolved. |
created_at | ISO 8601 | |
updated_at | ISO 8601 | Bumped on every new message. |
Conversation Session
Represents a single support interaction within a conversation. A conversation moves through open → resolved, and a new session is created on the next end-user message.
| Field | Type | Notes |
|---|---|---|
id | UUID string | |
status | string | open | resolved |
assigned_agent_id | UUID | null | User ID of the assigned agent. |
tags | string[] | Labels applied during or at resolve time. |
first_message_at | ISO 8601 | null | |
assigned_at | ISO 8601 | null | |
resolved_at | ISO 8601 | null |
Message
| Field | Type | Notes |
|---|---|---|
id | UUID string | |
conversation_id | UUID string | |
session_id | UUID string | Which session this message belongs to. |
sender_type | string | end_user | agent | bot |
sender_id | string | external_id for end users; user UUID for agents. |
content_type | string | text | image | file | quick_button | carousel | dropdown |
content | string | Plain text body or caption. |
rich_media | object | null | Structured payload for non-text types. |
created_at | ISO 8601 |
Error Reference
| HTTP / Socket event | Code | Meaning |
|---|---|---|
| HTTP | 400 | Missing or invalid request parameter. |
| HTTP | 401 | Not authenticated — no valid browser session cookie (redirects to login for HTML endpoints; 401 JSON for API endpoints). |
| HTTP | 403 | Authenticated but not authorized (wrong org, role too low, embed token invalid). |
| HTTP | 404 | Resource not found or not visible to this org. |
| HTTP | 409 | Conflict — e.g. resolving a conversation that is already resolved. |
| Socket.IO | error event | { "message": "<reason>" } — emitted only to the sender. |
Changelog
Changes to the integration surface — endpoints, Socket.IO events, widget URLs, and documented response fields.
- 2026-05-11 — Corrected widget URL prefix from
/widgetto/chat-widgetthroughout. Clarified API keys are stored but do not yet authenticate REST routes. Replaced non-existentPOST /api/conversations/initmobile endpoint with accurate WebView and planned-API notes. - 2026-05-11 — Initial integration guide published: REST API (list, get, history, assign, resolve, tag), Socket.IO event reference, web embed snippet, and mobile SDK channel setup.