API v1
Base URL: https://onix.sassly.ai/api/v1. Every path on this page is relative to the base URL. Request bodies are JSON
(Content-Type: application/json). The full OpenAPI 3.1 spec is at /docs/openapi.yaml
— import it into Postman, Insomnia, or an SDK generator.
Authentication
| Onix app (browser) | Integrations (your server) | |
|---|---|---|
| Identity | Session cookie after signing in with Google | Header Authorization: Bearer onx_live_… |
| Requests that change data | Require the session's X-CSRF-Token header; the app sends it automatically | No CSRF; the key needs the write scope |
| Permissions | Follow the user's role and channel access | The whole workspace, limited by the key's scope |
| Plans | All plans | Pro and Custom |
Creating an API key
- Open Settings → API (requires the Manage API keys permission; Pro only).
- Click Create key, give it a name (e.g. "CRM integration"), and choose a scope:
read(read-only) orwrite(read and change data). - Keys start with
onx_live_and are shown only once. Keep them in a secret manager, never in frontend code or a repository. - Up to 10 active keys per workspace. Requests with a revoked key get
401.
export ONIX_API_KEY="onx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
An API key acts on behalf of the workspace and isn't limited by roles, so treat it like a password. Management endpoints (members, roles, settings, API keys, billing) are app-only. The API doesn't support CORS: call it from your server, not from browser JavaScript.
Response format
Successful responses are always {"data": …, "meta": {…}}; meta appears only when relevant. Errors always look like:
{
"error": {
"code": "validation_error",
"message": "Workspace name must be 2–120 characters.",
"fields": {"name": "Workspace name must be 2–120 characters."}
}
}
codeis stable and safe to use in program logic;messageis human-readable text that follows the language (headerX-Locale: en|id).fieldsappears only on input errors, with a message per field.- Times are always UTC in ISO 8601, e.g.
2026-10-08T02:30:00Z. Convert them to your own time zone for display; the Onix app uses the user's personal time zone, or the workspace time zone. - Paginated lists accept
page(from 1) andper_page(max 100, default 25 unless stated otherwise) and returnmeta.page,meta.per_page,meta.total, andmeta.pages.
Rate limits
- 120 requests per minute per API key.
- Responses include the
X-RateLimit-LimitandX-RateLimit-Remainingheaders. - Going over the limit returns
429 rate_limitedwith aRetry-Afterheader (seconds until the next minute).
Error codes
| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized | The API key is missing, malformed, or revoked; or the app session has ended. |
| 402 | plan_required | The workspace isn't on an active Pro or Custom plan (API keys and Pro-only features). |
| 403 | forbidden | Missing permission, an app-only endpoint called with an API key, or a read key trying to change data. |
| 404 | not_found | The endpoint or data doesn't exist, including data that belongs to another workspace. |
| 405 | method_not_allowed | The method isn't supported by this endpoint; see the Allow header. |
| 409 | conflict / workspace_required | The data conflicts with the current state (e.g. a role still in use), or the user has no workspace yet. |
| 419 | csrf_mismatch | An app request other than GET without a valid X-CSRF-Token. Reload the page and try again. |
| 422 | validation_error | Invalid input; per-field details are in error.fields. |
| 423 | workspace_frozen | The workspace is frozen because its Custom contract ended. Only Plan & Billing, ownership transfer, notifications, and basic read data keep working. |
| 429 | rate_limited | Too many requests; wait for Retry-After. |
| 502 | ai_error | The AI provider is having trouble. Your AI quota isn't used; try again shortly. |
| 503 | ai_unavailable | AI features aren't set up on the Onix server yet. Contact the Onix admin. |
| 500 | server_error | A server problem. Safe to retry a little later. |
Endpoints
The Permission column shows what an app user needs (set through roles); "—" means every workspace member. API keys are treated as holding every workspace permission and see all data.
Data limits (teams): permissions control menus & actions, while the conversations & tickets a user sees follow teams. Unassigned conversations are visible to everyone with interactions.view; conversations assigned to a Frontline team are visible only to that team's members; conversations assigned to a person are visible only to that person. Tickets without an assignee are visible to the members of their Back Office team; tickets with an assignee are visible only to participants (assignees, creator, people @mentioned). Team-only knowledge base articles are visible only to members of the chosen teams (no team = all teams). Pipeline leads without an owner are visible to the lead's team; leads with an owner only to that owner. The Owner, admin roles, and members with "Can view all data" see everything (channel access still applies). Data a user may not see returns 404.
Available with an API key
| Endpoint | Permission | Description |
|---|---|---|
GET /me | — | User, active workspace, role, permissions, and effective time zone. With an API key: the user is the key's creator and via is "key". |
GET /workspace | — | Active workspace: name, plan, paid-until date, time zone, and plan limits. |
GET /dashboard | dashboard.view | Getting-started steps, quota usage, 14-day activity, recent activity, and a team summary (some parts need extra permissions). |
GET /channels | — | Channels the user can access (type whatsapp/email/livechat/api, status, settings, icon_url, hours = own business hours or null, auto_reply; live chat also includes the public key & embed code, API the webhook URL — never keys or secrets), plus the channel quota (shared by all types). |
GET /channels/{id}/icon | — | The channel's uploaded icon/logo (channel managers or members with access to that channel); 404 when it uses the default icon. |
GET /interactions | interactions.view | Inbox: conversations by kind (chat/group) and tab (new = not replied to at all, waiting = waiting reply, unreplied = both, replied, solved, closed). Each item has a reply_state. Filters channel_ids (one or more channel ids separated by commas, e.g. 1,3; the old channel_id is still accepted), assignee (me/none/id), team (mine/none/id), label_id, contact_id, q, created_from/created_to (date received, YYYY-MM-DD in the caller's time zone), category (id = that category and all its subcategories, or none). meta.counts = counts per tab; meta.teams = Frontline teams for the filter. On Pro/Custom each item has sla (reply target in business hours: level warn/breach, minutes, due time). Only conversations the caller may see (team data limits). |
GET /interactions/{id} | interactions.view | One conversation: contact/group, channel, status, team, assignee, WhatsApp labels, response times, custom_fields (custom interaction fields with their values), notes_count, tickets (ALL tickets from this conversation — even without ticket access — each with can_open, update_request, sla, and the thread with the ticket team), and for holders of contacts.view the contact's details & custom fields (contact.details, contact.custom_fields). |
POST /interactions/{id}/tickets/{ticket}/messages | interactions.reply | From the conversation to one of its tickets (no ticket access needed): {"text": "…"} sends a message into the ticket discussion; {"request_update": true} (text optional) asks the ticket team for an update — the ticket shows update_request until the team sends feedback/a conclusion or resolves it. Active tickets only (422), at most once every 15 minutes per ticket (429). Returns {"message", "tickets"}. |
GET /interactions/{id}/notes | interactions.view | The conversation's internal notes (newest first, up to 200), including ticket feedback & conclusions (meta.source). Write a note with POST /interactions/{id}/messages and type = note. |
GET /interactions/{id}/messages | interactions.view | Messages oldest → newest. Cursors before/after (message id), limit up to 100. |
POST /interactions/{id}/messages | interactions.reply | Send a message (write scope): {"text": "…"}, or multipart/form-data with file. Other types: note, sticker, location, contact; quote with reply_to_id; templates with template_id. Email conversations: html (minimal formatting: bold, italic, underline, lists, links; text is generated), cc (comma-separated, up to 10), reply_all, and several attachments files[] (multipart, up to 10 files, 20 MB in total); the recipient, "Re: …" subject, quoted previous email, and signature are filled in automatically. On a WhatsApp number with agent initials on, the sender's initials are added on the last line (not via API keys). Live chat & channel API conversations: text, one attachment, or a note (no quotes, stickers, locations, or contacts); replying to a live chat nobody has picked up also picks it up (not via API keys). |
PATCH /interactions/{id} | interactions.reply | Change status (open/solved/closed), team_id (a Frontline team), assignee_id, category_id (conversation category, null = none), and/or custom (custom interaction fields {key: value}; an empty value clears it). Moving teams or assigning others requires interactions.assign; the person must belong to the conversation's team (admins may have no team). Returns {"id", "visible": false} when the caller can no longer see it. |
PATCH /interactions/{id}/messages/{message} | interactions.reply | Edit your own text message (within 15 minutes of sending). |
DELETE /interactions/{id}/messages/{message} | interactions.reply | Unsend a message (delete for everyone), within 2 days. |
POST /interactions/{id}/messages/{message}/retry | interactions.reply | Resend a failed message. |
POST /interactions/{id}/messages/{message}/reaction | interactions.reply | Emoji reaction: {"emoji": "👍"}; empty = remove. |
GET /interactions/{id}/assignees | interactions.reply | Members who can be assigned to this conversation (with team_ids) and the Frontline teams with their members in meta.teams. |
GET /messages/{id}/media | interactions.view | A message's media file (private). Supports Range; ?download=1 to download. |
GET /messages/{id}/email | interactions.view | The original email (sanitized HTML without scripts) for display in a safe frame. Images from the internet are blocked; ?images=1 loads them. |
GET /messages/{id}/attachments/{index} | interactions.view | Email attachment number index (from 0, in the order of the message's email.attachments). ?download=1 to download. |
GET /contacts/{id}/avatar | interactions.view | The contact's WhatsApp profile photo. |
GET /groups/{id}/avatar | interactions.view | The WhatsApp group photo. |
POST /interactions | interactions.reply | Start a chat with a contact (write scope): {"contact_id", "channel_id", "text"}. An open conversation with that contact on the same number is reused. Email channels: subject is required, body in html or text, optional cc; always starts a new conversation to the contact's email. |
POST /interactions/{id}/feedback-seen | interactions.view | Mark the ticket feedback/conclusion in a conversation as read. |
GET /groups/{id} | interactions.view | Group page: info, members (matched to contacts), and the group's conversations. |
GET /tickets | tickets.view | Tickets the caller may see (team data limits). Filters status (active, open, …), priority, assignee (me/none/id), team (mine/id), q, contact_id, interaction_id, overdue. meta.counts = counts per status, meta.teams = Back Office teams for the filter. |
POST /tickets | tickets.create | Create a ticket (write scope): subject, team_id (a Back Office team, required), assignee_ids (optional, members of that team), description, priority, due_date, interaction_id or contact_id, and custom (custom ticket fields {key: value}). The monthly ticket quota applies (402). |
GET /tickets/candidates | tickets.create | Members who can be assigned or mentioned on tickets, with their Back Office team_ids & is_admin. |
GET /tickets/{id} | tickets.view | Ticket details, team, participants, the caller's permissions, custom_fields (custom ticket fields with their values), and a preview of the original conversation. |
PATCH /tickets/{id} | tickets.manage | Change status, priority, due_date, subject, description, team_id (moving teams releases assignees who are not in the new team), or custom (custom ticket fields; changed fields are logged). |
POST /tickets/{id}/assignees | tickets.manage | Add an assignee (a member of the ticket's team, or an admin): {"user_id": 5}. |
DELETE /tickets/{id}/participants/{user} | tickets.manage | Remove a participant (followers may unfollow themselves). Without an assignee, the ticket is visible to the whole team again. |
GET /tickets/{id}/messages | tickets.view | Ticket discussion + log in meta.events; after_message/after_event for new items only. |
POST /tickets/{id}/messages | tickets.view | Post a discussion message: {"body": "…", "mention_ids": [5]}, or multipart with files[] (max. 5). @Name in the text is recognized too; people mentioned can see the ticket. |
POST /tickets/{id}/messages/{message}/feedback | tickets.view | Send a discussion message as feedback to the original conversation (internal note). |
POST /tickets/{id}/conclusion | tickets.view | Conclusion for the agent: {"conclusion": "…", "resolve": true} (resolve needs tickets.manage). |
GET /ticket-messages/{id}/files/{index} | tickets.view | Discussion attachment (private). ?download=1 to download. |
GET /teams | — | Frontline & Back Office teams with their members; ?type=frontline|backoffice. |
GET /teams/{id} | — | A single team. |
GET /contacts | contacts.view | Contacts. Filters q, label_id, wa_label_id, owner (me/none/id), channel_id, sort (recent/name/created). |
POST /contacts | contacts.manage | Add a contact: name, phone, email, company, job_title, address, city, language, owner_id, label_ids, custom. |
GET /contacts/{id} | contacts.view | Full profile: identities, Onix & WhatsApp labels, custom fields, statistics & 12-month activity. |
PATCH /contacts/{id} | contacts.manage | Update a contact (only the fields sent). label_ids replaces all labels. |
POST /contacts/{id}/labels | contacts.manage | Add an Onix label: {"label_id": 3} (also allowed with interactions.reply). |
DELETE /contacts/{id}/labels/{label} | contacts.manage | Remove an Onix label. |
GET /contacts/{id}/timeline | contacts.view | Contact timeline, newest first (before = last id). |
GET /contacts/{id}/tickets | contacts.view | The contact's tickets with their status log (only those the caller can see). |
GET /contacts/{id}/notes | contacts.view | Team notes. |
POST /contacts/{id}/notes | contacts.view | Add a note: {"body": "…"}. |
PATCH /contacts/{id}/notes/{note} | contacts.view | Edit a note (author only). |
DELETE /contacts/{id}/notes/{note} | contacts.view | Delete a note (the author or contacts.manage). |
GET /labels | contacts.view | Onix labels with their contact counts; meta.colors = available colors. |
GET /custom-fields | contacts.view / interactions.view / tickets.view / leads.view | Custom field definitions per entity: ?entity=contact|interaction|ticket|lead|client (no entity = all). meta.max_per_entity = 30. |
GET /contact-fields | contacts.view | Custom contact field definitions (same as /custom-fields?entity=contact). |
GET /wa-label-rules | contacts.manage | WhatsApp label automation rules (Pro): {"label_name", "channel", "action", "value", "value_label", "active"}. Also allowed with channels.manage. |
GET /conversation-categories | interactions.view | Conversation categories in tree order (parent, then its subcategories): {"id", "name", "color", "position", "parent_id", "depth", "path_text", "children_count"}. Also allowed with contacts.view. |
GET /kb/categories | kb.view | Knowledge base categories (by position) with the number of published articles the caller may see. |
POST /kb/categories | kb.manage | Create a category: {"name", "description", "position"}. Names are unique per workspace. |
PATCH /kb/categories/{id} | kb.manage | Update a category (fields you don't send stay the same). |
DELETE /kb/categories/{id} | kb.manage | Delete a category; its articles become uncategorized. |
GET /kb/articles | kb.view | Paginated articles. Filters q (every word must match), category_id (id or none), visibility, status, team_id (team id = that team's team-only articles, none = articles for all teams). Without kb.manage only published articles are returned. Team-only articles are visible only to that team's members (the Owner, admins, "Can view all data", and API keys see everything). Each article has teams (empty = all teams). meta.counts = published & draft counts. |
POST /kb/articles | kb.manage | Create an article: {"title", "body", "category_id", "visibility", "status", "tags", "team_ids"}. Defaults to Internal + Draft, for all teams. The body is simple Markdown. team_ids = teams who can read it; managers who can't view all data may only choose their own teams (422). |
GET /kb/articles/{id} | kb.view | One full article with its body, teams, semantic search processing status (embedding_status, chunk_count, embedded_at_text), and can.insert (published Public articles can be inserted into replies). Another team's team-only article → 404. |
PATCH /kb/articles/{id} | kb.manage | Update an article (fields you don't send stay the same; team_ids: [] = all teams). Published + title/body changed → processed again for semantic search. |
DELETE /kb/articles/{id} | kb.manage | Delete an article. |
GET /kb/search | kb.view | Search published articles. mode=keyword (default, all plans) or mode=semantic (Pro/Custom, ai.use, 1 AI request per search). Parameters q, visibility, limit (1–20). Results: {"article", "match", "score", "snippet"}; meta.ai = AI usage this month. |
GET /reports/summary | reports.view | Summary cards for the range from–to (YYYY-MM-DD in the caller's time zone, inclusive; default the last 30 days, at most 366 days) and optional channel_id, team_id filters: incoming conversations, messages in/out, first response (average, median, % within target), resolution time, unreplied now, tickets. Formulas: Report formulas. |
GET /reports/daily | reports.view | One row per date in the range: {"date", "conversations", "messages_in", "messages_out"} (0 when empty). Same parameters as the summary. |
GET /reports/hourly | reports.view | 24 rows of messages in per hour 0–23 (caller's time zone): {"hour", "messages_in"}. |
GET /reports/breakdown | reports.view | type = channel, label, category, or team: one row per group with incoming conversations (by number also messages & average first response; by team also tickets created/resolved; by category in tree order, with conversations including subcategories and conversations_direct). |
GET /kpi | reports.view | Per-person KPI cards (Pro/Custom; Free → 402): frontline (first response, follow-up replies, resolution, solved per business day, recontacts, escalations, past target now) and backoffice (ticket response, on-time resolution by priority, resolved per business day, reopened, feedback, backlog). Times in business minutes/hours; from/to (workspace time zone, up to 93 days), team_id, channel_type (whatsapp/email/livechat/api: only conversations & tickets from that channel type, with that type's targets — email, live chat, and API have their own). See KPIs & SLA. |
GET /kpi/users/{id} | — | One person's KPIs with daily, breaches, and targets (with source). Yourself always; others need reports.view. |
GET /kpi/me | — | Your own KPIs today & over the last 7 days (the Dashboard "My KPIs" card). |
GET /kpi/config | settings.workspace | Workspace targets, team & personal targets, the metric list (including the email targets email_first_response & email_reply in hours, live chat livechat_first_response & livechat_reply and channel API api_first_response & api_reply in minutes), and the workspace business hours. |
GET /reports/agents | reports.view | Productivity of each active member: {"user", "replies", "conversations", "first_responses", "avg_first_response_minutes", "tickets_resolved"}. |
POST /interactions/{id}/summary | ai.use | AI summary of a conversation (Pro/Custom, 1 AI request): 3–6 bullet points, saved and shown in GET /interactions/{id} → ai_summary. |
GET /templates | interactions.reply | Reply templates. With interaction_id, each template includes rendered (variables filled in). Filters q, category. |
GET /templates/{id} | interactions.reply | One template. |
GET /templates/{id}/attachment | interactions.reply | The template attachment. |
GET /settings | settings.workspace | General settings (workspace time zone, response target, business hours business_hours + open_now, …) with choices in meta.choices. |
GET /members | settings.members | Every member, including the Owner. |
GET /invitations | settings.members | Invitations not yet accepted, including expired ones. |
GET /roles | settings.roles | Every role with its permissions and usage counts (or settings.members). |
GET /roles/{id} | settings.roles | A single role. |
GET /permissions | settings.roles | Permission catalog by menu group; meta.grantable = what this user may grant. |
GET /audit-logs | settings.audit | Audit log, newest first. Filters: category, user, from, to (YYYY-MM-DD, reader's zone), q. 50 per page by default. |
GET /pipelines | leads.view / leads.manage | Visible pipeline workflows (admins, "can view all data", API keys, and pipeline.manage: all; otherwise your teams' workflows): stages (kind new/progress/won/lost, label, color, probability) and the Back Office teams that handle them. meta = lost reasons, expense categories, color palette. |
GET /pipelines/{id} | leads.view / leads.manage | One workflow (holders of pipeline.manage also get the lead count per stage). |
GET /pipeline/settings | leads.view / leads.manage | Lost reasons, expense categories, stage color palette, and the In-progress stage limit. |
GET /pipeline/dashboard | leads.view / leads.manage | Pipeline dashboard: tiles (open, forecast, new in 7 days, won/lost, win rate, average days to close, overdue follow-ups), funnel, weekly (8 weeks), by_assignee, expenses, today's agenda, quota. Filters pipeline_id, from/to (current month by default), team_id, assignee_id. Only leads you can see. |
GET /leads | leads.view / leads.manage | List leads (data ownership: leads with an owner are only for that owner, without an owner for the lead's team; admins/API keys see all). Filters pipeline_id, stage_id, status (open/won/lost), team_id, assignee (me/none/id), client_id, contact_id, q (LEAD-…, title, contact, client), followup (overdue/today/upcoming/none), created_from/created_to; sort & dir. meta.totals = count, prospect value, deal value. |
POST /leads | leads.manage | Create a lead (create_lead; source api): pipeline_id, title, a contact (contact_id, or contact {name, phone, email} matched by phone then email, or interaction_id), client_id/client_name, stage_id, team_id, assignee_id, value_estimate, budget, expected_close, followup_at, note, custom. The monthly lead quota applies (402). |
GET /leads/board | leads.view / leads.manage | Kanban board for one workflow (pipeline_id required): stages + cards for open leads & this month's Won/Lost (max 100 per stage, more), count/value per stage, and a summary. |
GET /leads/candidates | leads.view / leads.manage | Members who can own leads, with Back Office team_ids & is_admin. |
GET /leads/duplicates | leads.manage | Open leads of a contact in the same workflow (contact_id, pipeline_id) — a warning before creating a lead. |
GET /leads/{id} | leads.view / leads.manage | Lead details: workflow & stage, contact, client, source conversation, the 30 latest activities, checklist, events, files, expenses, custom fields, and can. |
PATCH /leads/{id} | leads.manage | Change title, client_id/client_name, value_estimate, value_won (Won leads), budget, expected_close, followup_at/followup_note (null = clear), team_id, assignee_id, lost_reason/lost_note (Lost leads), custom. Returns {"id", "visible": false} when you can no longer see it. |
POST /leads/{id}/move | leads.manage | Move stage: {"stage_id"}; to Won requires value_won (defaults to the prospect value), to Lost requires a lost_reason from the list (+ lost_note). From Won/Lost back to a New/In-progress stage = reopen. |
GET /leads/{id}/activities | leads.view / leads.manage | Paged lead timeline (before = last id, kind=note for notes only). |
POST /leads/{id}/activities | leads.manage | Log an activity: {"kind": "call|chat|email|meeting|visit|note|other", "body", "happened_at", "duration_minutes"}. @Name sends a notification. |
GET /lead-files/{id} | leads.view / leads.manage | Lead file (private). ?download=1 to download. |
GET /lead-expenses/{id}/receipt | leads.view / leads.manage | Expense receipt (private). ?download=1 to download. |
GET /interactions/{id}/leads | interactions.view | The conversation's Leads tab: ALL leads from this conversation (brief, can_open) + other open leads of its contact that you can see. |
GET /contacts/{id}/leads | contacts.view | The contact's leads you can see (empty without Pipeline access). |
GET /clients | contacts.view / leads.view | Client master list (not limited by team): q, industry; each client with contacts_count and a summary of the leads you can see. meta.industries for filters. |
GET /clients/search | contacts.view / leads.view | Search clients for form pickers: ?q=. |
POST /clients | clients.manage | Add a client: {"name", "industry", "phone", "email", "website", "address", "city", "notes", "custom"}. Names are unique per workspace. |
GET /clients/{id} | contacts.view / leads.view | Client profile, linked contacts, the leads you can see, and a deal summary. |
PATCH /clients/{id} | clients.manage | Edit a client. |
GET /segments | broadcasts.view / broadcasts.manage | Contact segments: rules, summary, last_count, and how many broadcasts use them. |
GET /segments/fields | broadcasts.view / broadcasts.manage | Field, operator, & value catalog for segment rules (including custom fields cf:{key}). |
POST /segments/preview | broadcasts.view / broadcasts.manage | Preview rules without saving: {"match": "all|any", "rules": [{"field", "op", "value", "value2"}]} → count, with phone, unsubscribed, samples. |
GET /segments/{id} | broadcasts.view / broadcasts.manage | Segment details + recalculated members (preview). |
GET /broadcasts | broadcasts.view / broadcasts.manage | Broadcast list (status, channel_id, q) with per-status counts (stats). |
GET /broadcasts/options | broadcasts.view / broadcasts.manage | Numbers (broadcast permission), segments, default settings & safety limits, quota, message variables. |
GET /broadcasts/dashboard | broadcasts.view / broadcasts.manage | Figures for a period (from, to, channel_id; at most 92 days): funnel, daily, hourly, reasons, number usage today, latest & best broadcasts. |
GET /broadcasts/{id} | broadcasts.view / broadcasts.manage | Broadcast details: audience, messages, attachment, settings, counts, skip reasons, risk acknowledgement, wait, can. |
GET /broadcasts/{id}/recipients | broadcasts.view / broadcasts.manage | Recipients & per-recipient status (cumulative status, q), including the message sent. |
GET /broadcasts/{id}/export | broadcasts.view / broadcasts.manage | Per-recipient report ?format=xlsx|csv. |
GET /broadcasts/{id}/media | broadcasts.view / broadcasts.manage | The broadcast attachment (private). |
POST /broadcasts/{id}/estimate | broadcasts.view / broadcasts.manage | Estimated recipients (skip reasons), quota, finish time, and risk level. |
POST /broadcasts/{id}/preview | broadcasts.view / broadcasts.manage | Final text for up to 5 sample contacts (variant, optional text/footer). |
Onix app only
These endpoints accept only an app session (cookie + X-CSRF-Token). With an API key they return 403 forbidden.
| Endpoint | Permission | Description |
|---|---|---|
POST /wa-label-rules | contacts.manage | Create a WhatsApp label rule (Pro): action = contact_label (value = contact label id), assign_team (Frontline team id), or solve. Free → 402. |
PATCH /wa-label-rules/{id} | contacts.manage | Update a rule (including active). |
DELETE /wa-label-rules/{id} | contacts.manage | Delete a rule. |
POST /conversation-categories | contacts.manage | Create a conversation category: {"name", "color", "parent_id"} — up to 3 levels and 200 categories; names unique within the same parent. |
PATCH /conversation-categories/{id} | contacts.manage | Change a category's name, color, order, or parent (parent_id, null = top level; not into itself or its subcategories, at most 3 levels). |
DELETE /conversation-categories/{id} | contacts.manage | Delete a category; its subcategories and conversations move to its parent (uncategorized for a top-level category). |
PUT /kpi/config | settings.workspace | Save workspace targets (targets) and/or team & personal targets (overrides, replaces all). Business hours now live in PATCH /settings (business_hours is still accepted here). |
GET /kpi/export | reports.view | KPI CSV: type = summary, conversations, replies, or tickets; user_id for one person (allowed for yourself without reports.view); channel_type as above. Raw data has a channel_type column at the end. |
POST /kb/articles/{id}/embed | kb.manage | (Re)process a published article for semantic search (Pro/Custom) — the Process now / Reprocess / Try again buttons. |
GET /reports/export | reports.view | Download CSV (UTF-8 + BOM): type = summary, daily, channel, label, category, agents, or tickets, with the same range & filters as the JSON reports. |
PATCH /me | — | Personal language & time zone: {"locale": "en", "timezone": "Asia/Makassar"} ("" = follow the workspace). |
POST /me/preferences | — | Personal preferences, such as tour and sidebar state, and livechat_alert (0/1: sound & blinking tab title when a new live chat is waiting). |
GET /me/invitations | — | Valid invitations for this account's email. |
POST /me/invitations/{id}/accept | — | Accept an invitation; that workspace becomes the active one. |
POST /me/workspace | — | Switch the active workspace: {"workspace_id": 3}. |
GET /workspaces | — | Workspaces where the user is an active member. |
POST /workspaces | — | Create a workspace: {"name": "…", "timezone": "Asia/Jakarta"}. Free/Pro accounts own one workspace; Custom customers as many as their contract allows (409). meta.create on GET /workspaces says whether it is allowed. |
GET /workspace/transfer | — | Ownership transfer (Owner only): the pending request + possible recipients and their eligibility. |
POST /workspace/transfer | — | Request an ownership transfer: to_user_id, confirm_name (the workspace name), stay, stay_role_id, stay_team_ids. |
DELETE /workspace/transfer | — | Cancel the pending request. |
GET /me/transfers | — | Ownership transfer requests waiting for my answer. |
GET /me/transfers/{id} | — | Request details (can_accept, becomes_free). |
POST /me/transfers/{id}/accept | — | Accept: I become the Owner; that workspace becomes my active workspace. |
POST /me/transfers/{id}/decline | — | Decline the request. |
PATCH /workspace | settings.workspace | Rename the workspace. |
POST /workspace/leave | — | Leave the active workspace (not the Owner). |
PATCH /settings | settings.workspace | Update some General settings, e.g. {"timezone": "Asia/Jakarta"} or the workspace business hours (all plans) {"business_hours": {"enabled": true, "days": [{"day": 1, "open": true, "from": "08:00", "to": "17:00"}, …]}} — day 1 = Monday. |
GET /notifications | — | The 20 latest notifications and the unread count. |
POST /notifications/read | — | Mark one ({"id": 12}) or all notifications as read. |
POST /realtime/token | — | WebSocket (Centrifugo) token for live updates; {"channels": ["conv:12", "ticket:5"]} to join a conversation/ticket channel. |
POST /interactions/{id}/read | interactions.view | Mark as read; read receipts are sent to the customer. |
POST /interactions/{id}/typing | interactions.reply | Typing indicator: {"typing": true}. |
POST /interactions/{id}/pickup | interactions.reply | Pick up a live chat nobody has taken (atomic): whoever is too late gets 409 "Already picked up by {name}". The visitor sees "{name} joined the chat". |
POST /interactions/{id}/history | interactions.view | Copy older history from the phone for this conversation. |
POST /channels | channels.manage | Add a WhatsApp number: {"name": "Store CS"} (channel quota is checked). grant_selected_members: true = members with "selected channels" access get access too (also on POST /channels/email, /livechat, /api). |
GET /channels/{id} | channels.manage | Channel details. |
PATCH /channels/{id} | channels.manage | Every channel type: name, business hours hours (an object like business_hours, or null = follow the workspace hours), auto replies auto_reply (greeting_on, greeting, away_on, away). WhatsApp: groups, reject_calls, call_reply, agent_initial (add the agent's initials at the end of replies). Other email/live chat/API settings use PATCH /channels/{id}/email, /livechat, /api. |
DELETE /channels/{id} | channels.manage | Delete a channel (archived if it already has conversations). |
GET /channels/{id}/qr | channels.manage | QR code to scan (PNG data URL) + lifetime in seconds. |
POST /channels/{id}/pair | channels.manage | Pairing code: {"phone": "0812…"}. |
GET /channels/{id}/status | channels.manage | Latest number status. |
POST /channels/{id}/logout | channels.manage | Log the number out of Onix. |
POST /channels/{id}/reconnect | channels.manage | Reconnect. |
POST /channels/{id}/history | channels.manage | Import chat history: {"days": 30} (7/30/90 days). |
GET /channels/{id}/labels | channels.manage | WhatsApp Business labels (read from the phone). |
POST /channels/email/detect | channels.manage | Guess the provider from an address: {"address": "[email protected]"} → provider, suggested IMAP/SMTP servers, and a hint (e.g. needs an App Password; Microsoft 365 not supported yet). Signed-in sessions only. |
POST /channels/email/test | channels.manage | Test IMAP & SMTP sign-in without sending email: address, password, optional username, imap_host/imap_port/imap_security (ssl/starttls), smtp_host/smtp_port/smtp_security, smtp_username/smtp_password. Returns the selectable folders and the detected Sent folder. Errors are explained per field (422). |
POST /channels/email | channels.manage | Connect an email address (channel quota checked; an address can be in only one workspace → 409): the test fields above + name, folders (besides Inbox), sent_folder, read_sent, save_sent, history_days (0/3/7/30, default 7), sender_name, sender_with_agent, signature_html, ignore. The password is stored encrypted and never returned. |
PATCH /channels/{id}/email | channels.manage | Change email settings (same fields as above, except history_days). Server, username, password, or folder changes are tested again before saving. |
GET /channels/{id}/email/folders | channels.manage | Mailbox folders (signs in again) with a selected flag, plus the Sent folder. |
POST /channels/{id}/sync | channels.manage | Check the mailbox now (the pause after failed sign-ins is skipped). Returns {"sync", "channel"}. |
POST /channels/{id}/icon | channels.manage | Upload a channel icon/logo (multipart icon: square PNG/JPG/WebP, up to 512 KB). Live chat: the same logo appears in the widget header & launcher. |
DELETE /channels/{id}/icon | channels.manage | Go back to the default icon for the channel type. |
POST /channels/livechat | channels.manage | Create a live chat channel (all plans, one channel slot): {"name": "Website Chat", "title", "color", "grant_selected_members"} → a channel with livechat.key & livechat.embed_code. See Live chat. |
PATCH /channels/{id}/livechat | channels.manage | Widget settings: title, subtitle, color, font, radius, position, launcher_icon, intro, consent, start_label, attachments (off/images/files), email_fallback, allowed_domains; also name, hours, auto_reply. |
POST /channels/{id}/livechat/key | channels.manage | Replace the public key; the old embed code stops working. |
POST /channels/api | channels.manage | Create an API channel (Pro/Custom, one channel slot): {"name", "webhook_url", "events", "retries", "grant_selected_members"} → credentials.key (shown only once) & credentials.webhook_secret. See Channel API. |
GET /channels/{id}/api | channels.manage | Settings, the inbound message endpoint, the active key (prefix & last used), and the 50 latest webhook deliveries. |
PATCH /channels/{id}/api | channels.manage | webhook_url (https, public host), events (message.created always; conversation.updated optional), retries (0–5); also name, hours, auto_reply. |
POST /channels/{id}/api/key | channels.manage | Replace the channel key; the old key is rejected right away (401). |
GET /channels/{id}/api/secret | channels.manage | Show the webhook secret. |
POST /channels/{id}/api/secret | channels.manage | Replace the webhook secret. |
POST /channels/{id}/api/test-webhook | channels.manage | Send a ping event now: {"status", "http_status", "duration_ms", "error"}. |
POST /channels/{id}/api/test-message | channels.manage | Simulate an inbound message from your system (shows up in Interaction). |
POST /templates | templates.manage | Create a template: name, shortcut, category, body (JSON or multipart with file). |
PATCH /templates/{id} | templates.manage | Update a template. |
DELETE /templates/{id} | templates.manage | Delete a template. |
POST /templates/{id}/attachment | templates.manage | Set/replace the attachment (multipart file). |
DELETE /templates/{id}/attachment | templates.manage | Remove the attachment. |
GET /contacts/export | contacts.manage | Export contacts to CSV (same filters as the list). |
POST /contacts/import | contacts.manage | CSV import (multipart file): returns created, updated, skipped, errors. |
DELETE /contacts/{id} | contacts.manage | Delete a contact with its conversations, messages, media, notes, and timeline (tickets stay, unlinked). |
POST /contacts/{id}/merge | contacts.manage | Merge duplicates: {"source_id": 9} is merged into contact {id}. |
POST /labels | contacts.manage | Create a label: {"name": "VIP", "color": "orange"}. |
PATCH /labels/{id} | contacts.manage | Change a label's name, color, or order. |
DELETE /labels/{id} | contacts.manage | Delete a label (removed from all contacts). |
POST /custom-fields | contacts.manage | Create a custom contact, interaction, or ticket field: {"entity": "interaction", "label": "Invoice number", "type": "text"}. Keys are unique per entity. |
PATCH /custom-fields/{id} | contacts.manage | Change a field's name, options, or order (entity, type, & key stay the same). |
DELETE /custom-fields/{id} | contacts.manage | Delete a custom field; its values are no longer shown. |
POST /contact-fields | contacts.manage | Create a custom contact field: {"label": "Size", "type": "select", "options": ["S", "M", "L"]}. |
PATCH /contact-fields/{id} | contacts.manage | Change a field's name, options, or order. |
DELETE /contact-fields/{id} | contacts.manage | Delete a custom field. |
PATCH /members/{id} | settings.members | Change a member's role, team_ids, view_all, channel access, or status (active/disabled). Non-admin roles need at least one team. reply_initial (up to 20 characters, empty = automatic from the name, e.g. ^BS) can be set for every member, including the Owner. |
POST /invitations | settings.members | Invite a member: email, role_id, team_ids, view_all, channel_access, channel_ids. |
POST /teams | settings.members | Create a team: {"name": "Warehouse", "type": "backoffice", "member_ids": [5, 7]}. |
PATCH /teams/{id} | settings.members | Change a team's name, description, or members (member_ids = the full list). The type cannot be changed. |
DELETE /teams/{id} | settings.members | Delete a team; if it still has members or data (including team-only KB articles), ?replacement_id= (a team of the same type) is required. |
POST /invitations/{id}/resend | settings.members | Resend an invitation with a new link. |
DELETE /invitations/{id} | settings.members | Cancel an invitation. |
POST /roles | settings.roles | Create a role: name, permissions, and is_admin (Owner/admins only). |
PATCH /roles/{id} | settings.roles | Change a role's name, permissions, and is_admin flag (except Owner). |
DELETE /roles/{id} | settings.roles | Delete a role nobody uses. |
POST /roles/{id}/duplicate | settings.roles | Copy a role. |
GET /api-keys | settings.api | Active API keys, without their secret values. |
POST /api-keys | settings.api | Create a key: {"name": "…", "scope": "read"}. The full value is returned only once. |
DELETE /api-keys/{id} | settings.api | Revoke a key. |
GET /billing | settings.billing | Plan, usage, invoices, and billing profile. |
POST /billing/upgrade | settings.billing | Issue (or reuse) the upgrade invoice and create a Sassly Pay checkout. |
GET /billing/invoices/{number} | settings.billing | Invoice details with payment attempts. |
POST /billing/invoices/{number}/pay | settings.billing | Sassly Pay checkout URL (a still-valid attempt is reused). |
PATCH /billing/profile | settings.billing | Billing profile for upcoming invoices. |
POST /billing/downgrade-choice | settings.billing | Choose which channels & members stay active after moving to Free. |
GET /billing/return | settings.billing | Payment status after returning from checkout (checked with Sassly Pay on the server). |
POST /pipelines | pipeline.manage | Create a workflow: {"name", "description", "team_ids"} (Back Office teams); without stages → default stages. |
PATCH /pipelines/{id} | pipeline.manage | Change the name, description, teams, order; {"archived": false} restores it. |
PUT /pipelines/{id}/stages | pipeline.manage | Save the full stage layout: one New, 0–10 In progress, one Won, one Lost (label, color, probability %). |
DELETE /pipelines/{id} | pipeline.manage | Delete a workflow without leads; one that still has leads is archived. |
PUT /pipeline/settings | pipeline.manage | Save lost_reasons & expense_categories. |
GET /pipeline/export | reports.view | Raw data CSV: type = leads (all columns + custom fields), activities, or expenses; filters pipeline_id, status, from/to. |
GET /leads/import/template | leads.manage | Import template ?format=xlsx|csv. |
POST /leads/import/preview | leads.manage | Read an Excel/CSV file (multipart file): columns, sample values, mapping suggestions, a 1-hour token. |
POST /leads/import | leads.manage | Check (dry_run) or run the import with a column mapping (unmatched columns → new custom fields). |
POST /leads/bulk | leads.manage | Bulk actions (max 100): move, assign, followup. |
DELETE /leads/{id} | leads.manage | Delete a lead (admin, owner, or creator); quota is not returned. |
PATCH /lead-activities/{id} | leads.manage | Edit a manual activity (author or admin). |
DELETE /lead-activities/{id} | leads.manage | Delete a manual activity (author or admin). |
POST /leads/{id}/files | leads.manage | Upload a lead file (multipart file). |
DELETE /lead-files/{id} | leads.manage | Delete a file (uploader, owner, or admin). |
POST /leads/{id}/checklist | leads.manage | Add a checklist item {"text"}. |
PATCH /lead-checklist/{id} | leads.manage | Edit / tick a checklist item (text, done, position). |
DELETE /lead-checklist/{id} | leads.manage | Delete a checklist item. |
POST /leads/{id}/events | leads.manage | Add an event: {"kind", "title", "starts_at", "ends_at", "location", "notes", "remind_minutes"}. |
PATCH /lead-events/{id} | leads.manage | Edit an event (changing the start time resets the reminder). |
DELETE /lead-events/{id} | leads.manage | Delete an event. |
POST /leads/{id}/expenses | leads.manage | Record an expense (multipart): spent_on, category, amount, note, spent_by, receipt (photo/PDF). |
PATCH /lead-expenses/{id} | leads.manage | Edit an expense (recorder, payer, or admin). |
DELETE /lead-expenses/{id} | leads.manage | Delete an expense and its receipt. |
DELETE /clients/{id} | clients.manage | Delete a client; contacts & leads stay without the client link. |
POST /clients/{id}/contacts | clients.manage | Link ({"contact_id", "link": true}) or unlink a contact; the contact's leads without a client are linked too. |
POST /contacts/{id}/broadcast-opt-out | contacts.manage / broadcasts.manage | Unsubscribe ({"opt_out": true}) from or re-subscribe to broadcasts. |
POST /segments | broadcasts.manage | Create a segment: {"name", "description", "match", "rules"} (at most 15 rules, unique name). |
PATCH /segments/{id} | broadcasts.manage | Edit a segment. |
DELETE /segments/{id} | broadcasts.manage | Delete a segment (refused while a scheduled/running/paused broadcast uses it). |
GET /segments/{id}/export | broadcasts.manage | Download segment members (CSV). |
POST /broadcasts | broadcasts.manage | Create a draft (Pro/Custom): {"name", "channel_id", "audience", "messages", "settings"}. |
PATCH /broadcasts/{id} | broadcasts.manage | Edit a draft (only the parts sent). |
DELETE /broadcasts/{id} | broadcasts.manage | Delete a draft, completed, or cancelled broadcast. |
POST /broadcasts/{id}/media | broadcasts.manage | Attach a file (multipart file: image, video, document); DELETE to remove it. |
POST /broadcasts/{id}/test | broadcasts.manage | Send a test to one number {"phone", "variant"} (no quota used, limited per hour). |
POST /broadcasts/{id}/schedule | broadcasts.manage | Start now/schedule with {"risk_ack": true}; the recipient list is locked. |
POST /broadcasts/{id}/pause | broadcasts.manage | Pause. Also /resume, /cancel, /retry (resend failed), /unschedule (back to draft), /duplicate. |
Public
| Endpoint | Permission | Description |
|---|---|---|
GET /public/config | — | Plan prices & quotas, tax, and retention rules (follows X-Locale). |
GET /public/demo-livechat | — | The sample-data live chat key for the /demo/livechat page (null when there is none). |
POST /plan-requests | — | Custom plan request: name, email, message, optional company, phone, channels, agents. |
GET /invite/{token} | — | Invitation summary from the email link. |
Live chat widget (public)
Called by the live chat widget (the livechat.js loader & the Onix iframe) without signing in. {key} = the channel\'s public key. Apart from config and logo, all of them use the visitor token in Authorization: Bearer. Full details: Live chat.
| Endpoint | Access | Description |
|---|---|---|
GET /livechat/{key}/config | — | Widget look & texts, business-hours status, form token (CORS *). |
GET /livechat/{key}/logo | — | Widget logo. |
POST /livechat/{key}/visitors | — | Start a chat: name, email, phone (required), page_url, locale, form_token → visitor token. |
GET /livechat/{key}/conversation | — | The visitor's conversation + messages after ?after={id}, queue position, agent. |
POST /livechat/{key}/messages | — | Send a message: text, client_id (safe to resend), optional multipart attachment file. |
GET /livechat/{key}/files/{id} | — | Attachments in that visitor's conversation only. |
POST /livechat/{key}/realtime | — | WebSocket token for the visitor:{session} channel. |
POST /livechat/{key}/typing | — | The visitor is typing. |
POST /livechat/{key}/seen | — | The widget is open & messages up to message_id have been seen. |
Channel API (channel key)
For your system sending customer messages to Onix through an API channel, with the channel key onx_ch_… in Authorization: Bearer (not a workspace API key). Agent replies are sent to your webhook. Guide, sample payloads, and signature verification: Channel API.
| Endpoint | Access | Description |
|---|---|---|
POST /channel-api/messages | — | Inbound message: {"conversation_id", "contact": {"id", "name", "email", "phone"}, "message": {"id", "text", "attachments", "sent_at"}}. A unique message.id makes resending safe. |
POST /channel-api/messages/{id}/status | — | Mark an agent reply delivered/read. |
GET /channel-api/files/{token} | — | Agent reply attachments (signed link in the webhook, 7 days). |
Examples
Who owns this key
curl -s "https://onix.sassly.ai/api/v1/me" \
-H "Authorization: Bearer $ONIX_API_KEY"
{
"data": {
"user": {"id": 12, "name": "Budi Santoso", "email": "[email protected]", "avatar_url": null, "locale": "en", "timezone": null, "last_login_at": "2026-10-08T02:00:00Z"},
"via": "key",
"timezone": "Asia/Jakarta",
"workspace": {
"id": 3, "name": "Toko Budi", "slug": "toko-budi", "plan": "pro", "pro_until": "2026-11-08T03:15:00Z", "in_grace": false, "timezone": "Asia/Jakarta",
"limits": {"channels": 4, "users": 10, "tickets_per_month": 200, "replies_per_day": 0, "ai_per_month": 2000, "ai": true, "api": true},
"created_at": "2026-10-08T02:00:00Z"
},
"role": {"id": 1, "name": "Owner"},
"is_owner": true,
"is_platform_admin": false,
"permissions": ["dashboard.view", "interactions.view", "interactions.reply", "interactions.assign", "templates.manage", "channels.manage", "settings.members", "settings.roles", "settings.billing", "settings.audit", "settings.workspace", "settings.api"],
"channel_ids": null,
"realtime": true
}
}
In limits, 0 means unlimited. A null channel_ids means access to every channel. timezone is the effective zone (the user's personal zone, or the workspace zone).
Replying to a WhatsApp customer
Find unanswered conversations, then send a reply (requires a write key). The message goes out through the conversation's WhatsApp number and is recorded under the key's name.
curl -s "https://onix.sassly.ai/api/v1/interactions?tab=unreplied&per_page=5" \
-H "Authorization: Bearer $ONIX_API_KEY"
curl -s -X POST "https://onix.sassly.ai/api/v1/interactions/42/messages" \
-H "Authorization: Bearer $ONIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Hi, your order shipped today 🙏"}'
{
"data": {
"id": 981, "interaction_id": 42, "direction": "out", "type": "text", "body": "Hi, your order shipped today 🙏",
"media": null, "reply_to": null, "sender": {"name": "API · CRM", "user_id": 12, "jid": null},
"status": "sent", "error": null, "reactions": [], "sent_at": "2026-10-09T03:15:00Z", "created_at": "2026-10-09T03:15:00Z"
},
"meta": {"replies": {"used": 1, "limit": 0}}
}
Messages rejected by WhatsApp are kept with "status": "failed" and the reason in error; resend them via
/retry. At most 30 messages per minute per number (429 beyond that).
Attachments: send multipart/form-data with a file field (up to 16 MB).
Member audit log since the start of the month
curl -s "https://onix.sassly.ai/api/v1/audit-logs?category=anggota&from=2026-10-01&per_page=20" \
-H "Authorization: Bearer $ONIX_API_KEY"
{
"data": [
{
"id": 41, "action": "member.invited", "label": "Invited a member", "category": "anggota", "icon": "user-plus",
"target": "[email protected]", "details": "Role: Agent",
"user": {"id": 12, "name": "Budi Santoso", "avatar_url": null},
"ip": "203.0.113.10", "created_at": "2026-10-08T03:20:00Z"
}
],
"meta": {"page": 1, "per_page": 20, "total": 1, "pages": 1, "categories": [{"key": "anggota", "label": "Members & invitations"}, "…"], "users": [{"id": 12, "name": "Budi Santoso"}]}
}
JavaScript (Node 18+) and PHP
const res = await fetch("https://onix.sassly.ai/api/v1/members", {
headers: { Authorization: `Bearer ${process.env.ONIX_API_KEY}` },
});
const { data, error } = await res.json();
if (error) throw new Error(`${error.code}: ${error.message}`);
$ch = curl_init('https://onix.sassly.ai/api/v1/roles');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('ONIX_API_KEY')],
]);
$body = json_decode(curl_exec($ch), true);
Real-time events
The app receives events over WebSocket (Centrifugo) on the ws:{workspace_id} and user:{user_id} channels. Each event looks like {"event": "…", "data": {…}, "at": "…Z"}.
| Event | Channel | When |
|---|---|---|
notification.created | user:{id} | A new notification for the user. |
workspace.updated | ws:{id} | The workspace name changed. |
settings.updated | ws:{id} | General settings changed (e.g. the time zone). |
members.changed | ws:{id} | Members or invitations changed. |
teams.changed | ws:{id} | Teams or team members changed. |
billing.updated | ws:{id} | The plan changed (Pro activated, moved to Free, set by an admin). |
inbox.changed | ws:{id} | Something changed in the inbox: {"interaction_id", "channel_id", "reason"} without message content — reload the list through the API (channel access is still checked). |
channel.updated | ws:{id} | A channel's status or settings changed, including history import progress. |
labels.changed | ws:{id} | A channel's WhatsApp Business labels changed. |
message.created / message.updated | conv:{id} | A new or changed message (delivery status, reactions, edits, unsends, media downloaded). Payload = the message resource. |
interaction.updated | conv:{id} | The conversation's status, assignment, or summary changed. |
typing | conv:{id} | The customer (WhatsApp/live chat) or another agent is typing. |
visitor.online | conv:{id} | The live chat visitor has the widget open (sent by the widget about every 30 seconds). |
tickets.changed | ws:{id} | A ticket was created/changed: {"ticket_id", "reason"} without content — reload via the API (visibility is still checked). |
ticket.message / ticket.message.updated | ticket:{id} | New or changed discussion message (e.g. sent as feedback). Payload = ticket message resource. |
ticket.event | ticket:{id} | New ticket log entry (status, priority, due date, participants, feedback, conclusion). |
ticket.updated | ticket:{id} | Ticket data changed — reload its details. |
contacts.changed | ws:{id} | A contact was created/updated/merged/deleted/imported: {"contact_id", "reason"}. |
contacts.settings | ws:{id} | Onix labels or custom fields changed. |
kb.changed | ws:{id} | A knowledge base category or article changed, including finishing/failing semantic search processing: {"article_id"} or {"category_id"}. |
pipeline.changed | ws:{id} | A lead, workflow, or Pipeline setting changed: {"lead_id", "pipeline_id", "reason"} without content — reload via the API (data ownership is still checked). |
clients.changed | ws:{id} | A client was created/edited/deleted or a contact was linked: {"client_id"}. |
broadcasts.changed | ws:{id} | A broadcast was created/edited/started/paused/completed, sending progressed, or a recipient receipt/reply arrived: {"broadcast_id", "reason"} — reload via the API. |
segments.changed | ws:{id} | A segment was created/edited/deleted: {"segment_id"}. |
The conv:{id} channel is granted only if the user may view that conversation (channel access + team rules). When a conversation is assigned to another team/person, users who lose access are unsubscribed from its channel automatically.
The ticket:{id} channel is only granted to users who can open that ticket (participants, team members for tickets without an assignee, or those who see all data).
The live chat widget uses a separate visitor:{session} channel (token from POST /livechat/{key}/realtime) with the message, queue, typing, and read events — see Live chat.