Base URL: https://chatmode.egstars.com/api/v1
Authentication: Authorization: Bearer cm_live_...
Rate Limit: 60 req/min per key
Payloads: application/json

5-Minute Quickstart

Connect a bot, CRM, or backend to an existing Chatmode workspace in minutes.

1

Generate an API Key

Go to Dashboard → Integrations, create an API key, and copy the secret (cm_live_...). Keys are bound to your workspace.

2

Find workspace & channel slugs

Every path uses {workspace} (e.g. acme) and usually {channel} (e.g. support).

3

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.

ScopeDescription
members:readList members
guests:readList guests
guests:writeCreate, block, and unblock guests, and start visitor sessions
channels:readList channels
channels:writeCreate and update channels
threads:readList and get threads
threads:writeOpen and update threads
messages:readList messages
messages:writeSend messages, upload attachments, and send templates
templates:readList templates and metrics
webhooks:readList webhooks and delivery logs
webhooks:manageCreate, 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.

GET /api/v1/{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.

GET /api/v1/{workspace}/guests

Lists recent guests in the workspace (newest first).

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max 100
POST /api/v1/{workspace}/guests

Creates a guest by display name and returns a session token once (7-day expiry).

Request Body (JSON)

ParameterTypeDescription
display_namestringGuest 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).

POST /api/v1/{workspace}/guests/{guest_id}/block

Blocks a guest from further messaging.

POST /api/v1/{workspace}/guests/{guest_id}/unblock

Unblocks a previously blocked guest.

Channels

List, create, and update channels in the workspace.

GET /api/v1/{workspace}/channels

Lists accessible channels for the API key workspace.

Path Parameters

ParameterTypeDescription
workspacestringWorkspace 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"
    }
  ]
}
POST /api/v1/{workspace}/channels

Creates a channel (subject to plan channel limits).

Request Body (JSON)

ParameterTypeDescription
namestringChannel display name
slugstringURL slug (lowercase, unique in workspace)
guest_allowedbooleanWhether guests may join (default true on create)
PATCH /api/v1/{workspace}/channels/{channel}

Updates channel name, guest access, embed, or AI settings. Omitted fields keep current values.

Request Body (JSON)

ParameterTypeDescription
namestringChannel display name
guest_allowedbooleanWhether guests may join (default true on create)
embed_enabledbooleanEnable embed widget for this channel
embed_allowed_originsstring[]Allowed origins for the embed
ai_enabledbooleanEnable AI assistant on this channel
POST /api/v1/{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.

POST /api/v1/visitor/session

Request Body (JSON)

ParameterTypeDescription
workspacestringWorkspace slug (e.g. acme)
channelstringURL slug (lowercase, unique in workspace)
user_tokenstringHS256 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.

POST /api/v1/{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)

ParameterTypeDescription
guest_iduuidExisting guest UUID (provide this or display_name)
display_namestringCreate guest with this name, return token once, and open the thread
GET /api/v1/{workspace}/channels/{channel}/threads

Lists conversation threads in a channel, newest activity first.

Query Parameters

ParameterTypeDefaultDescription
statusstringallopen 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
    }
  ]
}
GET /api/v1/{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
  }
}
PATCH /api/v1/{workspace}/channels/{channel}/threads/{guest_id}

Resolve/reopen a conversation or assign/unassign an agent.

Request Body (JSON)

FieldTypeDescription
statusstringOptional. open or resolved
assignee_user_idUUIDOptional. Teammate to assign
clear_assigneebooleanOptional. 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.

GET /api/v1/{workspace}/channels/{channel}/messages

Lists messages oldest-first. Filter by thread and paginate with before_id.

Query Parameters

ParameterTypeDefaultDescription
thread_guest_idUUIDnoneGuest conversation filter (recommended)
thread_user_idUUIDnoneInternal member thread filter
limitinteger50Max 100
before_idUUIDnoneCursor: 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"
    }
  ]
}
POST /api/v1/{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)

FieldTypeRequiredDescription
bodystringYes*Message text (max 2,000 chars). Optional when attachment_id is set
thread_guest_idUUIDYes*Guest thread (*or thread_user_id)
thread_user_idUUIDYes*Member thread
attachment_idUUIDNoOptional. 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"
  }
}
POST /api/v1/{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).

FieldTypeRequiredDescription
filefileYesImage 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"
  }
}
GET /api/v1/{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"
    }
  ]
}
POST /api/v1/{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)

FieldTypeRequiredDescription
template or template_slugstringYesTemplate slug
thread_guest_idUUIDYes*Guest thread (*or thread_user_id)
thread_user_idUUIDYes*Member thread
parametersobjectNoMap of variable name to value (e.g. {"name":"Sarah"})
channel / channel_idstring / UUIDNoOnly 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.

GET /api/v1/{workspace}/templates/{slug}/metrics

Engagement metrics for one template (sends, clicks, CTR).

Query Parameters

ParameterTypeDefaultDescription
daysinteger30Lookback 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.

GET /api/v1/{workspace}/webhooks

Lists webhooks for the workspace. Secrets are never included.

POST /api/v1/{workspace}/webhooks

Creates a webhook. The secret is returned once in the response.

Request Body (JSON)

ParameterTypeDescription
urlstringHTTPS (or HTTP) delivery URL
eventsstring[]One or more event names from the catalog below
PATCH /api/v1/{workspace}/webhooks/{id}

Updates url, events, and/or active. Secrets are not returned.

DELETE /api/v1/{workspace}/webhooks/{id}

Deletes a webhook endpoint.

POST /api/v1/{workspace}/webhooks/{id}/rotate-secret

Rotates the signing secret and returns the new secret once.

GET /api/v1/{workspace}/webhooks/{id}/deliveries

Lists recent outbound deliveries for one webhook (newest first), including payload and HTTP outcome.

Query Parameters

ParameterTypeDescription
limitintMax 100
before_idUUIDCursor: deliveries older than this id
statusstringpending, success, failed, or rejected
eventstringEvent name filter (e.g. message.created)
GET /api/v1/{workspace}/webhook-deliveries/{delivery_id}

Returns one delivery including the stored JSON payload.

POST /api/v1/{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

EventWhen it firesPayload highlights
message.createdNew message from guest, member, or agentworkspace, channel, message, thread
message.updatedMessage body editedworkspace, channel, message
thread.resolvedConversation resolvedworkspace, channel, thread, resolved_at
thread.reopenedConversation reopenedworkspace, channel, thread
thread.assignedAssignee changedworkspace, channel, assignee
csat.submittedGuest submitted CSAT (1–5)guest_id, rating, comment
template.button_clickedTemplate button clickedtemplate_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"
  }
}
StatusError codeMeaning
400invalid_jsonBody missing or not valid JSON
401unauthorizedMissing or invalid API key
403forbiddenKey valid but wrong workspace / not allowed
403insufficient_scopeKey is missing a required scope for this endpoint
404not_found / template_not_found / channel_not_foundResource missing
422thread_required / template_required / invalid_template / …Validation failure
429too_many_requestsOver 60 req/min
500internalUnexpected server error