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.

Intended audience: Backend engineers integrating a third-party application, frontend developers embedding the chat widget, and mobile engineers adding in-app chat.

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.

Widget endpoints (/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.

WSemit("send_message", payload)

Payload

FieldTypeRequiredDescription
conversation_idstring (UUID)yesTarget conversation.
contentstringyes *Text body of the message. May be empty when rich_media is provided.
content_typestringnoOne of text, image, file, quick_button, carousel, dropdown. Default: text.
rich_mediaobjectnoStructured payload for non-text types. See Rich Media Types.
sender_typestringyesend_user or agent.
external_idstringyes for end_userThe 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.

Rate limiting: End-user messages are rate-limited to one message per 500 ms per external_id. Excess messages are silently dropped.
Session auto-reopen: If an end user sends a message after their conversation session was resolved, a new session is automatically created. Agents and bots cannot send into a resolved session.

Message History

Two endpoints provide message history depending on who is calling.

Agent — REST endpoint

GET/api/conversations/{conversation_id}/history
Query paramDefaultDescription
limit50Maximum number of messages to return.
offset0Pagination offset.

End User (Widget) — public endpoint

GET/chat-widget/history/{conversation_id}?external_id={sid}

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

GET/api/conversations/
Query paramValuesDescription
filterunassigned (default), mine, allunassigned — 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

GET/api/conversations/{conversation_id}

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)

POST/api/conversations/{conversation_id}/assign

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

WSemit("claim_conversation", payload)
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

1

End user sends first message

Widget emits send_message via Socket.IO. Server creates the conversation and an open session, then persists the message.

2

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.

3

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}.

4

All agents updated

conversation_assigned is broadcast to org:{org_id}:queue. The conversation disappears from the unassigned filter for all agents.

5

Agents and user exchange messages

Both sides emit send_message. Messages are broadcast to everyone in conv:{id}.

Resolve Conversation

POST/api/conversations/{conversation_id}/resolve

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

HTTPCondition
404Conversation not found or belongs to a different org.
409No open session — the conversation is already resolved.
After a session is resolved, the next message from the end user automatically creates a new session, so the conversation reappears in the unassigned queue without any manual action.

Tagging

Add tag to open session

POST/api/conversations/{conversation_id}/tags
{ "tag": "billing" }

Response: { "tags": ["billing"] }

Available tags for a conversation's bot

GET/api/conversations/{conversation_id}/available-tags
[{ "id": "uuid", "tag": "billing" }, ...]

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 online in 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

EventPayload fieldsDescription
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

EventEmitted toPayload
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

RoomWho joinsEvents received
conv:{conversation_id}End user (widget) and assigned agentmessage_received, typing_indicator, typing_cleared
org:{org_id}:queueAgents listening to the inbox queueconversation_updated, conversation_assigned
org:{org_id}:presenceAgents tracking team presenceagent_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

  1. Go to Integration → Web Chat in the platform dashboard.
  2. Select the bot you want to attach to this embed.
  3. Give it a name and (optionally) list allowed origins one per line.
  4. Click Create. Copy the generated token (shown once).
Allowed origins are the domains where the widget will load (e.g. 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:

  1. Reads data-embed-token from the script tag.
  2. Generates or retrieves a persistent session_id from localStorage (key: cw_sid_{token}). This is the end user's external_id.
  3. Injects an <iframe> pointing to /chat-widget?embed_token=...&session_id=....
  4. 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):

ParamDescription
titleHeader title shown in the widget. Default: Chat with us.
customer_namePre-fill the end user's display name.
session_idYour 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

  1. Go to Integration → Mobile SDK in the dashboard.
  2. Select the bot and click Create Channel. Each bot gets one mobile channel.
  3. Configure FCM and/or APNs credentials (see Push Notifications).
  4. Note the Channel ID — you will need it when opening conversations from the app.

Step 2 — Open a Conversation from the App

There is no server-side REST endpoint to init a conversation for mobile yet. The conversation auto-provisioning that exists in the web embed widget (/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.

A first-class mobile REST API for conversation provisioning is planned. Track the project roadmap for the endpoint when it becomes available.

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)

FieldWhere to find it
Server KeyFirebase Console → Project settings → Cloud Messaging → Server key
Sender IDFirebase Console → Project settings → Cloud Messaging → Sender ID

APNs (iOS / Apple Push Notifications)

FieldWhere to find it
Team IDApple Developer → Membership details
Bundle IDYour app's bundle identifier (e.g. com.example.myapp)
Key IDApple Developer → Certificates → Keys → your APNs Auth Key
Private KeyContents of the downloaded .p8 file (include the full PEM block)
Environmentsandbox (development) or production
The push notification dispatch is configured in the platform but delivery is not yet implemented server-side — the credentials are stored and will be used when the push dispatch service is added. Connect your device token via Socket.IO or REST when it becomes available.

Data Models

Conversation

The top-level entity. One conversation per end user per channel. Multiple sessions can exist over time (one per support interaction).

FieldTypeNotes
idUUID stringPrimary key.
end_userobjectNested: id, display_name, external_id.
channel_idUUID stringThe channel (webchat or mobile_sdk) this conversation belongs to.
organization_idUUID stringOwning org — used for all access-control checks.
active_sessionobject | nullThe current open session, or null if resolved.
created_atISO 8601
updated_atISO 8601Bumped 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.

FieldTypeNotes
idUUID string
statusstringopen | resolved
assigned_agent_idUUID | nullUser ID of the assigned agent.
tagsstring[]Labels applied during or at resolve time.
first_message_atISO 8601 | null
assigned_atISO 8601 | null
resolved_atISO 8601 | null

Message

FieldTypeNotes
idUUID string
conversation_idUUID string
session_idUUID stringWhich session this message belongs to.
sender_typestringend_user | agent | bot
sender_idstringexternal_id for end users; user UUID for agents.
content_typestringtext | image | file | quick_button | carousel | dropdown
contentstringPlain text body or caption.
rich_mediaobject | nullStructured payload for non-text types.
created_atISO 8601

Error Reference

HTTP / Socket eventCodeMeaning
HTTP400Missing or invalid request parameter.
HTTP401Not authenticated — no valid browser session cookie (redirects to login for HTML endpoints; 401 JSON for API endpoints).
HTTP403Authenticated but not authorized (wrong org, role too low, embed token invalid).
HTTP404Resource not found or not visible to this org.
HTTP409Conflict — e.g. resolving a conversation that is already resolved.
Socket.IOerror 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 /widget to /chat-widget throughout. Clarified API keys are stored but do not yet authenticate REST routes. Replaced non-existent POST /api/conversations/init mobile 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.