Control Panel API
Complete reference for the Quazzar Control Panel REST API. All endpoints use the /api/ prefix and require JWT bearer token authentication. Every request is scoped to the authenticated user’s tenant. The base URL is your Control Panel instance (for example, https://panel.quazzar.cloud/api/).
See Authentication for details on obtaining and using tokens.
Permissions
Each route needs a permission, granted by the caller’s role in the workspace (see Team & Roles). A missing permission answers 403 {"error": "forbidden"}.
| Permission | Owner | Admin | Manager | Viewer | Teacher |
|---|---|---|---|---|---|
fleet:read | ✓ | ✓ | ✓ | ✓ | — |
fleet:write | ✓ | ✓ | ✓ | — | — |
fleet:shell | ✓ | ✓ | — | — | — |
settings:read | ✓ | ✓ | ✓ | ✓ | — |
settings:write | ✓ | ✓ | — | — | — |
users:manage | ✓ | ✓ | — | — | — |
tickets:read | ✓ | ✓ | ✓ | ✓ | ✓ |
tickets:write | ✓ | ✓ | ✓ | — | — |
Platform staff with an admin global role hold every permission, except on the routes that change the workspace itself (PUT /api/v1/workspace/settings, PUT /api/team/members/{id}/role): there only the caller’s role in this workspace counts, and staff get 403 {"code": "staff_use_admin_console"}. They make those changes from the admin console.
| Route | Needs |
|---|---|
GET /api/tickets/… (project board: projects, sprints, milestones, tickets, {id}/comments, {id}/activity) | tickets:read |
POST / PUT / DELETE /api/tickets/…, POST /api/tickets/{id}/move, POST /api/tickets/{id}/comments | tickets:write |
GET /api/compliance/templates, GET /api/compliance/reports[/{id}[/download]] | settings:read |
POST /api/compliance/reports | settings:write |
POST /api/notify, POST /api/devices/{deviceID}/push-token, POST /api/devices/{deviceID}/mute | settings:write |
GET /api/migrations, GET /api/migrations/{id} | fleet:read (the orchestrator’s artifacts only with fleet:write) |
POST /api/migrations, POST /api/migrations/{id}/advance, /progress, /fail | fleet:write |
GET /api/governance/gitops/{id}/conflicts | fleet:read |
POST /api/governance/gitops/conflicts/{id}/resolve | fleet:write |
GET /api/fleet/instances/{id}/terminal-recordings[/{recID}] | fleet:shell (owner, admin) or platform staff |
PUT /api/v1/workspace/settings | settings:write from the workspace role |
PUT /api/team/members/{id}/role | users:manage from the workspace role |
Authentication
Endpoints for user registration, login, profile management, and TOTP two-factor authentication.
| Method | Path | Description |
|---|---|---|
POST | /api/auth/register | Register a new user account. ?ref=CODE (or "ref" in the body) credits the sign-up to a partner referral code |
POST | /api/auth/login | Authenticate and receive JWT tokens |
GET | /api/auth/me | Get the current authenticated user, with the current workspace’s tenant_name and tenant_mode (also on the user object of login, refresh and switch answers) |
PUT | /api/auth/profile | Update user profile |
POST | /api/auth/change-password | Change password (old_password, new_password) |
POST | /api/auth/password | Same as change-password; also accepts current_password |
GET / PUT | /api/auth/notifications | Notification preferences (same as /api/profile/notifications) |
POST | /api/auth/refresh | Re-issue the token (within the refresh window) |
POST | /api/auth/totp/verify-login | Second factor: authenticator code (mfa_token, code) |
POST | /api/auth/webauthn/login/begin | Second factor: start a security-key assertion (mfa_token from the login answer) → {session_id, options} |
POST | /api/auth/webauthn/login/finish | Second factor: finish it (session_id, credential) → token |
POST | /api/auth/webauthn/register/begin, /register/finish | Register a security key for the signed-in user (name, credential) |
GET / PATCH / DELETE | /api/auth/webauthn/credentials[/{id}] | List, rename or remove your security keys |
POST | /api/auth/onboarding | Onboarding: save the workspace mode, finish onboarding |
POST | /api/auth/accept-invite | Accept an invitation (public) |
POST | /api/auth/totp/enable | Enable TOTP two-factor authentication |
POST | /api/auth/totp/verify | Verify a TOTP code |
POST | /api/auth/totp/verify-login | Finish a sign-in with a TOTP code (mfa_token + code) |
GET | /api/v1/release-notes | Release notes for What’s New (public; ?product=os|cc|mobile&since&limit&instance_id) |
Login, TOTP login, refresh and SSO exchange return the same body. user.id is your membership in the current workspace; user.account_id is the account shared by all your workspaces:
{
"token": "eyJhbGciOi...",
"user": {
"id": "b1f0…", "tenant_id": "7c2e…", "account_id": "0d9a…",
"email": "[email protected]", "display_name": "Ada", "role": "org_owner",
"global_role": "", "email_verified": true, "onboarding_completed": true
}
}The JWT carries uid (membership), tid (workspace), aid (account), role, and wsl: true for sessions from a workspace’s own SSO connector, which cannot switch workspace.
When the account has a second factor, POST /api/auth/login answers {"mfa_required": true, "methods", "user_id", "mfa_token"} instead of a token. Finish with POST /api/auth/totp/verify-login {"mfa_token", "code"} (or a security key). The mfa_token is required: a bare user_id + code is refused with 401 {"error":"mfa_token_required"}. That is always the case for staff accounts. For other users it can be relaxed temporarily with CC_TOTP_REQUIRE_MFA_TOKEN=false (default true).
GET /api/v1/release-notes returns published notes to everyone. Notes in canary status are added for the canary audience only: a signed-in user (bearer token) whose workspace is on the canary release channel or runs a staged OS rollout to that version, or a node that passes ?instance_id= and whose rollout stage has been dispatched.
POST /api/auth/accept-invite takes {"token", "password", "name"}. For an email that already has an account, send that account’s password (or its bearer token) instead of a new one. The response is {"accepted": true, "user_id", "tenant_id", "existing_account"}; a missing proof returns 401 {"error", "existing_account": true}. The owner invitation of a tenant created in the admin console activates the owner’s new account: the password (at least 8 characters) becomes the account password and the answer carries "activated": true.
Workspaces
One account can belong to several workspaces. See Workspaces.
| Method | Path | Description |
|---|---|---|
GET | /api/workspaces | The caller’s workspaces |
POST | /api/workspaces | Create a workspace (caller becomes owner) and switch to it |
POST | /api/workspaces/{id}/switch | Re-issue the token for another workspace |
POST | /api/workspaces/{id}/leave | Leave a workspace (409 last_owner, 409 last_workspace) |
GET / PUT | /api/v1/workspace/settings | Workspace toggles: require_2fa, record_terminal_sessions, agent_auto_update (true / false / null = platform default). PUT: owners and admins of this workspace; platform staff get 403 staff_use_admin_console |
GET /api/workspaces:
{
"current_id": "7c2e…",
"workspaces": [
{
"id": "7c2e…", "name": "Acme Corp", "plan": "business", "status": "active",
"mode": "msp", "mode_chosen": true, "role": "org_owner", "member_id": "b1f0…",
"parent_tenant_id": null, "last_active_at": "2026-09-25T08:10:00Z",
"created_at": "2026-01-02T10:00:00Z", "open_incidents": 2, "unread_notifications": 5,
"initials": "AC", "color": "#A78BFF", "current": true
}
]
}POST /api/workspaces body: {"name": "Beta Labs", "mode": "business", "as_client": false}. mode is business (default), msp, family or school. as_client: true (from an MSP workspace, owners and admins) links the new workspace as an MSP client.
Switch and create return {"token", "user", "workspace"} (the login body plus the workspace object above). The previous token can no longer be refreshed. Errors: 404 not a member, 403 suspended workspace or access disabled in that workspace, 403 {"error": "reauth_required"} for a workspace-SSO session.
The workspace mode is also at GET / PUT /api/v1/workspace/mode ({"mode", "chosen"}). Every member can read it; PUT needs settings:write (owner or admin).
POST /api/workspaces/{id}/leave returns {"status": "left", "workspace_id", "mode": "deleted" | "disabled"}; leaving the workspace the token is scoped to adds token, user and workspace for the workspace you were moved to (or "reauth_required": true).
When a workspace has require_2fa on (and at least one owner has 2FA), members without TOTP or a security key get 403 {"error": "mfa_enrollment_required"} from the workspace API (except the workspace list / switch / leave and GET /api/v1/workspace/settings); login, /api/auth/me and switch responses carry "mfa_enrollment_required": true. See Workspace Settings.
Onboarding
| Method | Path | Description |
|---|---|---|
POST | /api/auth/onboarding | {"mode": "msp", "complete": false} saves the mode; {} finishes onboarding and returns a fresh token |
POST | /api/onboarding/enroll-token | Fleet registration token + CLI command (fleet:write) |
GET | /api/onboarding/first-heartbeat | Wait for the first node (fleet:read) |
POST | /api/invitations/bulk | Invite several emails (users:manage) |
POST /api/onboarding/enroll-token body (optional): {"ttl_minutes": 60} (max 1440). Response:
{
"id": "5a1e…", "token": "Xb3…", "created_at": "…", "expires_at": "…",
"cc_url": "https://cp.quazzar.cloud",
"command": "quazzar enroll --cc https://cp.quazzar.cloud --token Xb3…"
}GET /api/onboarding/first-heartbeat?since=<RFC3339>&timeout=<seconds> waits for a node of the workspace that registered or sent a heartbeat since since (default: any node). With Accept: text/event-stream (or ?stream=1) it is a Server-Sent Events stream: : waiting comments every 15 s, then event: heartbeat with the node as data, or event: timeout (default and max wait 600 s). Otherwise it long-polls (default 25 s, max 60 s):
{ "connected": true, "instance": { "id": "…", "hostname": "node-1", "status": "online", "version": "0.11.0", "last_heartbeat": "…", "created_at": "…" } }POST /api/invitations/bulk body: {"emails": "[email protected], [email protected]", "role": "org_viewer"} (emails may also be an array; max 50). Response 201 (or 200 when nothing was sent): {"invited": [Invitation…], "skipped": [{"email", "reason"}]} with reasons invalid email, already a member, already invited.
Team
| Method | Path | Description |
|---|---|---|
GET | /api/team/members | Members with last_seen_at, current_page and presence (online, idle, offline). ?active_only=true leaves out deprovisioned (SCIM-disabled) members, the ones an incident can’t be assigned to |
PUT | /api/team/members/{id}/role | {"role": "org_admin"}; owners and admins of this workspace only (platform staff get 403 staff_use_admin_console) |
GET | /api/presence | {"users": [online…], "members": [{id, email, display_name, role, presence, last_seen_at, current_page}]} |
POST | /api/presence/heartbeat | {"page": "/fleet"}, returns 204 |
Role changes return {"id", "email", "role", "previous_role"}. Only an owner may grant or change org_owner; demoting the last owner returns 409. Changes are audited as team.role_change.
Notifications
| Method | Path | Description |
|---|---|---|
GET | /api/notifications?unread=1&limit=50&before=<RFC3339> | The caller’s feed in the current workspace |
GET | /api/notifications/count | {"unread_count": 3} |
POST | /api/notifications/{id}/read | {"id", "read": true, "unread_count"}; 404 for someone else’s id |
POST | /api/notifications/read-all | {"updated": 3, "unread_count": 0} |
{
"unread_count": 1,
"notifications": [
{
"id": "…", "kind": "node.offline", "severity": "warning",
"title": "node-1 is offline", "body": "No heartbeat for more than 10 minutes.",
"link": "/fleet/…", "read_at": null, "created_at": "…"
}
]
}Kinds: incident.opened, incident.escalated, node.offline, canary.paused, invitation.accepted, billing.payment_failed, ai.budget. Severities: info, success, warning, critical. Kept for 30 days.
Global search
GET /api/search/global?q=<text>&limit=50 returns {"results": [{source, id, title, snippet, path, icon, origin_node?}]}. Sources: workspaces (your workspaces and, in an MSP workspace, its client workspaces), nodes (hostname, location or tag match), incidents, and results federated from online nodes.
Fleet Management
Monitor and manage all Cloud OS instances connected to the Control Panel. The fleet API provides a centralized view of instance health, metrics, and remote command execution.
| Method | Path | Description |
|---|---|---|
GET | /api/fleet/instances | List all connected Cloud OS instances |
GET | /api/fleet/summary | Get fleet summary (total instances, health overview) |
GET | /api/fleet/health | Get health status for all instances |
GET | /api/fleet/metrics/{instanceId} | Get metrics for a specific instance |
GET | /api/fleet/instances/{id}/metrics/history | Get historical metric data for a specific instance |
POST | /api/fleet/instances/{id}/impersonate | Mint an impersonate token to open an OS session |
GET | /api/fleet/alerts | List active alerts across the fleet |
POST | /api/fleet/commands | Send a command to one or more instances |
POST | /api/fleet/tokens | Generate a fleet enrollment token |
GET | /api/fleet/instances?page=1&per_page=50&sort=-cpu | Paged, sorted, filtered fleet list with facets (see below) |
GET | /api/fleet/instances/page | Same paged list, always the paged envelope |
GET | /api/fleet/regions | Known regions and per-region node counts |
PUT | /api/fleet/instances/{id}/region | Pin a node’s region ({"region": "eu-west"}; "" resets to automatic) |
PUT | /api/fleet/instances/{id}/client | Link a node to an MSP client ({"client_id": "<uuid>"} or null) |
GET | /api/fleet/instances/{id}/detail | Node facts, apps and last-hour sparklines |
GET | /api/fleet/instances/{id}/logs/stream | Realtime node logs (Server-Sent Events) |
GET | /api/fleet/instances/{id}/logs/ws | Realtime node logs (WebSocket) |
POST | /api/fleet/instances/{id}/terminal/ticket | Single-use ticket for the node terminal (owner / admin) |
GET | /api/fleet/instances/{id}/terminal/ws?ticket=… | Node terminal (WebSocket, ticket-authenticated) |
GET | /api/fleet/instances/{id}/terminal-recordings | Recorded terminal sessions of a node (workspace owners, admins and platform staff; when the workspace records terminals) |
GET | /api/fleet/instances/{id}/terminal-recordings/{recID} | One recording (application/x-asciicast, asciicast v2) |
GET | /api/fleet/overview?range=24h | Dashboard aggregates |
GET | /api/fleet/heatmap?metric=cpu | Heatmap rows grouped by region (cpu, mem, disk) |
POST | /api/fleet/bulk | Start a bulk job over many nodes |
GET | /api/fleet/bulk-jobs | List bulk jobs, most recent first |
GET | /api/fleet/bulk-jobs/{id} | A bulk job with per-node results |
POST | /api/fleet/bulk-jobs/{id}/cancel | Cancel a running bulk job |
GET | /api/fleet/clusters | Clusters with node count, Raft term, health and primary |
GET | /api/fleet/clusters/{id}/topology | Cluster map: primary, nodes, links, Raft |
Node Health
Every fleet endpoint reports a node’s health as online, degraded or offline:
- offline — never connected, last heartbeat older than 10 minutes, or marked offline;
- degraded — heartbeat late (over 90 s), CPU, memory or disk at 90 % or more, the node reports itself critical, or an app container is restarting or dead;
- online — otherwise.
degraded_reasons lists the causes: heartbeat_late, cpu_high, mem_high, disk_high, agent_critical, app_failed (offline nodes: never_connected or heartbeat_missing).
Paged Fleet List
Without query parameters GET /api/fleet/instances returns the plain array of instances (unchanged). With any of the parameters below it returns a page:
| Parameter | Description |
|---|---|
page, per_page | 1-based page, page size (default 50, max 200) |
sort | host, ip, region, tenant, cpu, mem, disk, version, uptime, status, last_seen (last heartbeat; never-connected nodes last); prefix - for descending |
status | online, degraded, offline, or pending (enrolled but never connected; these are also counted as offline) |
region | a region, or unknown |
q | substring of host name, IP, tag or location (or an exact node id) |
group | fleet group id or name |
version | exact Cloud OS version |
tenant | MSP client id (or none) |
ids | comma-separated instance ids (repeatable, up to 200) — only those nodes of your workspace; ids of other workspaces are ignored |
{
"items": [
{
"id": "…", "hostname": "edge-ew2-0632", "status": "healthy", "version": "3.1.4",
"region": "eu-west", "region_source": "agent", "zone": "eu-west-3", "cloud_provider": "aws",
"ip_address": "10.2.120.15", "public_ip": "", "ip_addresses": ["10.2.120.15"],
"cpu_percent": 96, "memory_percent": 66, "disk_percent": 59, "tags": ["web"],
"msp_client_id": "…", "client_name": "Acme Hosting", "uptime_seconds": 6566400,
"health": "degraded", "degraded_reasons": ["cpu_high"], "failed_apps": 0,
"last_heartbeat": "2026-09-25T10:00:00Z"
}
],
"total": 5284, "page": 1, "per_page": 50,
"facets": {
"status": {"online": 5105, "degraded": 118, "offline": 61, "pending": 4},
"regions": {"eu-west": 900, "us-east": 884, "unknown": 3},
"versions": {"3.1.4": 5231},
"clients": {"<client id>": 1200, "none": 30}
}
}Facets count over q, group and tenant only, not over status, region or version. online + degraded + offline is the total; pending is the part of offline that never connected. clients is omitted for non-MSP fleets.
Dashboard Overview
GET /api/fleet/overview?range=24h&top=6&alerts=8 (range: 1h, 6h, 24h, 7d; cached ~20 s):
{
"range": "24h", "generated_at": "…", "healthy_pct": 96.6,
"totals": {
"total": {"value": 5284, "delta": 12}, "online": {"value": 5105, "delta": 12},
"degraded": {"value": 118, "delta": -4}, "offline": {"value": 61, "delta": 31}
},
"tenants": {"value": 41, "delta": 2, "kind": "clients"},
"incidents": {"open": 3, "delta": 1, "max_severity": "critical"},
"newest_version": {"version": "3.2.1", "count": 53, "total": 5284},
"cpu": {"bucket_seconds": 3600, "points": [{"t": "…", "p50": 41.2, "p95": 77.9, "nodes": 5190}]},
"resources": {"avg_cpu": 41.3, "avg_mem": 52.1, "avg_disk": 60.4, "nodes": 5223},
"top_cpu": [{"id": "…", "hostname": "edge-ew2-0632", "region": "eu-west", "health": "degraded", "cpu_percent": 96, "memory_percent": 66}],
"recent_alerts": [{"id": "…", "kind": "alert", "severity": "high", "title": "cpu > 90%", "rule": "cpu", "status": "open", "instance_id": "…", "hostname": "…", "at": "…"}],
"open_alerts": 3,
"regions": [{"region": "eu-west", "total": 900, "online": 870, "degraded": 20, "offline": 10}]
}Deltas compare with the hourly snapshot closest to 24 hours ago and are null until one exists. resources averages CPU, memory and disk over the nodes that are not offline (nodes of them; an average is null when no node reports it).
GET /api/fleet/groups returns each group with instance_count (nodes in the group) and online_count (of them healthy-online), so a page never needs the whole node list for counts.
Heatmap by Region
GET /api/fleet/heatmap?metric=cpu|mem|disk returns {"metric", "total", "rows": [{"region", "total", "online", "degraded", "offline", "avg", "cells": [{"id", "host", "status", "value", "tenant"}]}]}. Without metric the endpoint keeps its previous {"cells": [...]} response.
Node Detail
GET /api/fleet/instances/{id}/detail returns {"instance": <fleet row>, "facts": {...}, "apps": [...], "metrics": {"cpu": [...], "mem": [...], "disk": [...], "net_in": [...]}}. Facts include OS, kernel, CPU model and cores, RAM, disk size, boot time and uptime, agent version, IP addresses, region, zone, cloud provider, tenant, group, cluster (id, name, role, ip, term), tags, last heartbeat and its age, agent_connected, certificate expiry and license plan. Apps carry app_id, name, status, image, cpu_percent, mem_bytes and restart_count. Metric points are {"t", "v"} per minute over the last hour (net_in in bytes per second).
Realtime Node Logs
GET /api/fleet/instances/{id}/logs/stream (SSE; use fetch() so you can send the Authorization header) or /logs/ws (WebSocket). Query: source (journal or app), unit (repeatable; default quazzard.service), app, level (debug, info, warn, error), since (RFC 3339 or a duration such as 15m), lines (up to 5000, default 200), follow (default true), grep.
Messages: {"type": "ready", "command_id": "…"}, {"type": "batch", "lines": [{"ts", "level", "source", "message"}]}, then {"type": "end"} or {"type": "error", "error": "…"}. Requires the fleet write permission; returns 409 when the node’s agent connection is down.
Node Terminal
Browsers can’t send the Authorization header on a WebSocket, so the terminal uses a ticket:
POST /api/fleet/instances/{id}/terminal/ticket(bearer token; needs fleet write and the owner or admin role, not available in staff sessions) returns{"ticket": "…", "expires_in": 30, "ws_path": "/api/fleet/instances/{id}/terminal/ws"}. It answers409 {"code": "agent_offline"}when the node’s agent is not connected,403 {"code": "shell_permission"}or403 {"code": "impersonation"}when you may not open a shell, and404for a node outside your workspace.- Open
wss://…{ws_path}?ticket=<ticket>&cols=<n>&rows=<n>within 30 seconds. The ticket works once, for that node, for you.
On the socket, binary frames from the server are terminal output and binary frames you send are keystrokes. Text frames are JSON: the server sends {"type": "ready", "session_id", "idle_timeout_sec", "max_duration_sec"} when the shell starts, then {"type": "exit", "reason"} or {"type": "error", "code", "error"} before closing (code: agent_offline, shell_disabled — the node’s own policy forbids remote shells —, too_many, pty_failed, timeout). Send {"type": "resize", "cols", "rows"} to resize. Sessions end after 15 minutes idle or 2 hours; ticket issue, start and end are audited (fleet.terminal.ticket, fleet.terminal.start, fleet.terminal.end).
Bulk Jobs
curl -X POST "https://panel.quazzar.cloud/api/fleet/bulk" \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"action": "tag", "filter": {"region": "eu-west", "status": "degraded"}, "params": {"tags": ["investigate"]}}'action is restart (params.app_id), update (params.version optional), tag / untag (params.tags), move_group (params.group_id, null to clear), remove or run_command (params.type, params.payload). Target nodes with instance_ids or filter (status, region, q, group, version, tenant), up to 10,000. The response (202) is the job:
{
"id": "…", "kind": "tag", "target": "filter status=degraded region=eu-west (118 nodes)",
"params": {"tags": ["investigate"]}, "total": 118, "done": 40, "succeeded": 39, "failed": 1,
"cancelled": 0, "running": 16, "progress": 0.34, "status": "running",
"created_by": "…", "created_by_email": "[email protected]", "created_at": "…", "updated_at": "…", "finished_at": null
}status ends as succeeded, partial, failed or cancelled. GET /api/fleet/bulk-jobs/{id} returns {"job": {...}, "items": [{"instance_id", "hostname", "status", "command_id", "output", "error", "started_at", "finished_at"}]} (failed first); GET /api/fleet/bulk-jobs?kind=&status=&limit=&offset= returns {"jobs": [...]}.
Cluster Topology
GET /api/fleet/clusters/{id}/topology:
{
"cluster_id": "…", "name": "acme-core", "health": "ok",
"primary": {"id": "…", "host": "web-ew1-0001"},
"nodes": [{"key": "…", "id": "…", "host": "web-ew1-0002", "status": "online", "role": "worker", "wg_ip": "10.42.0.2", "rtt_ms": 3.4, "managed": true}],
"links": [{"from": "…", "to": "…", "state": "up", "rtt_ms": 3.4}],
"raft": {"term": 42, "leader": {"id": "…", "host": "web-ew1-0001"}, "commit_index": 90211},
"mesh_rtt_ms": 3.4, "apps": 39, "client_name": "Acme Hosting"
}Links go from the primary to each member and are up, degraded (member degraded or over 100 ms) or down. health is ok, degraded or down. Members of the OS cluster that are not registered in the Control Panel have "managed": false, "id": null and a key os:<id>.
Historical Metrics
curl "https://panel.quazzar.cloud/api/fleet/instances/inst_abc123/metrics/history?range=24h" \
-H "Authorization: Bearer <token>"Query parameters:
| Parameter | Values | Default | Description |
|---|---|---|---|
range | 1h, 6h, 24h, 7d, 30d, 3mo | 24h | Time range to query |
metric | cpu, mem, disk, net | (all) | Filter to a single metric |
The response returns time-bucketed arrays of [timestamp, value] pairs. Data source is selected automatically: raw heartbeat data for ranges ≤ 6 h, hourly rollup for ≤ 30 d, daily rollup for longer ranges.
Impersonate (Open in Node)
curl -X POST "https://panel.quazzar.cloud/api/fleet/instances/inst_abc123/impersonate" \
-H "Authorization: Bearer <token>"{
"session_url": "https://my-node.example.com/api/cp/session?token=<signed-jwt>"
}Redirect the browser to session_url. The OS validates the token and creates an authenticated session. Tokens are single-use and expire after 60 seconds.
Example: List Fleet Instances
curl https://panel.quazzar.cloud/api/fleet/instances \
-H "Authorization: Bearer <token>"[
{
"id": "inst_abc123",
"name": "production-01",
"hostname": "cloud.example.com",
"version": "0.2.9",
"status": "healthy",
"apps_count": 12,
"cpu_percent": 35.2,
"memory_percent": 62.1,
"last_seen": "2025-01-15T10:30:00Z"
},
{
"id": "inst_def456",
"name": "staging-01",
"hostname": "staging.example.com",
"version": "0.2.9",
"status": "healthy",
"apps_count": 5,
"cpu_percent": 12.8,
"memory_percent": 28.4,
"last_seen": "2025-01-15T10:29:55Z"
}
]Example: Get Fleet Summary
curl https://panel.quazzar.cloud/api/fleet/summary \
-H "Authorization: Bearer <token>"{
"total_instances": 8,
"healthy": 7,
"degraded": 1,
"offline": 0,
"total_apps": 45,
"total_cpu_cores": 32,
"total_memory_gb": 128
}Example: Send a Fleet Command
curl -X POST https://panel.quazzar.cloud/api/fleet/commands \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"instance_ids": ["inst_abc123", "inst_def456"],
"command": "update",
"params": {
"version": "0.3.0"
}
}'Example: Generate Enrollment Token
curl -X POST https://panel.quazzar.cloud/api/fleet/tokens \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "datacenter-west",
"expires_in_hours": 24
}'{
"token": "flt_abc123def456...",
"name": "datacenter-west",
"expires_at": "2025-01-16T10:30:00Z"
}Use fleet enrollment tokens to connect new Cloud OS instances to the Control Panel. Pass the token during Cloud OS initial configuration to automatically register the instance.
Node Agent Protocol (port 8443)
Cloud OS nodes talk to the Control Panel’s agent server on port 8443. Apart from registration, every call is authenticated with the node’s mTLS client certificate; you don’t call these endpoints yourself, but they explain what a node does.
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/agent/v1/register | registration token | {"registration_token", "instance_id", "hostname", "version", "csr", "hardware_fingerprint"} → {"signed_certificate", "ca_certificate", "license_token", "cert_expires_at", "cp_node_secret"}. A node that is already registered in the token’s workspace re-registers in place (same instance, tags and group kept, not counted again against the node limit); an instance id registered in another workspace gets 409. |
POST | /api/agent/v1/renew | mTLS client cert | {"csr": "<PEM>"} → {"signed_certificate", "ca_certificate", "cert_expires_at"}. Issues a new 90-day certificate for the node identified by the presented certificate (the CSR subject is ignored). 401 without a verified client certificate, 400 for an invalid CSR, 403 when the node has been deleted from the fleet. Nodes call it automatically once less than 30 days are left. |
POST | /api/agent/v1/heartbeat | mTLS client cert | Status, metrics summary and inventory; pending commands ride on the response |
Client certificates are valid for 90 days. An expired certificate is rejected during the TLS handshake, so such a node can only come back through /register with a new token — see Recovering a node whose certificate expired.
AI health
Fleet AI health: the ai_health block nodes send with their heartbeat, the derived status, and Memory MCP retrieval evals. See AI Health. Reads need fleet read; POST …/eval and PUT /settings need fleet write.
| Endpoint | Method | Description |
|---|---|---|
/api/ai-health | GET | {"summary", "nodes"}: KPI counts (healthy, degraded, failing, unconfigured, unknown, provider_probed, provider_reachable, canary_nodes, canary_failing, open_alerts, eval_nodes, last_eval_score, last_eval_at) and every node with status, reasons, snapshot, collected_at, canary_fail_streak, canary_alert, provider_alert, last_eval |
/api/ai-health/nodes/{id}/history?hours=168 | GET | Recorded snapshots (newest first, hours up to 720) and the node’s last 20 eval runs; 404 for a node outside your workspace |
/api/ai-health/evals?instance_id=&limit=50 | GET | Eval runs, optionally for one node |
/api/ai-health/nodes/{id}/eval | POST | Body {"sample_size": 30} (optional, 1–200). Queues an ai.eval.retrieval command; 202 {"run"}, 409 while an eval is pending on the node, 404 for a foreign node. Audited as ai.eval.run |
/api/ai-health/settings | GET, PUT | Standing-eval policy {"standing_enabled", "interval_hours" (1–720), "sample_size" (5–200), "min_score" (0–1, 0 = no alert)} plus standing_allowed. Turning standing_enabled on below Pro answers 402 upgrade_required with feature: "ai_standing_eval". Audited as ai.eval.settings |
An eval run has status pending, ok, empty (too few notes), unsupported (older node), error or timeout (no answer in 30 minutes), a score (mean MRR, 0–1) and the node’s result: {"variant", "sample_size", "probes": [{"name": "title" | "deep", "n", "recall": [{"k", "recall"}], "mrr"}], "score", "duration_ms"}.
GET /api/license/cc-features lists ai_standing_eval (required plan pro).
Billing
Manage subscriptions, usage, packages, and invoices for your tenant. All billing endpoints are Stripe-backed.
| Method | Path | Description |
|---|---|---|
GET | /api/billing/subscription | Get current subscription details |
PUT | /api/billing/subscription | Update subscription plan |
GET | /api/billing/licenses | List active licenses |
GET | /api/billing/usage | Get current plan usage (nodes, users, storage) |
GET | /api/billing/packages | List available plans and pricing |
GET | /api/billing/invoices | List Stripe invoices (60 s cache) |
GET | /api/license/cc-features | Plan-gated Control Center features for your workspace: {plan, features: {staged_rollouts, scim, gitops, ai_budgets: {allowed, required_plan}}}. Any signed-in member. See Control Center features by plan. |
AI usage and budgets
The fleet AI cost ledger. See AI Usage & Budgets for the fields and the budget rules.
| Method | Path | Description |
|---|---|---|
GET | /api/ai/usage/summary?month=YYYY-MM | Spend MTD (or a past month), forecast, top model, per day / node / model, budget status (billing:read, every plan) |
GET | /api/ai/usage/policy | The workspace AI budget and node overrides (billing:read) |
PUT | /api/ai/usage/policy | Set the AI budget (billing:write, Business+; 402 upgrade_required with "feature": "ai_budgets" otherwise; 422 on invalid values) |
DELETE | /api/ai/usage/policy | Remove the AI budget (billing:write, every plan) |
Example: Get Subscription
curl https://panel.quazzar.cloud/api/billing/subscription \
-H "Authorization: Bearer <token>"{
"plan": "pro",
"status": "active",
"instance_limit": 25,
"instances_used": 8,
"features": ["fleet-management", "ha-clustering", "governance", "priority-support"],
"billing_cycle": "monthly",
"next_invoice_date": "2025-02-01T00:00:00Z"
}Example: Get Usage
curl https://panel.quazzar.cloud/api/billing/usage \
-H "Authorization: Bearer <token>"{
"nodes": { "used": 8, "limit": 25 },
"users": { "used": 12, "limit": 50 },
"storage_gb": { "used": 42.3, "limit": 500 }
}Example: List Invoices
curl https://panel.quazzar.cloud/api/billing/invoices \
-H "Authorization: Bearer <token>"[
{
"id": "in_1abc...",
"date": "2026-05-01T00:00:00Z",
"period_start": "2026-04-01T00:00:00Z",
"period_end": "2026-04-30T23:59:59Z",
"amount_due": 4900,
"currency": "usd",
"status": "paid",
"pdf_url": "https://invoice.stripe.com/..."
}
]OAuth (CP as IdP)
The Control Panel acts as an OAuth 2.0 Authorization Code identity provider for connected Cloud OS nodes. These endpoints are used by the “Sign in with Quazzar Cloud” flow on the OS login page.
| Method | Path | Description |
|---|---|---|
GET | /api/oauth/authorize/preflight | Check whether the requesting node is authorized to start the flow |
POST | /api/oauth/authorize/consent | User approves the authorization; returns a redirect with a code |
POST | /api/oauth/token | Exchange an authorization code for a CP-signed identity assertion |
Flow overview
- The OS redirects the user to the CP authorization URL with
client_id,redirect_uri,state, andcode_challenge. - The CP calls
GET /api/oauth/authorize/preflightto verify the node is registered and theredirect_urimatches. - The user approves on the CP consent screen (
POST /api/oauth/authorize/consent). - The CP redirects to the OS
redirect_uriwith a single-use, time-limitedcodeand the originalstate. - The OS calls
POST /api/oauth/tokento exchange the code for an identity token, then creates a local session.
Codes are stored in oauth_codes (migration 073), are single-use, and expire after 5 minutes.
SSO sign-in and SCIM
The Control Center’s own SSO sign-in (/api/auth/sso/*), sign-in domain verification (/api/sso/login-domains), the Settings → SSO summary (/api/sso/summary), the SCIM admin API (/api/sso/scim/*) and the SCIM 2.0 server (/scim/v2, authenticated with a per-workspace SCIM bearer token) are documented in SSO & SCIM.
Governance
Manage compliance policies, canary deployments, GitOps configurations, and governance scores across your fleet.
| Method | Path | Description |
|---|---|---|
GET | /api/governance/policies | List governance policies |
POST | /api/governance/policies | Create a governance policy |
PUT | /api/governance/policies/{id} | Update a governance policy |
DELETE | /api/governance/policies/{id} | Delete a governance policy |
GET | /api/governance/canary | List canary deployments |
POST | /api/governance/canary | Create a canary deployment |
PUT | /api/governance/canary/{id} | Update a canary deployment |
GET | /api/governance/gitops | List GitOps configurations |
POST | /api/governance/gitops | Create a GitOps configuration |
PUT | /api/governance/gitops/{id} | Update a GitOps configuration |
GET | /api/governance/gitops/{id}/conflicts | List a configuration’s conflicts (fleet read) |
POST | /api/governance/gitops/conflicts/{id}/resolve | Resolve a conflict (fleet write) |
GET | /api/governance/scores | Get governance scores across the fleet |
GitOps endpoints need the Enterprise plan; staged rollouts and canaries (/api/fleet/rollouts, /api/fleet/canary) need Business or above. Below that, the calls that start something return 402 upgrade_required (see Staged Rollouts and GitOps Drift).
Example: Create a Governance Policy
curl -X POST https://panel.quazzar.cloud/api/governance/policies \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Require encrypted backups",
"type": "backup",
"rule": {
"condition": "backup.encryption",
"operator": "equals",
"value": true
},
"enforcement": "mandatory",
"applies_to": "all_instances"
}'Example: Get Governance Scores
curl https://panel.quazzar.cloud/api/governance/scores \
-H "Authorization: Bearer <token>"{
"overall_score": 87,
"by_instance": [
{
"instance_id": "inst_abc123",
"name": "production-01",
"score": 92,
"violations": 1
},
{
"instance_id": "inst_def456",
"name": "staging-01",
"score": 78,
"violations": 4
}
]
}Kubernetes
Manage Kubernetes clusters and deployments through the Control Panel.
| Method | Path | Description |
|---|---|---|
GET | /api/k8s/clusters | List Kubernetes clusters |
POST | /api/k8s/clusters | Register a Kubernetes cluster |
GET | /api/k8s/clusters/{id} | Get cluster details |
PUT | /api/k8s/clusters/{id} | Update cluster configuration |
DELETE | /api/k8s/clusters/{id} | Remove a cluster |
GET | /api/k8s/deployments | List deployments across clusters |
POST | /api/k8s/deployments | Create a deployment |
GET | /api/k8s/deployments/{id} | Get deployment details |
PUT | /api/k8s/deployments/{id} | Update a deployment |
DELETE | /api/k8s/deployments/{id} | Delete a deployment |
Example: List Clusters
curl https://panel.quazzar.cloud/api/k8s/clusters \
-H "Authorization: Bearer <token>"[
{
"id": "k8s_abc123",
"name": "production-cluster",
"provider": "hetzner",
"version": "1.29",
"nodes": 5,
"status": "healthy",
"region": "eu-central"
}
]High Availability
Manage high availability clustering and failover for the Control Panel itself.
| Method | Path | Description |
|---|---|---|
GET | /api/cc/ha/status | Get HA cluster status |
GET | /api/cc/ha/nodes | List HA cluster nodes |
POST | /api/cc/ha/failover | Trigger a manual failover |
POST | /api/cc/ha/upgrade | Initiate a rolling upgrade |
Example: Get HA Status
curl https://panel.quazzar.cloud/api/cc/ha/status \
-H "Authorization: Bearer <token>"{
"mode": "active-passive",
"status": "healthy",
"leader": "node-1",
"nodes": 3,
"replication_lag_ms": 12,
"last_failover": "2025-01-10T08:15:00Z"
}Hosting
Manage hosting providers, servers, and infrastructure resources.
| Method | Path | Description |
|---|---|---|
GET | /api/hosting/providers | List configured hosting providers |
GET | /api/hosting/servers | List provisioned servers |
POST | /api/hosting/servers | Provision a new server |
GET | /api/hosting/servers/{id} | Get server details |
DELETE | /api/hosting/servers/{id} | Decommission a server |
GET | /api/hosting/regions | List available regions |
GET | /api/hosting/sizes | List available server sizes |
Example: Provision a Server
curl -X POST https://panel.quazzar.cloud/api/hosting/servers \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"provider": "hetzner",
"region": "eu-central",
"size": "cx31",
"name": "production-03",
"install_cloud_os": true
}'{
"id": "srv_abc123",
"name": "production-03",
"provider": "hetzner",
"region": "eu-central",
"size": "cx31",
"status": "provisioning",
"ip_address": null,
"created_at": "2025-01-15T10:30:00Z"
}MSP (Managed Service Provider)
Endpoints for managed service providers to manage client organizations and white-label branding.
| Method | Path | Description |
|---|---|---|
GET | /api/msp/clients | List MSP client organizations, each with nodes, nodes_online, users, mrr_cents, mrr_source (billing_report | estimate) and health (ok | degraded | down) |
POST | /api/msp/clients | Create a client organization |
GET | /api/msp/clients/{id} | Get client details |
PUT | /api/msp/clients/{id} | Update client configuration |
DELETE | /api/msp/clients/{id} | Remove a client |
GET | /api/msp/branding/{clientID} | Get a client’s branding |
PUT | /api/msp/branding/{clientID} | Update a client’s white-label branding |
GET | /api/msp/branding/{clientID}/domain-status | Custom domain: DNS records to add, dns_verified, tls_status (none | pending_dns | issuing | active | error), tls_issuer, tls_expires_at |
POST | /api/msp/branding/{clientID}/domain-status/recheck | Check DNS and TLS now |
Example: Create an MSP Client
curl -X POST https://panel.quazzar.cloud/api/msp/clients \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"contact_email": "[email protected]",
"instance_limit": 10,
"plan": "pro"
}'Example: Update White-Label Branding
curl -X PUT https://panel.quazzar.cloud/api/msp/branding/<client-id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"company_name": "CloudHost Pro",
"logo_url": "https://cdn.cloudhost.pro/logo.svg",
"primary_color": "#2563EB",
"support_email": "[email protected]"
}'MSP features require the Enterprise plan. Client organizations are fully isolated tenants with their own instances, users, and billing.
Partners
Manage partner relationships and partner tier configurations.
| Method | Path | Description |
|---|---|---|
GET | /api/partners | List partner organizations |
POST | /api/partners | Register as a partner |
GET | /api/partners/{id} | Get partner details |
PUT | /api/partners/{id} | Update partner information |
GET | /api/partner-tiers | List available partner tiers |
GET | /api/partners/me/referrals | The caller’s partner workspace: referral codes, sign-up counts, latest referred workspaces (staff: every partner) |
GET | /api/partners/{id}/referrals | One partner’s referrals |
POST | /api/partners/{id}/referral-codes | New referral code |
PUT | /api/partners/{id}/referral-codes/{codeId} | {"active": bool} |
Example: List Partner Tiers
curl https://panel.quazzar.cloud/api/partner-tiers \
-H "Authorization: Bearer <token>"[
{
"id": "tier_silver",
"name": "Silver",
"discount_percent": 10,
"min_instances": 5,
"features": ["priority-support", "co-branding"]
},
{
"id": "tier_gold",
"name": "Gold",
"discount_percent": 20,
"min_instances": 25,
"features": ["priority-support", "co-branding", "dedicated-account-manager"]
}
]