Skip to Content
DocsAPI ReferenceControl Panel API

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

PermissionOwnerAdminManagerViewerTeacher
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.

RouteNeeds
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}/commentstickets:write
GET /api/compliance/templates, GET /api/compliance/reports[/{id}[/download]]settings:read
POST /api/compliance/reportssettings:write
POST /api/notify, POST /api/devices/{deviceID}/push-token, POST /api/devices/{deviceID}/mutesettings: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, /failfleet:write
GET /api/governance/gitops/{id}/conflictsfleet:read
POST /api/governance/gitops/conflicts/{id}/resolvefleet:write
GET /api/fleet/instances/{id}/terminal-recordings[/{recID}]fleet:shell (owner, admin) or platform staff
PUT /api/v1/workspace/settingssettings:write from the workspace role
PUT /api/team/members/{id}/roleusers:manage from the workspace role

Authentication

Endpoints for user registration, login, profile management, and TOTP two-factor authentication.

MethodPathDescription
POST/api/auth/registerRegister a new user account. ?ref=CODE (or "ref" in the body) credits the sign-up to a partner referral code
POST/api/auth/loginAuthenticate and receive JWT tokens
GET/api/auth/meGet 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/profileUpdate user profile
POST/api/auth/change-passwordChange password (old_password, new_password)
POST/api/auth/passwordSame as change-password; also accepts current_password
GET / PUT/api/auth/notificationsNotification preferences (same as /api/profile/notifications)
POST/api/auth/refreshRe-issue the token (within the refresh window)
POST/api/auth/totp/verify-loginSecond factor: authenticator code (mfa_token, code)
POST/api/auth/webauthn/login/beginSecond factor: start a security-key assertion (mfa_token from the login answer) → {session_id, options}
POST/api/auth/webauthn/login/finishSecond factor: finish it (session_id, credential) → token
POST/api/auth/webauthn/register/begin, /register/finishRegister 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/onboardingOnboarding: save the workspace mode, finish onboarding
POST/api/auth/accept-inviteAccept an invitation (public)
POST/api/auth/totp/enableEnable TOTP two-factor authentication
POST/api/auth/totp/verifyVerify a TOTP code
POST/api/auth/totp/verify-loginFinish a sign-in with a TOTP code (mfa_token + code)
GET/api/v1/release-notesRelease 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.

MethodPathDescription
GET/api/workspacesThe caller’s workspaces
POST/api/workspacesCreate a workspace (caller becomes owner) and switch to it
POST/api/workspaces/{id}/switchRe-issue the token for another workspace
POST/api/workspaces/{id}/leaveLeave a workspace (409 last_owner, 409 last_workspace)
GET / PUT/api/v1/workspace/settingsWorkspace 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

MethodPathDescription
POST/api/auth/onboarding{"mode": "msp", "complete": false} saves the mode; {} finishes onboarding and returns a fresh token
POST/api/onboarding/enroll-tokenFleet registration token + CLI command (fleet:write)
GET/api/onboarding/first-heartbeatWait for the first node (fleet:read)
POST/api/invitations/bulkInvite 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

MethodPathDescription
GET/api/team/membersMembers 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

MethodPathDescription
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.


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.

MethodPathDescription
GET/api/fleet/instancesList all connected Cloud OS instances
GET/api/fleet/summaryGet fleet summary (total instances, health overview)
GET/api/fleet/healthGet health status for all instances
GET/api/fleet/metrics/{instanceId}Get metrics for a specific instance
GET/api/fleet/instances/{id}/metrics/historyGet historical metric data for a specific instance
POST/api/fleet/instances/{id}/impersonateMint an impersonate token to open an OS session
GET/api/fleet/alertsList active alerts across the fleet
POST/api/fleet/commandsSend a command to one or more instances
POST/api/fleet/tokensGenerate a fleet enrollment token
GET/api/fleet/instances?page=1&per_page=50&sort=-cpuPaged, sorted, filtered fleet list with facets (see below)
GET/api/fleet/instances/pageSame paged list, always the paged envelope
GET/api/fleet/regionsKnown regions and per-region node counts
PUT/api/fleet/instances/{id}/regionPin a node’s region ({"region": "eu-west"}; "" resets to automatic)
PUT/api/fleet/instances/{id}/clientLink a node to an MSP client ({"client_id": "<uuid>"} or null)
GET/api/fleet/instances/{id}/detailNode facts, apps and last-hour sparklines
GET/api/fleet/instances/{id}/logs/streamRealtime node logs (Server-Sent Events)
GET/api/fleet/instances/{id}/logs/wsRealtime node logs (WebSocket)
POST/api/fleet/instances/{id}/terminal/ticketSingle-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-recordingsRecorded 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=24hDashboard aggregates
GET/api/fleet/heatmap?metric=cpuHeatmap rows grouped by region (cpu, mem, disk)
POST/api/fleet/bulkStart a bulk job over many nodes
GET/api/fleet/bulk-jobsList bulk jobs, most recent first
GET/api/fleet/bulk-jobs/{id}A bulk job with per-node results
POST/api/fleet/bulk-jobs/{id}/cancelCancel a running bulk job
GET/api/fleet/clustersClusters with node count, Raft term, health and primary
GET/api/fleet/clusters/{id}/topologyCluster 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:

