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",
    "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. 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).

// 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