Chattr

Widget API Reference

REST / JSON CORS: any origin Base URL: https://chattr.ge

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.

Credentials — 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

ConceptDescription
site_keyUUID 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_uuidUUID you generate once per device/browser and persist in a cookie. Ties all conversations from the same visitor together.
conversation uuidUUID returned when a conversation is created. Used in all subsequent calls for that conversation.
conversation_idNumeric ID — only needed to subscribe to the Pusher real-time channel.

Step-by-step Flow

  1. Call GET /widget/config?site={site_key} on init to get your Pusher credentials and site settings. Your site_key is in the Chattr dashboard → Integrations → Custom Widget.
  2. Generate visitor_uuid once per browser (UUID v4) and store it in a cookie (1-year expiry).
  3. Call GET /widget/visitor-conversations to check if this visitor already has an open conversation.
  4. If an open conversation exists: resume it — load messages, subscribe to Pusher.
  5. If not: show your pre-chat UI (name/email fields or skip based on site config), then call POST /widget/conversations.
  6. After creating or resuming, subscribe to the Pusher channel for real-time incoming messages.
  7. Use GET /widget/conversations/{uuid}/messages as a polling fallback (5s offline, 15s when Pusher is connected).

1. Get Site Config

GET /widget/config?site={site_key} Returns Pusher credentials + site settings in one JSON call

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
}
Fetch this once on init and cache it. All the values you need to configure Pusher.js and show the pre-chat form are in this single response.

2. List Visitor's Conversations

GET /widget/visitor-conversations Check for existing conversations to resume

Call this on page load before showing any UI. If an active (non-resolved) conversation exists, go straight to the chat view.

Query Parameters

ParamTypeRequiredDescription
visitor_uuidUUID stringYesVisitor's persistent device UUID
site_keyUUID stringYesYour 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

Resume any conversation where 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

POST /widget/conversations Create a new conversation and send the first message 30 req/min

Request Body (JSON)

FieldTypeRequiredNotes
site_keyUUIDYes
visitor_uuidUUIDYesGenerate once, store in cookie
messagestring (max 5000)YesFirst message text
visitor_namestring (max 255)NoDefaults to "Visitor"
visitor_emailemail (max 255)No
visitor_phonestring (max 50)No
visitor_pagestring (max 2000)NoCurrent page URL
visitor_metaobjectNoAny 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
}
Store data.uuid in a cookie (e.g. chattr_conv_{site_key}), then subscribe to the Pusher channel using data.conversation_id.

4. Get Messages

GET /widget/conversations/{uuid}/messages Load all messages — use as initial load and polling fallback

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
}
FieldValues
sender_typevisitor | operator | system
typetext | image | file
statusopen | pending | resolved
Recommended poll interval: 5s when Pusher is disconnected, 15s when Pusher is connected (as a consistency check).

5. Send a Message

POST /widget/conversations/{uuid}/messages Send text or a file attachment 120 req/min

Text message (Content-Type: application/json)

{ "body": "Can you help me with order #1234?" }

File attachment (Content-Type: multipart/form-data)

FieldTypeRequiredNotes
bodystringNo*Text caption. Required if no attachment.
attachmentfileNo*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.

Returns 422 if the conversation is resolved. Call reopen first.

6. Typing Indicator

POST /widget/conversations/{uuid}/typing Notify the operator that the visitor is typing 120 req/min
{ "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

POST /widget/conversations/{uuid}/rating Submit a satisfaction rating (1–5 stars) 20 req/min
FieldTypeRequiredNotes
ratingintegerYes1–5
commentstring (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

POST /widget/conversations/{uuid}/reopen Re-open a resolved conversation so the visitor can send more messages 20 req/min

No request body required.

Response 200

{ "success": true, "data": { "status": "open" }, "message": null }

9. Preset Q&A Buttons

GET /widget/preset-qa?site={site_key} Get quick-reply buttons configured by the operator

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
}
FieldNotes
question_displayLabel to show on the button
answerAuto-reply text rendered when clicked
trigger_keywordsShow this button when any keyword matches visitor's typed text
is_stickyAlways show (vs. only on keyword match)
Handling a click: Send 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:

SettingMeaning
preChatEnabledShow the form at all — if false, let the visitor type immediately
preChatAskNameInclude a name field
preChatAskEmailInclude 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 StatusMeaning
404Site key or conversation UUID not found
422Validation error or business rule violation (e.g. conversation is resolved)
429Rate limit exceeded — back off and retry. Use exponential backoff starting at 2s.

Per-endpoint rate limits

EndpointLimit
POST /widget/conversations30 req/min
POST /widget/conversations/{uuid}/messages120 req/min
POST /widget/conversations/{uuid}/typing120 req/min
POST /widget/conversations/{uuid}/rating20 req/min
POST /widget/conversations/{uuid}/reopen20 req/min
All GET widget endpoints240 req/min (shared)

Powered by Chattr — Live Chat Platform