Skip to content

Authentication & API keys

Programs authenticate to the PlusPanel API with an API key sent as a bearer token. The panel itself uses a browser session; the CLI can get a key for you through a browser sign-in (device flow).

GET /api/v1/auth/me HTTP/1.1
Host: panel.example.com
Authorization: Bearer ppk_k3nmw2x7q4tz_8lV0m1Q2…
  • A key has the form ppk_<key id>_<secret>. The key id (12 characters) is public: it appears in key lists and in the audit log. The secret is shown once, when the key is created or rotated. PlusPanel stores only a hash of the key and cannot show it again.
  • A request with an Authorization header ignores cookies entirely: no session, no CSRF token needed.
  • A malformed, unknown, expired or revoked key answers 401 with the code invalid_api_key and WWW-Authenticate: Bearer error="invalid_token".
  • GET /api/v1/auth/me tells you who the key acts as, and with which roles and permissions.

Scopes come from the same roles and permissions as the panel; there is no second permission system. Each permission belongs to a resource and is either a read or a write:

Scope Allows
<resource>:read Reading that resource, for example servers:read, accounts:read
<resource>:write Changing it (and reading it: write implies read), e.g. sites:write
*:read Reading every resource
* Everything the user may do (there is no *:write: write *)

The resources are accounts, alerts, apikeys, audit, backups, databases, dns, jobs, mail, observability, plans, runtime, security, servers, sftp, sites, storage, tenant, updates, users, waitlist and webhooks; GET /api/v1/api-keys/scopes lists them with the permissions each scope covers, and the panel shows the same list when you create a key. On GET requests a resource’s read scope also covers the reads that the panel guards with a management permission (listing sites, for example), except a few strict ones that return a secret or the data itself: a job’s one-time output, file contents and the web terminal (accounts), invitation links (users), restores and backup downloads (backups) and lab test probes (jobs) need the write scope of their resource.

A key never does more than its user. A request made with a key is allowed only if both are true:

  1. the user’s role (and, for resellers and clients, the accounts they reach) allows it, exactly as in the panel, and
  2. one of the key’s scopes covers the permission.

If the user loses a role, their keys lose it too. A few self-service actions are never available with a key, whatever its scopes: changing a password or a second factor, managing sessions, step-up, impersonation and approving a CLI sign-in. GET /auth/me, GET /tenant/edition and GET /api-keys/scopes work with any key.

A key that is missing a scope answers 403 insufficient_scope (the message names the scope it needs); an endpoint that needs a signed-in session answers 403 api_key_not_allowed.

  • A personal key acts as you. It stops working when your user is disabled or removed.
  • An organization key acts as a dedicated service user of the organization (role admin, no password, unable to sign in). It survives the people who created it and is managed by the organization’s admins (creating one needs the organization-wide apikeys.manage permission). Use it for integrations that must keep working when staff changes. An organization key can create organization keys only.

Keys created by the CLI’s browser sign-in are personal keys (source device, valid 90 days by default; see below).

  • Expiry is optional and set when the key is created (it cannot be changed later). An expired key answers 401 invalid_api_key.
  • Rotation (POST /api-keys/{id}/rotate) gives the key a new secret and keeps its id, scopes and settings. With grace_seconds (at most 86400) the old secret stays valid for a grace period while you deploy the new one. A personal key is rotated only by its owner.
  • Revocation is immediate and final.
  • The panel shows each key’s last use (time and IP address) and its daily request counts.

A key can be limited to a list of networks (CIDRs or single addresses, IPv4 and IPv6; a CIDR with host bits set, like 192.0.2.7/24, is refused). A request from any other address answers 403 api_key_ip_denied, even with a valid key.

A key can create or change keys only with the apikeys:write scope, and only with scopes, expiry, allowlist and rate limit no wider than its own (else 403 api_key_wider_than_caller). Every creation, rotation, revocation, change and refusal is recorded in the audit log.

The CLI’s pluspanel login gets its key through a browser sign-in shaped like RFC 8628; other tools can use the same two public endpoints (no credential, no Idempotency-Key):

  1. POST /api/v1/auth/device/code with {"client_name": "…"} answers a secret device_code, a user_code (XXXX-XXXX), the panel’s /device page as verification_uri (and verification_uri_complete with the code), expires_in (600 seconds) and interval (5 seconds). At most 10 codes a minute and 5 pending requests per address (429 rate_limited).
  2. Show the user_code and open the page. The person signs in as usual, checks the request (name, address, time) and approves it (confirming their identity again if they have not in the last 5 minutes) or denies it.
  3. Poll POST /api/v1/auth/device/token with {"device_code": "…"} every interval seconds. Until the decision it answers 400 with the code authorization_pending; slow_down means you polled too fast (add 5 seconds to the interval); access_denied and expired_token (after 10 minutes, for an unknown code, or once the key was handed out) end the flow. These are problem documents: read the RFC 8628 error from code.
  4. The first poll after the approval answers 200 with token (shown once), key, tenant and user: a personal key named CLI: <client_name>, with full or read-only access as approved.
Terminal window
curl -fsS -X POST "$PLUSPANEL_URL/api/v1/auth/device/code" \
-H "Content-Type: application/json" -d '{"client_name": "deploy tool on laptop"}'
# {"device_code":"…","user_code":"BCDF-GHJK","verification_uri":"https://panel.example.com/device",
# "verification_uri_complete":"https://panel.example.com/device?code=BCDF-GHJK","expires_in":600,"interval":5}
curl -sS -X POST "$PLUSPANEL_URL/api/v1/auth/device/token" \
-H "Content-Type: application/json" -d '{"device_code": "…"}'
# before the decision: 400 {"code":"authorization_pending", …}
# after approval: 200 {"token":"ppk_…","key":{…},"tenant":{"slug":…,"name":…},"user":{…}}

Each key has a request budget per minute (600 by default, in every edition; a key can be given a lower limit). See Conventions → Rate limits.

  • One key per integration, with the narrowest scopes it needs and an expiry date.
  • Keep keys in a secret store or an environment variable, never in source code.
  • Prefer an organization key for long-running integrations, and an IP allowlist for servers with fixed addresses.
  • Rotate on a schedule, and immediately if a key may have leaked; revoke keys you no longer use.