ParameterDescription
page, per_page1-based page, page size (default 50, max 200)
sorthost, ip, region, tenant, cpu, mem, disk, version, uptime, status, last_seen (last heartbeat; never-connected nodes last); prefix - for descending
statusonline, degraded, offline, or pending (enrolled but never connected; these are also counted as offline)
regiona region, or unknown
qsubstring of host name, IP, tag or location (or an exact node id)
groupfleet group id or name
versionexact Cloud OS version
tenantMSP client id (or none)
idscomma-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:

  1. 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 answers 409 {"code": "agent_offline"} when the node’s agent is not connected, 403 {"code": "shell_permission"} or 403 {"code": "impersonation"} when you may not open a shell, and 404 for a node outside your workspace.
  2. 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:

ParameterValuesDefaultDescription
range1h, 6h, 24h, 7d, 30d, 3mo24hTime range to query
metriccpu, 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.

MethodPathAuthDescription
POST/api/agent/v1/registerregistration 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/renewmTLS 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/heartbeatmTLS client certStatus, 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.

EndpointMethodDescription
/api/ai-healthGET{"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=168GETRecorded 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=50GETEval runs, optionally for one node
/api/ai-health/nodes/{id}/evalPOSTBody {"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/settingsGET, PUTStanding-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.

MethodPathDescription
GET/api/billing/subscriptionGet current subscription details
PUT/api/billing/subscriptionUpdate subscription plan
GET/api/billing/licensesList active licenses
GET/api/billing/usageGet current plan usage (nodes, users, storage)
GET/api/billing/packagesList available plans and pricing
GET/api/billing/invoicesList Stripe invoices (60 s cache)
GET/api/license/cc-featuresPlan-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.

MethodPathDescription
GET/api/ai/usage/summary?month=YYYY-MMSpend MTD (or a past month), forecast, top model, per day / node / model, budget status (billing:read, every plan)
GET/api/ai/usage/policyThe workspace AI budget and node overrides (billing:read)
PUT/api/ai/usage/policySet the AI budget (billing:write, Business+; 402 upgrade_required with "feature": "ai_budgets" otherwise; 422 on invalid values)
DELETE/api/ai/usage/policyRemove 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.

MethodPathDescription
GET/api/oauth/authorize/preflightCheck whether the requesting node is authorized to start the flow
POST/api/oauth/authorize/consentUser approves the authorization; returns a redirect with a code
POST/api/oauth/tokenExchange an authorization code for a CP-signed identity assertion

Flow overview

  1. The OS redirects the user to the CP authorization URL with client_id, redirect_uri, state, and code_challenge.
  2. The CP calls GET /api/oauth/authorize/preflight to verify the node is registered and the redirect_uri matches.
  3. The user approves on the CP consent screen (POST /api/oauth/authorize/consent).
  4. The CP redirects to the OS redirect_uri with a single-use, time-limited code and the original state.
  5. The OS calls POST /api/oauth/token to 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.

MethodPathDescription
GET/api/governance/policiesList governance policies
POST/api/governance/policiesCreate a governance policy
PUT/api/governance/policies/{id}Update a governance policy
DELETE/api/governance/policies/{id}Delete a governance policy
GET/api/governance/canaryList canary deployments
POST/api/governance/canaryCreate a canary deployment
PUT/api/governance/canary/{id}Update a canary deployment
GET/api/governance/gitopsList GitOps configurations
POST/api/governance/gitopsCreate a GitOps configuration
PUT/api/governance/gitops/{id}Update a GitOps configuration
GET/api/governance/gitops/{id}/conflictsList a configuration’s conflicts (fleet read)
POST/api/governance/gitops/conflicts/{id}/resolveResolve a conflict (fleet write)
GET/api/governance/scoresGet 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.

MethodPathDescription
GET/api/k8s/clustersList Kubernetes clusters
POST/api/k8s/clustersRegister 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/deploymentsList deployments across clusters
POST/api/k8s/deploymentsCreate 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.

MethodPathDescription
GET/api/cc/ha/statusGet HA cluster status
GET/api/cc/ha/nodesList HA cluster nodes
POST/api/cc/ha/failoverTrigger a manual failover
POST/api/cc/ha/upgradeInitiate 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.

MethodPathDescription
GET/api/hosting/providersList configured hosting providers
GET/api/hosting/serversList provisioned servers
POST/api/hosting/serversProvision a new server
GET/api/hosting/servers/{id}Get server details
DELETE/api/hosting/servers/{id}Decommission a server
GET/api/hosting/regionsList available regions
GET/api/hosting/sizesList 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.

MethodPathDescription
GET/api/msp/clientsList MSP client organizations, each with nodes, nodes_online, users, mrr_cents, mrr_source (billing_report | estimate) and health (ok | degraded | down)
POST/api/msp/clientsCreate 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-statusCustom 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/recheckCheck 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.

MethodPathDescription
GET/api/partnersList partner organizations
POST/api/partnersRegister as a partner
GET/api/partners/{id}Get partner details
PUT/api/partners/{id}Update partner information
GET/api/partner-tiersList available partner tiers
GET/api/partners/me/referralsThe caller’s partner workspace: referral codes, sign-up counts, latest referred workspaces (staff: every partner)
GET/api/partners/{id}/referralsOne partner’s referrals
POST/api/partners/{id}/referral-codesNew 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"] } ]