API v1

REST and JSON. The Onix app itself uses the same API, so the data you see in the app comes through these endpoints.

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)
IdentitySession cookie after signing in with GoogleHeader Authorization: Bearer onx_live_…
Requests that change dataRequire the session's X-CSRF-Token header; the app sends it automaticallyNo CSRF; the key needs the write scope
PermissionsFollow the user's role and channel accessThe whole workspace, limited by the key's scope
PlansAll plansPro and Custom

Creating an API key

  1. Open Settings → API (requires the Manage API keys permission; Pro only).
  2. Click Create key, give it a name (e.g. "CRM integration"), and choose a scope: read (read-only) or write (read and change data).
  3. Keys start with onx_live_ and are shown only once. Keep them in a secret manager, never in frontend code or a repository.
  4. 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."}
  }
}
  • code is stable and safe to use in program logic; message is human-readable text that follows the language (header X-Locale: en|id).
  • fields appears 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) and per_page (max 100, default 25 unless stated otherwise) and return meta.page, meta.per_page, meta.total, and meta.pages.

Rate limits

  • 120 requests per minute per API key.
  • Responses include the X-RateLimit-Limit and X-RateLimit-Remaining headers.
  • Going over the limit returns 429 rate_limited with a Retry-After header (seconds until the next minute).

Error codes

HTTPcodeMeaning
401unauthorizedThe API key is missing, malformed, or revoked; or the app session has ended.
402plan_requiredThe workspace isn't on an active Pro or Custom plan (API keys and Pro-only features).
403forbiddenMissing permission, an app-only endpoint called with an API key, or a read key trying to change data.
404not_foundThe endpoint or data doesn't exist, including data that belongs to another workspace.
405method_not_allowedThe method isn't supported by this endpoint; see the Allow header.
409conflict / workspace_requiredThe data conflicts with the current state (e.g. a role still in use), or the user has no workspace yet.
419csrf_mismatchAn app request other than GET without a valid X-CSRF-Token. Reload the page and try again.
422validation_errorInvalid input; per-field details are in error.fields.
429rate_limitedToo many requests; wait for Retry-After.
500server_errorA 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

EndpointPermissionDescription
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 /dashboarddashboard.viewGetting-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 /settingssettings.workspaceGeneral settings (workspace time zone, response target, …) with choices in meta.choices.
GET /memberssettings.membersEvery member, including the Owner.
GET /invitationssettings.membersInvitations not yet accepted, including expired ones.
GET /rolessettings.rolesEvery role with its permissions and usage counts (or settings.members).
GET /roles/{id}settings.rolesA single role.
GET /permissionssettings.rolesPermission catalog by menu group; meta.grantable = what this user may grant.
GET /audit-logssettings.auditAudit 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.

EndpointPermissionDescription
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 /workspacesettings.workspaceRename the workspace.
POST /workspace/leave—Leave the active workspace (not the Owner).
PATCH /settingssettings.workspaceUpdate 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.membersChange a member's role, channel access, or status (active/disabled).
POST /invitationssettings.membersInvite a member: email, role_id, channel_access, channel_ids.
POST /invitations/{id}/resendsettings.membersResend an invitation with a new link.
DELETE /invitations/{id}settings.membersCancel an invitation.
POST /rolessettings.rolesCreate a role: name and permissions.
PATCH /roles/{id}settings.rolesChange a role's name and permissions (except Owner).
DELETE /roles/{id}settings.rolesDelete a role nobody uses.
POST /roles/{id}/duplicatesettings.rolesCopy a role.
GET /api-keyssettings.apiActive API keys, without their secret values.
POST /api-keyssettings.apiCreate a key: {"name": "…", "scope": "read"}. The full value is returned only once.
DELETE /api-keys/{id}settings.apiRevoke a key.
GET /billingsettings.billingPlan, usage, invoices, and billing profile.
POST /billing/upgradesettings.billingIssue (or reuse) the upgrade invoice and create a Sassly Pay checkout.
GET /billing/invoices/{number}settings.billingInvoice details with payment attempts.
POST /billing/invoices/{number}/paysettings.billingSassly Pay checkout URL (a still-valid attempt is reused).
PATCH /billing/profilesettings.billingBilling profile for upcoming invoices.
POST /billing/downgrade-choicesettings.billingChoose which channels & members stay active after moving to Free.
GET /billing/returnsettings.billingPayment status after returning from checkout (checked with Sassly Pay on the server).

Public

EndpointPermissionDescription
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"}.

EventChannelWhen
notification.createduser:{id}A new notification for the user.
workspace.updatedws:{id}The workspace name changed.
settings.updatedws:{id}General settings changed (e.g. the time zone).
members.changedws:{id}Members or invitations changed.
billing.updatedws:{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.