Widget API Reference
Build a fully custom chat widget UI while Chattr handles everything on the backend — conversations, real-time delivery, operator panel, FCM push, and email notifications. You call the REST API below; the operator sees everything in the Chattr dashboard and app as normal.
site_key is required. There are no auth headers or API tokens. Instead, every request that creates or looks up site data requires your site_key (a UUID from your Chattr dashboard). It identifies which operator account receives the conversation. The site_key is intentionally public — it lives in your website's embed code — so it is a routing identifier, not a secret. Once a conversation is created, subsequent calls (send message, typing, etc.) use the conversation uuid returned at creation — no site_key needed for those.
Core Concepts
| Concept | Description |
|---|---|
site_key | UUID identifying your site — copy it from the Chattr dashboard → Sites. Required on all requests that target a site (start conversation, list conversations, preset Q&A). This is your account credential; without it the backend doesn't know which operator to route messages to. |
visitor_uuid | UUID you generate once per device/browser and persist in a cookie. Ties all conversations from the same visitor together. |
conversation uuid | UUID returned when a conversation is created. Used in all subsequent calls for that conversation. |
conversation_id | Numeric ID — only needed to subscribe to the Pusher real-time channel. |
Step-by-step Flow
- Call GET /widget/config?site={site_key} on init to get your Pusher credentials and site settings. Your
site_keyis in the Chattr dashboard → Integrations → Custom Widget. - Generate
visitor_uuidonce per browser (UUID v4) and store it in a cookie (1-year expiry). - Call GET /widget/visitor-conversations to check if this visitor already has an open conversation.
- If an open conversation exists: resume it — load messages, subscribe to Pusher.
- If not: show your pre-chat UI (name/email fields or skip based on site config), then call POST /widget/conversations.
- After creating or resuming, subscribe to the Pusher channel for real-time incoming messages.
- Use GET /widget/conversations/{uuid}/messages as a polling fallback (5s offline, 15s when Pusher is connected).
1. Get Site Config
Start here. Returns everything your custom widget needs to connect — Pusher key, cluster, host, port, and all site settings. Your site key is in the Chattr dashboard → Integrations → Custom Widget section.
Response 200
{
"success": true,
"data": {
"site_key": "your-site-uuid",
"pusher_key": "xxxxxxxxxxxxxxxx",
"pusher_cluster": "mt1",
"pusher_host": "soketi.chattr.ge",
"pusher_port": 443,
"pusher_tls": true,
"widget_color": "#2D3FE0",
"widget_position": "bottom-right",
"widget_vertical_offset": 22,
"widget_button_style": "gradient",
"widget_icon_style": "classic",
"welcome_message": "Hello! How can we help you?",
"pre_chat_enabled": true,
"pre_chat_ask_name": true,
"pre_chat_name_required": true,
"pre_chat_ask_email": true,
"pre_chat_email_required": false,
"pre_chat_ask_phone": false,
"pre_chat_phone_required": false,
"show_typing": true,
"show_read_receipts": true
},
"message": null
}
2. List Visitor's Conversations
Call this on page load before showing any UI. If an active (non-resolved) conversation exists, go straight to the chat view.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
visitor_uuid | UUID string | Yes | Visitor's persistent device UUID |
site_key | UUID string | Yes | Your site key |
Response 200
{
"success": true,
"data": [
{
"uuid": "conv-uuid-here",
"status": "open",
"created_at": "2025-05-24T10:00:00.000000Z",
"last_message_at": "2025-05-24T10:05:00.000000Z",
"preview": "Hi, I need help with my order…"
}
],
"message": null
}
Status values: open | pending | resolved
status !== "resolved". If multiple, prefer the one matching a stored conversation UUID cookie, then fall back to the most recent active one.3. Start a Conversation
Request Body (JSON)
| Field | Type | Required | Notes |
|---|---|---|---|
site_key | UUID | Yes | |
visitor_uuid | UUID | Yes | Generate once, store in cookie |
message | string (max 5000) | Yes | First message text |
visitor_name | string (max 255) | No | Defaults to "Visitor" |
visitor_email | email (max 255) | No | |
visitor_phone | string (max 50) | No | |
visitor_page | string (max 2000) | No | Current page URL |
visitor_meta | object | No | Any key-value pairs — shown to the operator alongside the conversation. A specific subset is also given to the AI assistant as background (see the note below). Send as many custom fields as you need. |
visitor_meta accepts any arbitrary keys. Common ones: id, firstName, lastName, fullName, email, company, phone, address1, city, country, language, status, currency, groupName — but you can include any custom field (e.g. planTier, accountId, orderId, region).
AI use of visitor_meta. When AI replies are enabled, a fixed subset of these fields is also included in the assistant's context so it can answer with awareness of the visitor's account, which means those values are sent to the configured AI provider:
fullName, firstName, lastName, email, company, phone, city, state, country, language, status, currency, groupName, activeServices, openTickets, pendingInvoices, totalInvoicesDue.
Everything else you send — including id, address1, address2, postcode and any custom keys of your own — is shown to operators only and never reaches the model. Values are truncated to 200 characters and flattened to a single line.
Because visitor_meta is posted by the browser, the assistant is told to treat it as unverified and not to rely on it for anything consequential — entitlements, balances, discounts. Enable HMAC identity verification (hmac_verify_enabled, with a signed identity object on conversation start) and the assistant is told it may rely on these values instead. To keep visitor_meta out of the AI's context entirely, turn off ai_use_visitor_context in your site's AI settings.
// Request body example
{
"site_key": "your-site-uuid",
"visitor_uuid": "generated-visitor-uuid",
"message": "Hello, I need help!",
"visitor_name": "Jane Doe",
"visitor_email": "jane@example.com",
"visitor_page": "https://yoursite.com/checkout",
"visitor_meta": {
"id": "12345",
"fullName": "Jane Doe",
"company": "Acme Corp",
"phone": "+1 555 000 0000",
"planTier": "enterprise",
"accountId": "acc_9xkZ2",
"region": "us-east"
}
}
Response 201
{
"success": true,
"data": {
"uuid": "conv-uuid-here",
"conversation_id": 42,
"messages": [
{
"id": 1,
"conversation_id": 42,
"sender_type": "visitor",
"sender_id": null,
"sender": null,
"type": "text",
"body": "Hello, I need help!",
"attachment_path": null,
"attachment_name": null,
"attachment_size": null,
"reactions": [],
"is_read": false,
"read_at": null,
"edited_at": null,
"created_at": "2025-05-24T10:00:00.000000Z"
}
]
},
"message": null
}
data.uuid in a cookie (e.g. chattr_conv_{site_key}), then subscribe to the Pusher channel using data.conversation_id.4. Get Messages
Returns all messages (notes are excluded — those are internal operator-only). Also returns the current conversation status and numeric conversation_id needed for Pusher.
Response 200
{
"success": true,
"data": [
{
"id": 1,
"conversation_id": 42,
"sender_type": "visitor",
"sender_id": null,
"sender": null,
"type": "text",
"body": "Hello, I need help!",
"attachment_path": null,
"attachment_name": null,
"attachment_size": null,
"reactions": [],
"is_read": false,
"read_at": null,
"edited_at": null,
"created_at": "2025-05-24T10:00:00.000000Z"
},
{
"id": 2,
"conversation_id": 42,
"sender_type": "operator",
"sender_id": 7,
"sender": { "id": 7, "name": "Support Agent", "avatar": null },
"type": "text",
"body": "Hi Jane, happy to help!",
"attachment_path": null,
"attachment_name": null,
"attachment_size": null,
"reactions": [{ "emoji": "👍", "operator_id": 7 }],
"is_read": true,
"read_at": "2025-05-24T10:01:30.000000Z",
"edited_at": null,
"created_at": "2025-05-24T10:01:00.000000Z"
}
],
"conversation_id": 42,
"status": "open",
"message": null
}
| Field | Values |
|---|---|
sender_type | visitor | operator | system |
type | text | image | file |
status | open | pending | resolved |
5. Send a Message
Text message (Content-Type: application/json)
{ "body": "Can you help me with order #1234?" }
File attachment (Content-Type: multipart/form-data)
| Field | Type | Required | Notes |
|---|---|---|---|
body | string | No* | Text caption. Required if no attachment. |
attachment | file | No* | Max 10 MB. Allowed: jpg, jpeg, png, gif, webp, pdf, doc, docx, xls, xlsx, zip, txt |
Response 201
{
"success": true,
"data": {
"id": 5,
"type": "image",
"body": "See this screenshot",
"attachment_path": "https://chattr.ge/storage/attachments/42/screenshot.jpg",
"attachment_name": "screenshot.jpg",
"attachment_size": 204800,
"sender_type": "visitor",
"created_at": "2025-05-24T10:03:00.000000Z"
},
"message": null
}
type is set to "image" for image MIME types, "file" for all others.
422 if the conversation is resolved. Call reopen first.6. Typing Indicator
{ "is_typing": true }
Send is_typing: false after 3 seconds of inactivity. The operator panel shows a live typing indicator.
Response 200
{ "success": true, "data": null, "message": null }
7. Rate a Conversation
| Field | Type | Required | Notes |
|---|---|---|---|
rating | integer | Yes | 1–5 |
comment | string (max 1000) | No |
{ "rating": 5, "comment": "Very helpful, thank you!" }
Response 200
{ "success": true, "data": { "rating": 5 }, "message": "Thank you for your feedback!" }
8. Reopen a Resolved Conversation
No request body required.
Response 200
{ "success": true, "data": { "status": "open" }, "message": null }
9. Preset Q&A Buttons
Response 200
{
"success": true,
"data": [
{
"id": 1,
"question_display": "What are your hours?",
"answer": "We're available Monday–Friday, 9am–6pm GMT.",
"trigger_keywords": ["hours", "open", "available"],
"is_sticky": true
}
],
"message": null
}
| Field | Notes |
|---|---|
question_display | Label to show on the button |
answer | Auto-reply text rendered when clicked |
trigger_keywords | Show this button when any keyword matches visitor's typed text |
is_sticky | Always show (vs. only on keyword match) |
question_display as a visitor message via the API, then immediately render answer as an operator bubble in your UI — no extra API call needed for the answer.
Real-time via Pusher
Subscribe to the conversation channel after creating or resuming a conversation. Use Pusher.js (or any Pusher-compatible client library).
Setup
const pusher = new Pusher(PUSHER_KEY, {
cluster: PUSHER_CLUSTER, // e.g. "mt1"
forceTLS: true
});
// conversationUuid = the `uuid` returned from POST /widget/conversations
// (the unguessable conversation identifier). The channel is keyed by uuid,
// never the numeric id, so it cannot be subscribed to by guessing ids.
const channel = pusher.subscribe(`conversation.${conversationUuid}`);
Event: MessageSent
Fires when any message is sent. The event payload has the same shape as a message object from GET /messages.
channel.bind('MessageSent', (data) => {
// Skip internal operator notes
if (data.type === 'note') return;
// Deduplicate — you may have already added it optimistically
const alreadyHave = messages.some(m => m.id && m.id === data.id);
if (!alreadyHave) {
messages.push(data);
renderMessages();
}
});
Event: OperatorTyping
channel.bind('OperatorTyping', ({ is_typing }) => {
typingIndicator.hidden = !is_typing;
});
Session Management
Two cookies drive the session. Generate them client-side — no server interaction needed.
// Persistent visitor identity (1 per browser, 1-year expiry)
function getVisitorUuid() {
let id = getCookie('chattr_uuid');
if (!id) {
id = crypto.randomUUID();
setCookie('chattr_uuid', id, 365);
}
return id;
}
// Per-site conversation pointer (30-day expiry)
function getConvUuid(siteKey) {
return getCookie(`chattr_conv_${siteKey}`);
}
function setConvUuid(siteKey, uuid) {
setCookie(`chattr_conv_${siteKey}`, uuid, 30);
}
// Cookie helpers
function getCookie(name) {
const m = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
return m ? m[2] : null;
}
function setCookie(name, value, days) {
const exp = new Date(Date.now() + days * 86400000).toUTCString();
document.cookie = `${name}=${value};expires=${exp};path=/;SameSite=Lax`;
}
Pre-chat form logic
Read the site config to decide what to show before the first message:
| Setting | Meaning |
|---|---|
preChatEnabled | Show the form at all — if false, let the visitor type immediately |
preChatAskName | Include a name field |
preChatAskEmail | Include an email field |
Visitor identity pre-fill (skip pre-chat form)
If you already know the logged-in user (e.g. SaaS app), pass their data in visitor_meta and skip the form entirely:
startConversation(user.name, user.email, firstMessage, {
id: user.id,
fullName: user.name,
email: user.email,
company: user.company,
});
Full Vanilla JS Example
A minimal working implementation covering the full flow (~70 lines).
const BASE = 'https://chattr.ge';
const SITE = 'your-site-key-uuid';
const VIS_ID = getVisitorUuid();
let convUuid = null;
let messages = [];
async function init() {
// 1. Check for existing conversations
const r = await fetch(`${BASE}/widget/visitor-conversations?visitor_uuid=${VIS_ID}&site_key=${SITE}`);
const { data: convs } = await r.json();
const active = convs.find(c => c.status !== 'resolved');
if (active) {
convUuid = active.uuid;
await loadMessages(); // loads messages + subscribes Pusher
} else {
showPreChatForm();
}
}
async function startConversation(name, email, firstMessage, meta = null) {
const r = await fetch(`${BASE}/widget/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
site_key: SITE,
visitor_uuid: VIS_ID,
visitor_name: name,
visitor_email: email,
message: firstMessage,
visitor_page: location.href,
visitor_meta: meta,
}),
});
const { data } = await r.json();
convUuid = data.uuid;
setConvUuid(SITE, convUuid);
messages = data.messages;
subscribeRealtime(data.conversation_id);
renderMessages();
}
async function sendMessage(text) {
const r = await fetch(`${BASE}/widget/conversations/${convUuid}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: text }),
});
const { data: msg } = await r.json();
messages.push(msg);
renderMessages();
}
async function loadMessages() {
const r = await fetch(`${BASE}/widget/conversations/${convUuid}/messages`);
const res = await r.json();
messages = res.data;
subscribeRealtime(convUuid);
renderMessages();
if (res.status === 'resolved') showReopenBar();
}
function subscribeRealtime(conversationUuid) {
const pusher = new Pusher(PUSHER_KEY, { cluster: PUSHER_CLUSTER, forceTLS: true });
const ch = pusher.subscribe(`conversation.${conversationUuid}`);
ch.bind('MessageSent', (msg) => {
if (msg.type === 'note') return;
if (messages.some(m => m.id === msg.id)) return; // deduplicate
messages.push(msg);
renderMessages();
playNotificationSound();
});
ch.bind('OperatorTyping', ({ is_typing }) => {
document.getElementById('typing-indicator').hidden = !is_typing;
});
// Slow poll as fallback
setInterval(() => {
if (convUuid) loadMessages();
}, ch.connection?.state === 'connected' ? 15000 : 5000);
}
function sendTypingIndicator(isTyping) {
if (!convUuid) return;
fetch(`${BASE}/widget/conversations/${convUuid}/typing`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_typing: isTyping }),
});
}
init();
Errors & Rate Limits
All endpoints return a consistent shape on error:
{
"success": false,
"data": null,
"message": "Error description"
}
| HTTP Status | Meaning |
|---|---|
404 | Site key or conversation UUID not found |
422 | Validation error or business rule violation (e.g. conversation is resolved) |
429 | Rate limit exceeded — back off and retry. Use exponential backoff starting at 2s. |
Per-endpoint rate limits
| Endpoint | Limit |
|---|---|
| POST /widget/conversations | 30 req/min |
| POST /widget/conversations/{uuid}/messages | 120 req/min |
| POST /widget/conversations/{uuid}/typing | 120 req/min |
| POST /widget/conversations/{uuid}/rating | 20 req/min |
| POST /widget/conversations/{uuid}/reopen | 20 req/min |
| All GET widget endpoints | 240 req/min (shared) |
Powered by Chattr — Live Chat Platform