Developers & Integrations
Chatmode REST API v1
Workspace-scoped REST API for bots, CRM sync, widgets, and backend automations — organized by purpose.
5-Minute Quickstart
Connect a bot, CRM, or backend to an existing Chatmode workspace in minutes.
Generate an API Key
Go to Dashboard → Integrations, create an API key, and copy the secret (cm_live_...). Keys are bound to your workspace.
Find workspace & channel slugs
Every path uses {workspace} (e.g. acme) and usually {channel} (e.g. support).
List conversations & send a message
# Channels
curl -s "https://chatmode.egstars.com/api/v1/my-workspace/channels" \
-H "Authorization: Bearer cm_live_your_api_key"
# Open conversations
curl -s "https://chatmode.egstars.com/api/v1/my-workspace/channels/support/threads?status=open" \
-H "Authorization: Bearer cm_live_your_api_key"
# Reply into a guest thread
curl -s -X POST "https://chatmode.egstars.com/api/v1/my-workspace/channels/support/messages" \
-H "Authorization: Bearer cm_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"body": "Hello! Our support team received your inquiry.",
"thread_guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1"
}'
const API_KEY = 'cm_live_your_api_key';
const BASE = 'https://chatmode.egstars.com/api/v1';
const WS = 'my-workspace';
const CHANNEL = 'support';
async function getOpenThreads() {
const res = await fetch(`${BASE}/${WS}/channels/${CHANNEL}/threads?status=open`, {
headers: { Authorization: `Bearer ${API_KEY}` }
});
return (await res.json()).threads;
}
async function replyToGuest(guestId, body) {
const res = await fetch(`${BASE}/${WS}/channels/${CHANNEL}/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ body, thread_guest_id: guestId })
});
return res.json();
}
import requests
API_KEY = "cm_live_your_api_key"
BASE = "https://chatmode.egstars.com/api/v1"
WS = "my-workspace"
CHANNEL = "support"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
threads = requests.get(
f"{BASE}/{WS}/channels/{CHANNEL}/threads",
headers=headers,
params={"status": "open"},
).json().get("threads", [])
if threads:
reply = requests.post(
f"{BASE}/{WS}/channels/{CHANNEL}/messages",
headers=headers,
json={
"body": "Hello! How can we assist you today?",
"thread_guest_id": threads[0]["guest_id"],
},
)
print(reply.json())
Admin & Access
API keys, optional scopes, and browser CORS are managed in the dashboard. List workspace members via the API to drive thread assignment.
API keys
- Create keys in Dashboard → Integrations.
- Every API client must send Authorization: Bearer cm_live_... on every request. Rate limits apply.
- Keys are workspace-scoped and rate-limited to 60 requests/minute.
- Optional scopes limit what each key can call; leave scopes empty for full access.
Scopes
Assign one or more scopes when creating a key. Write does not imply read — select both when needed.
| Scope | Description |
|---|---|
members:read | List members |
guests:read | List guests |
guests:write | Create, block, and unblock guests, and start visitor sessions |
channels:read | List channels |
channels:write | Create and update channels |
threads:read | List and get threads |
threads:write | Open and update threads |
messages:read | List messages |
messages:write | Send messages, upload attachments, and send templates |
templates:read | List templates and metrics |
webhooks:read | List webhooks and delivery logs |
webhooks:manage | Create, update, delete, rotate secrets, and replay deliveries |
Missing a required scope returns HTTP 403 with code insufficient_scope.
CORS (browser & mobile WebViews)
For frontend JavaScript on an external site:
- Add your origin under Allowed Origins on the API key (e.g. https://app.example.com).
- Chatmode answers browser OPTIONS preflight with the required CORS headers.
- Server-to-server calls (Node, Python, Go, PHP) do not need CORS.
Prefer calling the API from your backend when the key must stay secret.
{workspace}/members
Lists workspace members (user_id, username, role) for assignee UX on PATCH thread.
Response (200 OK)
{
"members": [
{
"user_id": "522d4834-8c4d-4952-ba61-4fa301d293f9",
"username": "alex",
"display_name": "Alex",
"role": "owner"
}
]
}
Users & Guests
Provision guest sessions for messaging. Create returns a one-time session token for your end-user client.
{workspace}/guests
Lists recent guests in the workspace (newest first).
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Max 100 |
{workspace}/guests
Creates a guest by display name and returns a session token once (7-day expiry).
Request Body (JSON)
| Parameter | Type | Description |
|---|---|---|
display_name | string | Guest display name |
Response (201 Created)
{
"guest": {
"id": "c62b6623-64e9-44be-9304-f58c704f58c1",
"display_name": "Sarah Connor",
"blocked": false,
"expires_at": "2026-09-20T12:00:00Z",
"created_at": "2026-09-13T12:00:00Z"
},
"token": "raw_guest_session_token_once"
}
Pass token to your client as Authorization: Bearer …, chatmode_guest cookie, or WebSocket auth_token. It is never returned again (list/block/unblock).
{workspace}/guests/{guest_id}/block
Blocks a guest from further messaging.
{workspace}/guests/{guest_id}/unblock
Unblocks a previously blocked guest.
Channels
List, create, and update channels in the workspace.
{workspace}/channels
Lists accessible channels for the API key workspace.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspace | string | Workspace slug (e.g. acme) |
Response (200 OK)
{
"channels": [
{
"id": "7b79a528-91c6-48c2-a8c4-06ec9a6479b1",
"slug": "general",
"name": "General",
"guest_allowed": true,
"created_at": "2026-09-01T12:00:00Z"
}
]
}
{workspace}/channels
Creates a channel (subject to plan channel limits).
Request Body (JSON)
| Parameter | Type | Description |
|---|---|---|
name | string | Channel display name |
slug | string | URL slug (lowercase, unique in workspace) |
guest_allowed | boolean | Whether guests may join (default true on create) |
{workspace}/channels/{channel}
Updates channel name, guest access, embed, or AI settings. Omitted fields keep current values.
Request Body (JSON)
| Parameter | Type | Description |
|---|---|---|
name | string | Channel display name |
guest_allowed | boolean | Whether guests may join (default true on create) |
embed_enabled | boolean | Enable embed widget for this channel |
embed_allowed_origins | string[] | Allowed origins for the embed |
ai_enabled | boolean | Enable AI assistant on this channel |
{workspace}/channels/{channel}/embed-secret/rotate
Replaces the channel identity secret. Previous visitor JWTs stop verifying. The new secret is returned in this response and on later channel reads.
HS256 secret for visitor identity tokens. Present for managers. Empty until embed is enabled.
Visitor session
Exchange a host-signed JWT for a guest bearer. Requires Authorization: Bearer cm_live_... with guests:write, like every other API call, and counts toward 60 requests per minute per key. Each call issues a new guest token and invalidates the previous one for that user.
Sign HS256 with the channel identity secret. iss is {workspace}/{channel}. sub is your stable user id (max 128). name is the display name (2–32 letters, digits, spaces, hyphens, or underscores). exp is a unix timestamp at most 24 hours ahead.
Browsers must send workspace and channel as query parameters so the preflight can be checked. An origin that is not on embed_allowed_origins receives no CORS header. An empty allowlist disables browser CORS; server-to-server calls still work.
Request Body (JSON)
| Parameter | Type | Description |
|---|---|---|
workspace | string | Workspace slug (e.g. acme) |
channel | string | URL slug (lowercase, unique in workspace) |
user_token | string | HS256 JWT |
# Claims: iss=acme/support sub=user-42 name=Sara exp=<unix, max 24h>
# Signed with the channel embed_identity_secret.
# API key required. 60 requests/minute per key.
curl -s -X POST "https://chatmode.egstars.com/api/v1/visitor/session" \
-H "Authorization: Bearer cm_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"workspace": "acme",
"channel": "support",
"user_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'
# Response
# { "token": "...", "guest": { "id": "...", "display_name": "Sara" },
# "workspace": "acme", "channel": "support", "websocket": "/ws/acme/support" }
Conversations
Each guest visitor has a conversation thread identified by guest_id. Open a thread via API (with guest_id or display_name), then send messages and resolve.
{workspace}/channels/{channel}/threads
Ensures an open thread exists. Pass guest_id for an existing guest, or display_name to create a guest (returns token once) and open the thread.
Request Body (JSON)
| Parameter | Type | Description |
|---|---|---|
guest_id | uuid | Existing guest UUID (provide this or display_name) |
display_name | string | Create guest with this name, return token once, and open the thread |
{workspace}/channels/{channel}/threads
Lists conversation threads in a channel, newest activity first.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | all | open or resolved |
Response (200 OK)
{
"threads": [
{
"guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1",
"display_name": "Sarah Connor",
"status": "open",
"assignee_user_id": "522d4834-8c4d-4952-ba61-4fa301d293f9",
"assignee_name": "alex",
"last_activity_at": "2026-09-06T00:15:30Z",
"resolved_at": null
}
]
}
{workspace}/channels/{channel}/threads/{guest_id}
Returns status and assignment for one guest conversation.
Response (200 OK)
{
"thread": {
"guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1",
"display_name": "Sarah Connor",
"status": "open",
"assignee_user_id": "522d4834-8c4d-4952-ba61-4fa301d293f9",
"assignee_name": "alex",
"last_activity_at": "2026-09-06T00:15:30Z",
"resolved_at": null
}
}
{workspace}/channels/{channel}/threads/{guest_id}
Resolve/reopen a conversation or assign/unassign an agent.
Request Body (JSON)
| Field | Type | Description |
|---|---|---|
status | string | Optional. open or resolved |
assignee_user_id | UUID | Optional. Teammate to assign |
clear_assignee | boolean | Optional. true to unassign |
{
"status": "resolved"
}
Messaging
Send and list messages, upload image attachments, and deliver Easy Templates into existing conversations. Posts are authored as the workspace owner.
{workspace}/channels/{channel}/messages
Lists messages oldest-first. Filter by thread and paginate with before_id.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
thread_guest_id | UUID | none | Guest conversation filter (recommended) |
thread_user_id | UUID | none | Internal member thread filter |
limit | integer | 50 | Max 100 |
before_id | UUID | none | Cursor: messages older than this ID |
Response (200 OK)
{
"messages": [
{
"id": "9369d7ee-45df-4d51-8e9d-c782782e3a19",
"channel": "support",
"author": { "type": "guest", "name": "Sarah Connor" },
"body": "Hello, I have a question regarding my order.",
"thread": { "guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1" },
"created_at": "2026-09-06T00:10:15Z"
}
]
}
{workspace}/channels/{channel}/messages
Posts a message into a thread (text and/or attachment). Broadcasts to active WebSocket listeners and may emit message.created webhooks.
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
body | string | Yes* | Message text (max 2,000 chars). Optional when attachment_id is set |
thread_guest_id | UUID | Yes* | Guest thread (*or thread_user_id) |
thread_user_id | UUID | Yes* | Member thread |
attachment_id | UUID | No | Optional. Attachment from POST .../attachments (same channel). Body may be empty when set. |
{
"body": "Your order #10492 has been shipped!",
"thread_guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1",
"attachment_id": "18cbeff1-0428-4447-9750-f8ec009ae842"
}
Response (201 Created)
{
"message": {
"id": "18cbeff1-0428-4447-9750-f8ec009ae842",
"channel": "support",
"author": { "type": "owner", "name": "Acme Team" },
"body": "Your order #10492 has been shipped!",
"thread": { "guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1" },
"attachment": {
"id": "18cbeff1-0428-4447-9750-f8ec009ae842",
"url": "/files/18cbeff1-0428-4447-9750-f8ec009ae842",
"mime_type": "image/png",
"filename": "shipping.png"
},
"created_at": "2026-09-06T01:25:00Z"
}
}
{workspace}/channels/{channel}/attachments
Uploads an image attachment for the channel. Pass the returned id as attachment_id when posting a message. Requires messages:write.
Request Body (JSON)
Send multipart/form-data with a single file field (not JSON).
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Image file: jpeg, png, webp, or gif (subject to plan size limits) |
Response (201 Created)
{
"attachment": {
"id": "18cbeff1-0428-4447-9750-f8ec009ae842",
"url": "/files/18cbeff1-0428-4447-9750-f8ec009ae842",
"mime_type": "image/png",
"filename": "photo.png"
}
}
{workspace}/templates
Lists active Easy Templates in the workspace, including variables and buttons.
Response (200 OK)
{
"templates": [
{
"id": "a1b2c3d4-1111-2222-3333-444455556666",
"slug": "order-shipped",
"name": "Order shipped",
"category": "shipping",
"header_text": "Order update",
"body": "Hi {{name}}, your order {{order_id}} is on the way.",
"footer_text": "Chatmode Support",
"buttons": [
{ "type": "url", "text": "Track package", "url": "https://example.com/track/{{order_id}}" },
{ "type": "quick_reply", "text": "Talk to agent", "payload": "talk_agent" }
],
"variables": ["name", "order_id"],
"is_active": true,
"created_at": "2026-09-01T12:00:00Z"
}
]
}
{workspace}/channels/{channel}/messages/template
Sends an Easy Template into a conversation. Alias without channel in the path: POST /api/v1/{workspace}/messages/template (pass channel or channel_id in the body, or the first workspace channel is used).
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
template or template_slug | string | Yes | Template slug |
thread_guest_id | UUID | Yes* | Guest thread (*or thread_user_id) |
thread_user_id | UUID | Yes* | Member thread |
parameters | object | No | Map of variable name to value (e.g. {"name":"Sarah"}) |
channel / channel_id | string / UUID | No | Only needed on the workspace-level alias route |
{
"template": "order-shipped",
"thread_guest_id": "c62b6623-64e9-44be-9304-f58c704f58c1",
"parameters": {
"name": "Sarah",
"order_id": "10492"
}
}
Response (201 Created)
Same message envelope as POST .../messages.
{workspace}/templates/{slug}/metrics
Engagement metrics for one template (sends, clicks, CTR).
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
days | integer | 30 | Lookback window in days |
Response (200 OK)
{
"template_id": "a1b2c3d4-1111-2222-3333-444455556666",
"template_slug": "order-shipped",
"days": 30,
"total_sends": 128,
"total_clicks": 41,
"clicks_by_type": {
"url": 30,
"quick_reply": 11
},
"ctr": 0.3203
}
Events & Webhooks
Register webhooks via the API below or in Dashboard → Integrations. Chatmode POSTs signed JSON when subscribed events occur.
{workspace}/webhooks
Lists webhooks for the workspace. Secrets are never included.
{workspace}/webhooks
Creates a webhook. The secret is returned once in the response.
Request Body (JSON)
| Parameter | Type | Description |
|---|---|---|
url | string | HTTPS (or HTTP) delivery URL |
events | string[] | One or more event names from the catalog below |
{workspace}/webhooks/{id}
Updates url, events, and/or active. Secrets are not returned.
{workspace}/webhooks/{id}
Deletes a webhook endpoint.
{workspace}/webhooks/{id}/rotate-secret
Rotates the signing secret and returns the new secret once.
{workspace}/webhooks/{id}/deliveries
Lists recent outbound deliveries for one webhook (newest first), including payload and HTTP outcome.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | int | Max 100 |
before_id | UUID | Cursor: deliveries older than this id |
status | string | pending, success, failed, or rejected |
event | string | Event name filter (e.g. message.created) |
{workspace}/webhook-deliveries/{delivery_id}
Returns one delivery including the stored JSON payload.
{workspace}/webhook-deliveries/{delivery_id}/replay
Re-POSTs the stored payload to the webhook's current URL/secret and creates a new delivery row (replay_of_id).
Event catalog
| Event | When it fires | Payload highlights |
|---|---|---|
message.created | New message from guest, member, or agent | workspace, channel, message, thread |
message.updated | Message body edited | workspace, channel, message |
thread.resolved | Conversation resolved | workspace, channel, thread, resolved_at |
thread.reopened | Conversation reopened | workspace, channel, thread |
thread.assigned | Assignee changed | workspace, channel, assignee |
csat.submitted | Guest submitted CSAT (1–5) | guest_id, rating, comment |
template.button_clicked | Template button clicked | template_slug, button_index, button_type, actor |
HMAC-SHA256 signature
Every delivery includes X-Chatmode-Signature = hex HMAC-SHA256 of the raw body with your webhook secret:
X-Chatmode-Signature: hex(hmac_sha256(webhook_secret, raw_request_body))
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret_here';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-chatmode-signature'];
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex');
const ok = crypto.timingSafeEqual(
Buffer.from(signature || '', 'hex'),
Buffer.from(expected, 'hex')
);
if (!ok) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
console.log(event.event, event);
res.status(200).json({ received: true });
});
app.listen(3000);
import hmac, hashlib
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = b"your_webhook_secret_here"
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Chatmode-Signature", "")
raw = request.get_data()
expected = hmac.new(WEBHOOK_SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, "Invalid webhook signature")
event = request.get_json()
print(event.get("event"), event)
return jsonify({"received": True}), 200
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
)
var webhookSecret = []byte("your_webhook_secret_here")
func webhookHandler(w http.ResponseWriter, r *http.Request) {
sig := r.Header.Get("X-Chatmode-Signature")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, webhookSecret)
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var payload map[string]any
_ = json.Unmarshal(body, &payload)
log.Printf("event=%v", payload["event"])
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Fatal(http.ListenAndServe(":3000", nil))
}
Errors & HTTP Status Codes
4xx/5xx responses use a consistent envelope:
{
"error": {
"code": "forbidden",
"message": "API key does not grant access to this workspace"
}
}
| Status | Error code | Meaning |
|---|---|---|
400 | invalid_json | Body missing or not valid JSON |
401 | unauthorized | Missing or invalid API key |
403 | forbidden | Key valid but wrong workspace / not allowed |
403 | insufficient_scope | Key is missing a required scope for this endpoint |
404 | not_found / template_not_found / channel_not_found | Resource missing |
422 | thread_required / template_required / invalid_template / … | Validation failure |
429 | too_many_requests | Over 60 req/min |
500 | internal | Unexpected server error |