Operator API Reference
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.
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
- Call POST /api/auth/register (new account) or POST /api/auth/login (existing account) to receive a
token. - Store it securely (Keychain/Keystore on mobile, server-side session on a backend integration — never in client-side JS on a public page).
- Send it as
Authorization: Bearer {token}on every request below. - Call POST /api/auth/logout to revoke it when the operator signs out.
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.
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
Request Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Max 255 |
email | string | Yes | Must be unique |
password | string | Yes | Min 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."
}
Request Body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Yes | |
password | string | Yes | |
two_factor_code | string | Conditional | Required 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.
Profile & Status
Request Body (all fields optional)
| Field | Type | Notes |
|---|---|---|
name, email | string | email must stay unique |
password | string | Min 8, requires password_confirmation |
avatar | string | URL/path |
show_online_status, show_typing_indicator, send_read_receipts | boolean | Per-operator privacy toggles |
away_message | string | Max 500 |
Body: fcm_token (required string, max 500).
Body: is_online (required bool), away_message (optional string, max 500).
Sites
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."
}
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.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.<script> tag
Operators
Body: email (required, must belong to an existing Chattr account). Returns 422 if that user is already an operator on the site.
Returns 422 if {userId} is your own id — the owner can't remove themselves this way.
Analytics & Business Hours
{
"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
}
List & Search Conversations
Query Parameters
| Param | Type | Notes |
|---|---|---|
site_id | int | Restrict to one site |
status | string | open | pending | resolved |
assigned_to | string | me | unassigned |
unread | bool | Only conversations with unread visitor messages |
department_id | int | |
tag_id | int | |
channel | string | web | telegram |
per_page | int | Max 100, default 25 |
is_proactive=true, proactive_engaged=false) are excluded automatically — they aren't "real" inbox items until the visitor responds.Query: q (required, min 2 chars), site_id (optional). Internal notes are excluded from the message-body match. Capped at 50 results.
Status, Assignment & Presence
Body: status (required, open/pending/resolved). Setting resolved queues a conversation summarization job used for cross-session visitor memory.
Body: operator_id (nullable int — omit/null to unassign).
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.
Also flips ai_active to false — an operator joining always takes over from the AI.
Body: is_typing (required bool). Broadcasts OperatorTyping on conversation.{uuid}.
Messages
Query: per_page (max 100, default 50).
Request Body (multipart or JSON)
| Field | Type | Required | Notes |
|---|---|---|---|
body | string | Conditional | Required unless attachment is present. Max 10,000 chars. |
attachment | file | Conditional | Required unless body is present. Max 10MB; jpg,jpeg,png,gif,webp,pdf,doc,docx,xls,xlsx,zip,txt. |
type | string | No | text (default) / image / file / note |
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.Body: body (required). Returns 403 if the message wasn't sent by you.
Body: emoji (required, max 10 chars). Calling it again with the same emoji from the same operator removes the reaction.
Body: feedback (required, good or bad). Returns 422 if the message wasn't AI-generated.
Internal Notes
Body: body (required, max 10,000).
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
Pass site_id to include both that site's replies and your personal (site-less) ones. search matches shortcut, title, and body.
| Field | Type | Required | Notes |
|---|---|---|---|
shortcut | string | Yes | e.g. /hello, max 100 |
title | string | Yes | Max 255 |
body | string | Yes | Max 5,000 |
site_id | int | No | Omit for a personal reply usable on any site |
keywords | string | No | Comma-separated, used for search matching |
is_sticky | bool | No | Pins it to the top of the picker |
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.
{
"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: pending → processing → ready (or failed, see error_message).
Upload a File / ZIP
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).
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
Body: url (required, public http/https only), title (optional, defaults to the page's host+path).
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.
Update, Reprocess & Delete
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.
No body. Resets status to pending and re-dispatches ingestion — old chunks are deleted and replaced once the new ones finish embedding.
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.
| Channel | Type | Events |
|---|---|---|
private-site.{siteKey} | Private — owner/operators only | NewConversation (new inbox item) |
conversation.{uuid} | Public, keyed by unguessable UUID | MessageSent, MessageEdited, MessageReacted, OperatorTyping, VisitorTyping, ReadReceiptSent, ConversationStatusChanged, AiMessageChunk |
private-operator.{operatorId} | Private — that operator only | Reserved for future per-operator notifications; no event broadcasts on it yet |
type: "note") are never broadcast on conversation.{uuid} — fetch them via the authenticated messages endpoint only.Errors & Rate Limits
| Status | Meaning |
|---|---|
401 | Missing, invalid, or revoked bearer token |
403 | Authenticated, but not the owner/operator of this site/conversation/message — or the account is banned |
404 | Record not found, or doesn't belong to the given parent (e.g. a message from a different conversation) |
422 | Validation error — see the errors object for per-field messages |
429 | Rate 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