Chattr

Operator API Reference

REST / JSON Sanctum Bearer Token Base URL: https://chattr.ge/api

This is the authenticated REST API behind the Chattr operator dashboard and the Android app — everything a custom integration or bot needs to manage sites, read and reply to conversations, and keep the AI's RAG knowledge base up to date. It is separate from the anonymous Widget API, which visitors use to actually chat.

Every endpoint on this page (except register/login) requires an Authorization: Bearer {token} header. Tokens are Laravel Sanctum personal access tokens returned by /api/auth/register or /api/auth/login — there is no separate API key or site secret involved in authenticating the request itself (the site_key concept only exists on the anonymous widget side).

Authentication

  1. Call POST /api/auth/register (new account) or POST /api/auth/login (existing account) to receive a token.
  2. Store it securely (Keychain/Keystore on mobile, server-side session on a backend integration — never in client-side JS on a public page).
  3. Send it as Authorization: Bearer {token} on every request below.
  4. Call POST /api/auth/logout to revoke it when the operator signs out.
Accounts with two-factor authentication enabled must include two_factor_code (a 6-digit TOTP code, or an unused recovery code) in the login body. Omitting it when 2FA is required returns 422 with data.requires_2fa: true so your client can prompt for the code and retry.
Banned accounts (is_banned = true) are rejected with 403 at login, and any existing token for a banned account is rejected on every subsequent request too — the ban check runs on the Sanctum token guard itself, not just at login time.

Response Format

Every endpoint returns the same envelope:

{
  "success": true,
  "data": { "...": "..." },
  "message": "Human-readable message, or null"
}

List endpoints that paginate (conversations, messages) add a meta object alongside data:

"meta": {
  "current_page": 1,
  "last_page": 4,
  "total": 83,
  "per_page": 25
}

