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. |
| 429 | rate_limited | Too many requests; wait for Retry-After. |
| 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.
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, plus the channel quota. |
GET /settings | settings.workspace | General settings (workspace time zone, response target, …) 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. |
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 |
|---|---|---|
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. |
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 (onboarding): {"name": "…", "timezone": "Asia/Jakarta"}. |
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"}. |
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. |
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 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). |
Public
| Endpoint | Permission | Description |
|---|---|---|
GET /public/config | — | Plan prices & quotas, tax, and retention rules (follows X-Locale). |
POST /plan-requests | — | Custom plan request: name, email, message, optional company, phone, channels, agents. |
GET /invite/{token} | — | Invitation summary from the email link. |
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", "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).
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. |
billing.updated | ws:{id} | The plan changed (Pro activated, moved to Free, set by an admin). |
Endpoints for the WhatsApp inbox, tickets, contacts, and the knowledge base arrive together with those features in later phases.