API v1
Base URL: https://onix.sassly.ai/api/v1. All paths on this page are relative to the base URL. Request bodies are JSON
(Content-Type: application/json).
The full specification in OpenAPI 3.1 format: /docs/openapi.yaml — import it into
Postman, Insomnia, or an SDK generator.
Authentication
There are two ways to authenticate, depending on who's calling:
| The Onix app (browser) | Integrations (your server) | |
|---|---|---|
| Identity | Session cookie after signing in with Google | Header Authorization: Bearer onx_live_… |
| Requests that change data | X-CSRF-Token header required; the app sends it automatically | No CSRF |
| Access | Follows 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 Manage API keys access; 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. Store yours in a secret manager; never put it in frontend code or a repository. - Up to 10 active keys per workspace. Revoke keys you no longer use; 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. Right now every endpoint you
can call with an API key only reads data; the write scope is reserved for endpoints that change data (such as sending
messages or creating tickets) arriving in upcoming releases. The API doesn't support CORS: call it from your server, not from
JavaScript in a browser.
Response format
Successful responses always look like {"data": …, "meta": {…}}; meta is only included when relevant, such as pagination or quota usage. Errors always look like this:
{
"error": {
"code": "validation_error",
"message": "The workspace name must be 2–120 characters.",
"fields": {"name": "The workspace name must be 2–120 characters."}
}
}
codeis stable and safe to use in your program logic;messageis human-readable text that may change or follow the display language.fieldsonly appears on input errors and holds a message per field.- Timestamps are ISO 8601 with a time-zone offset, e.g.
2026-10-08T09:30:00+07:00. - Paginated lists accept
page(starting at 1) andper_page(max 100, default 25 unless stated otherwise), and returnmeta.page,meta.per_page,meta.total, andmeta.has_more.
Rate limits
- 120 requests per minute per API key.
- Responses to requests with a valid API key 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 expired. |
| 402 | plan_required | The workspace isn't on an active Pro or Custom plan (API keys and Pro-only features). |
| 403 | forbidden | No permission for this action, an app-only endpoint was called with an API key, or a read key tried to change data. |
| 404 | not_found | The endpoint or data doesn't exist, including data that belongs to another workspace. |
| 405 | method_not_allowed | This endpoint doesn't support the method; see the Allow header. |
| 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. |
| 429 | rate_limited | More than 120 requests per minute per key; wait as indicated by Retry-After. |
| 500 | server_error | A problem on our side. Safe to retry a little later. |
Endpoints
The Permission column shows the access an app user needs (granted through their role); "—" means any workspace member can call it. An API key counts as having every workspace permission.
Available with an API key
| Endpoint | Permission | Description |
|---|---|---|
GET /me | — | The user, active workspace, role, and permissions. With an API key: the user is the key's creator and via is "key". |
GET /workspace | — | The active workspace: name, plan, Pro end date, time zone, and plan limits. |
GET /settings | settings.workspace | The workspace's General settings (such as the time zone), with the allowed values in meta.choices. |
GET /dashboard | dashboard.view | The first steps (Get started with Onix) and plan quota usage. |
GET /channels | — | Channels the user can access, with the channel quota. WhatsApp numbers appear here once the WhatsApp channel is available. |
GET /members | settings.members or settings.roles | All members including the Owner; meta.users holds user-quota usage and the limit. |
GET /invitations | settings.members | Invitations that haven't been accepted yet, including expired ones. |
GET /roles | settings.roles or settings.members | All roles with their permissions and how many people use them. |
GET /roles/{id} | settings.roles or settings.members | A single role. |
GET /permissions | settings.roles or settings.members | The permission catalog grouped by menu; meta.grantable lists the permissions this user may grant. |
GET /audit-logs | settings.audit | The audit log, newest first. Filters: category, user (ID), from, to (YYYY-MM-DD), q. Paginated, 50 per page by default. |
App only
These endpoints only accept an app session (cookie + X-CSRF-Token). Calling them with an API key returns 403 forbidden.
| Endpoint | Permission | Description |
|---|---|---|
POST /me/preferences | — | Save personal preferences, such as tour status and the sidebar. |
GET /workspaces | — | Workspaces where the user is an active member (for the workspace switcher). |
PATCH /workspace | settings.workspace | Rename the workspace: {"name": "…"}. |
PATCH /settings | settings.workspace | Update some General settings, e.g. {"timezone": "Asia/Jakarta"}. |
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 | — | A WebSocket token for live updates in the app. |
PATCH /members/{id} | settings.members | Change a member's role, channel access, or status (active/disabled). |
POST /invitations | settings.members | Invite a member: email, role_id, channel_access, channel_ids. |
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 and permissions. |
PATCH /roles/{id} | settings.roles | Change a role's name and permissions (except Owner). |
DELETE /roles/{id} | settings.roles | Delete a role that 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. |
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"},
"workspace": {
"id": 3,
"name": "Toko Budi",
"slug": "toko-budi",
"plan": "pro",
"pro_until": "2026-11-08T10:15:00+07:00",
"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-08T09:00:00+07:00"
},
"role": null,
"is_owner": true,
"is_platform_admin": false,
"permissions": ["dashboard.view", "settings.members", "settings.roles", "settings.billing", "settings.audit", "settings.workspace", "settings.api"],
"channel_ids": null,
"preferences": [],
"realtime": true,
"via": "key"
}
}
In limits, a value of 0 means unlimited. A channel_ids value of null means access to all channels.
List members
curl -s "https://onix.sassly.ai/api/v1/members" \
-H "Authorization: Bearer $ONIX_API_KEY"
{
"data": [
{
"id": 1,
"user": {"id": 12, "name": "Budi Santoso", "email": "[email protected]", "avatar_url": null},
"role": {"id": 1, "name": "Owner"},
"status": "active",
"disabled_reason": null,
"channel_access": "all",
"channel_ids": [],
"is_owner": true,
"is_me": true,
"editable": false,
"joined_at": "2026-10-08T09:00:00+07:00"
},
{
"id": 2,
"user": {"id": 15, "name": "Rina Wulandari", "email": "[email protected]", "avatar_url": null},
"role": {"id": 3, "name": "Agent"},
"status": "active",
"disabled_reason": null,
"channel_access": "all",
"channel_ids": [],
"is_owner": false,
"is_me": false,
"editable": true,
"joined_at": "2026-10-08T10:31:00+07:00"
}
],
"meta": {"users": {"used": 2, "limit": 10}, "quota_error": null}
}
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-08T10:20:00+07:00"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 1,
"has_more": false,
"categories": [{"key": "workspace", "label": "Workspace & settings"}, {"key": "anggota", "label": "Members & invitations"}, "…"],
"users": [{"id": 12, "name": "Budi Santoso"}]
}
}
Category keys (such as anggota) are fixed identifiers; their display labels follow the language.
A full OpenAPI 3.1 reference for every endpoint, with JavaScript and PHP examples, is coming soon. Endpoints for the WhatsApp inbox, tickets, contacts, and the knowledge base will be added along with those features.