Validation failures return 422 with an additional errors object (Laravel's standard {"field": ["message"]} shape). See Errors & Rate Limits for the full status code table.

Register & Login

POST /api/auth/register Create an operator account

Request Body

FieldTypeRequiredNotes
namestringYesMax 255
emailstringYesMust be unique
passwordstringYesMin 8 chars, must match password_confirmation

Response 201

{
  "success": true,
  "data": {
    "user": { "id": 12, "name": "Nino", "email": "nino@example.com", "...": "..." },
    "token": "1|abcdef123456..."
  },
  "message": "Registration successful."
}
POST /api/auth/login Exchange credentials for a bearer token

Request Body

FieldTypeRequiredNotes
emailstringYes
passwordstringYes
two_factor_codestringConditionalRequired only if the account has 2FA enabled

Response 200

{ "success": true, "data": { "user": {"...": "..."}, "token": "2|..." }, "message": "Login successful." }

Errors: 401 invalid credentials · 403 account suspended · 422 with data.requires_2fa: true when a TOTP/recovery code is missing or wrong.

POST /api/auth/logout Revoke the current token

Profile & Status

GET /api/auth/me Current operator profile
PUT /api/auth/me Update profile / notification preferences

Request Body (all fields optional)

FieldTypeNotes
name, emailstringemail must stay unique
passwordstringMin 8, requires password_confirmation
avatarstringURL/path
show_online_status, show_typing_indicator, send_read_receiptsbooleanPer-operator privacy toggles
away_messagestringMax 500
PUT /api/auth/fcm-token Register a Firebase Cloud Messaging token (Android app)

Body: fcm_token (required string, max 500).

PUT /api/auth/status Toggle online/away

Body: is_online (required bool), away_message (optional string, max 500).

Sites

GET /api/sites List sites you own or operate
POST /api/sites Create a site

Body: name (required), domain (required), settings (optional object, merged into the default settings shown below).

Response 201

{
  "success": true,
  "data": {
    "id": 4,
    "name": "My Shop",
    "domain": "myshop.ge",
    "site_key": "b1e2c3d4-....-....-....-............",
    "settings": { "widget_color": "#2D3FE0", "widget_position": "bottom-right", "...": "..." },
    "unread_count": 0,
    "created_at": "2026-07-27T10:00:00.000000Z"
  },
  "message": "Site created."
}
A newly created site automatically gets a random UUID site_key — this is the value visitors' widget requests use, and what GET /api/sites/{site}/embed-code returns pre-built into a script tag.
GET /api/sites/{site} Get one site
PUT /api/sites/{site} Update a site Owner only

Body (all optional): name, domain, and any settings.* key — e.g. settings.widget_color (hex), settings.widget_position (bottom-right/bottom-left), settings.pre_chat_form_enabled, settings.pre_chat_ask_name/_email/_phone (+ matching _required flags), settings.show_operator_status, settings.show_typing_indicator, settings.show_read_receipts, settings.offline_email_enabled, settings.offline_message, settings.welcome_message, settings.language.

settings is deep-merged with the site's existing settings — send only the keys you want to change, the rest are preserved.
DELETE /api/sites/{site} Delete a site and everything under it Owner only
GET /api/sites/{site}/embed-code Ready-to-paste widget <script> tag

Operators

GET /api/sites/{site}/operators List operators on a site
POST /api/sites/{site}/operators/invite Invite an existing user by email Owner only

Body: email (required, must belong to an existing Chattr account). Returns 422 if that user is already an operator on the site.

DELETE /api/sites/{site}/operators/{userId} Remove an operator Owner only

Returns 422 if {userId} is your own id — the owner can't remove themselves this way.

Analytics & Business Hours

GET /api/sites/{site}/analytics Conversation volume, ratings, response time, department/tag breakdown
{
  "success": true,
  "data": {
    "site_id": 4,
    "total_conversations": 214,
    "open_conversations": 6,
    "resolved_today": 11,
    "avg_response_time_seconds": 87,
    "avg_rating": 4.6,
    "messages_count": 1830,
    "unassigned_rate_percent": 3.2,
    "department_volume": [ { "id": 1, "name": "Sales", "conversations": 90 } ],
    "tag_volume": [ { "id": 2, "name": "billing", "color": "#5e6c84", "conversations": 14 } ],
    "help_article_views": 58,
    "help_deflection_rate_percent": 21.3
  },
  "message": null
}
GET /api/sites/{site}/business-hours Configured weekly schedule + timezone

List & Search Conversations

GET /api/conversations Paginated inbox listing across all your sites

Query Parameters

ParamTypeNotes
site_idintRestrict to one site
statusstringopen | pending | resolved
assigned_tostringme | unassigned
unreadboolOnly conversations with unread visitor messages
department_idint
tag_idint
channelstringweb | telegram
per_pageintMax 100, default 25
Proactive-trigger conversations that the visitor hasn't replied to yet (is_proactive=true, proactive_engaged=false) are excluded automatically — they aren't "real" inbox items until the visitor responds.
GET /api/conversations/search?q= Full-text search across visitor name/email and message bodies

Query: q (required, min 2 chars), site_id (optional). Internal notes are excluded from the message-body match. Capped at 50 results.

GET /api/conversations/{conversation} Get one conversation

Status, Assignment & Presence

PUT /api/conversations/{conversation}/status Change status

Body: status (required, open/pending/resolved). Setting resolved queues a conversation summarization job used for cross-session visitor memory.

PUT /api/conversations/{conversation}/assign Assign or unassign an operator

Body: operator_id (nullable int — omit/null to unassign).

PUT /api/conversations/{conversation}/department Move to a department and re-route

Body: department_id (nullable int, must belong to the conversation's site). Triggers the site's routing strategy (manual/round-robin/least-busy) for the new department.

POST /api/conversations/{conversation}/join Join a conversation

Also flips ai_active to false — an operator joining always takes over from the AI.

POST /api/conversations/{conversation}/leave Leave a conversation
POST /api/conversations/{conversation}/typing Broadcast an operator typing indicator

Body: is_typing (required bool). Broadcasts OperatorTyping on conversation.{uuid}.

POST /api/conversations/{conversation}/read Mark all unread visitor messages as read

Messages

GET /api/conversations/{conversation}/messages List messages, newest first, paginated

Query: per_page (max 100, default 50).

POST /api/conversations/{conversation}/messages Send a text message or file/image attachment

Request Body (multipart or JSON)

FieldTypeRequiredNotes
bodystringConditionalRequired unless attachment is present. Max 10,000 chars.
attachmentfileConditionalRequired unless body is present. Max 10MB; jpg,jpeg,png,gif,webp,pdf,doc,docx,xls,xlsx,zip,txt.
typestringNotext (default) / image / file / note
Sending a message here always disables ai_active on the conversation (the operator has taken over) and forwards the message through any connected channel adapter — e.g. it's relayed to Telegram if that conversation came in through a connected Telegram bot.
PUT /api/conversations/{conversation}/messages/{message} Edit your own message

Body: body (required). Returns 403 if the message wasn't sent by you.

POST /api/conversations/{conversation}/messages/{message}/react Toggle an emoji reaction

Body: emoji (required, max 10 chars). Calling it again with the same emoji from the same operator removes the reaction.

POST /api/conversations/{conversation}/messages/{message}/feedback Thumbs up/down an AI-generated reply

Body: feedback (required, good or bad). Returns 422 if the message wasn't AI-generated.

Internal Notes

POST /api/conversations/{conversation}/notes Add an operator-only note

Body: body (required, max 10,000).

Notes are never broadcast on the visitor-facing conversation.{uuid} channel — they only surface to other operators through the authenticated messages endpoint. The visitor has no way to see them, even by inspecting network traffic.

Saved Replies

GET /api/saved-replies?search=&site_id= List/search your saved replies

Pass site_id to include both that site's replies and your personal (site-less) ones. search matches shortcut, title, and body.

POST /api/saved-replies Create a saved reply
FieldTypeRequiredNotes
shortcutstringYese.g. /hello, max 100
titlestringYesMax 255
bodystringYesMax 5,000
site_idintNoOmit for a personal reply usable on any site
keywordsstringNoComma-separated, used for search matching
is_stickyboolNoPins it to the top of the picker
PUT /api/saved-replies/{savedReply} Update (owner of the reply only)
DELETE /api/saved-replies/{savedReply} Delete (owner of the reply only)

Knowledge Base (RAG)

Per-site documents that get chunked, embedded (OpenAI text-embedding-3-small), and retrieved by the AI at reply time — the top 5 most relevant chunks are injected into the system prompt on every AI turn. All write endpoints are owner-only.

GET /api/sites/{site}/knowledge-base List documents with status + chunk count
{
  "success": true,
  "data": [
    {
      "id": 9,
      "title": "Return Policy",
      "source_type": "url",
      "source_url": "https://myshop.ge/returns",
      "original_filename": null,
      "status": "ready",
      "chunk_count": 6,
      "error_message": null,
      "is_public": false,
      "slug": null,
      "category": null,
      "sort_order": 0,
      "views_count": 0,
      "created_at": "2026-07-01T09:00:00.000000Z"
    }
  ],
  "message": null
}

status transitions: pendingprocessingready (or failed, see error_message).

Upload a File / ZIP

POST /api/sites/{site}/knowledge-base/upload Upload a single document Owner only

Multipart body: file (required, max 20MB, pdf,docx,txt,md,json). Returns the created document at status: "pending" and queues ingestion asynchronously (extraction → chunking → embedding).

POST /api/sites/{site}/knowledge-base/upload-zip Bulk-upload a ZIP of documents Owner only

Multipart body: file (required, max 50MB, .zip). Extracts up to 20 supported files (skips unsupported types, empty files, and anything over 20MB per entry — with a count of skipped files in the response message) and queues each as its own document.

Submit a URL to Scrape

POST /api/sites/{site}/knowledge-base/url Ingest a public web page Owner only

Body: url (required, public http/https only), title (optional, defaults to the page's host+path).

Kept in sync automatically. Every URL-sourced document is re-fetched and re-embedded once a day by a scheduled job (knowledge-base:resync-urls, 04:00 server time) — if the page content changes, the AI's knowledge updates on its own within 24 hours. Use POST .../reprocess to force an immediate refresh instead of waiting for the schedule.
Private, loopback, and link-local hosts (e.g. cloud metadata endpoints) are rejected up front — only public http/https URLs are accepted, and every redirect hop is re-validated at fetch time.

Update, Reprocess & Delete

PUT /api/sites/{site}/knowledge-base/{document} Update metadata / publish as a Help Center article Owner only

Body (all optional): is_public (bool — exposes it in the widget's Help tab, auto-generates a slug the first time it's set), title, category, sort_order.

POST /api/sites/{site}/knowledge-base/{document}/reprocess Force a re-fetch + re-embed right now Owner only

No body. Resets status to pending and re-dispatches ingestion — old chunks are deleted and replaced once the new ones finish embedding.

DELETE /api/sites/{site}/knowledge-base/{document} Delete a document and its chunks Owner only

Real-time (Pusher / Soketi)

The operator dashboard subscribes to the same self-hosted Pusher-compatible server (Soketi) the widget uses. Private channels are authorized through Laravel's broadcasting auth endpoint using the same Sanctum bearer token — point your Pusher client's authEndpoint at /broadcasting/auth and set the Authorization header.

ChannelTypeEvents
private-site.{siteKey}Private — owner/operators onlyNewConversation (new inbox item)
conversation.{uuid}Public, keyed by unguessable UUIDMessageSent, MessageEdited, MessageReacted, OperatorTyping, VisitorTyping, ReadReceiptSent, ConversationStatusChanged, AiMessageChunk
private-operator.{operatorId}Private — that operator onlyReserved for future per-operator notifications; no event broadcasts on it yet
Internal notes (type: "note") are never broadcast on conversation.{uuid} — fetch them via the authenticated messages endpoint only.

Errors & Rate Limits

StatusMeaning
401Missing, invalid, or revoked bearer token
403Authenticated, but not the owner/operator of this site/conversation/message — or the account is banned
404Record not found, or doesn't belong to the given parent (e.g. a message from a different conversation)
422Validation error — see the errors object for per-field messages
429Rate limit exceeded — back off and retry

Rate limit

The API applies Laravel's default api throttle: 60 requests/minute per authenticated user.

Powered by Chattr — Live Chat Platform