Skip to content

OAuth 2.1

CloudPress runs a standard OAuth 2.1 authorization server. Use it for third-party apps acting on a user's behalf; use API keys for first-party / server-to-server integrations.

OAuth access tokens are never admin and are fail-closed: an endpoint that does not declare a scope is unavailable via OAuth, and a token missing a required scope is rejected. Session and API-key credentials bypass scope checks entirely.


Authorization Server Metadata

Metadata is per-brand, derived from the request host.

GET /.well-known/oauth-authorization-server

Returned Params
  • issuer: String
  • authorization_endpoint: String | https://<host>/oauth/authorize
  • token_endpoint: String | https://<host>/oauth/token
  • revocation_endpoint: String | https://<host>/oauth/revoke
  • introspection_endpoint: String | https://<host>/oauth/introspect
  • registration_endpoint: String | https://<host>/oauth/registration
  • response_types_supported: Array | ["code"]
  • grant_types_supported: Array | ["authorization_code", "refresh_token"]
  • code_challenge_methods_supported: Array | ["S256"]
  • token_endpoint_auth_methods_supported: Array | ["none"]
  • scopes_supported: Array | see Scopes
  • service_documentation: String

Only none is advertised

This server issues public PKCE clients exclusively, so token_endpoint_auth_methods_supported advertises just ["none"] — the confidential methods are deliberately absent. They were listed once, and spec-compliant clients responded by registering with client_secret_post and getting a 400; advertising only what is actually accepted fixed that.


Flows

  • Grant types: authorization_code and refresh_token only.
  • PKCE is mandatory (S256 only).
  • Refresh tokens rotate — the previous refresh token is revoked on use. Rotation only advances once the new access token is validated against the brand, trial, and role checks.
  • No OpenID Connect — there is no /.well-known/openid-configuration, no userinfo, no ID tokens. The OIDC gem is wired only for Dynamic Client Registration.
  • Brand isolation — a token is bound to the brand (hostname) it was issued under and is rejected on any other brand. The authorize screen only lists the user's non-trial accounts on the current brand.

Token revocation on role change

Removing a user's role on an account revokes their OAuth tokens (and sessions) for that account.


Dynamic Client Registration (DCR)

POST /oauth/registration (RFC 7591)

Only public PKCE clients are issued. Sending a token_endpoint_auth_method other than "none" does not fail the registration — the value is silently coerced to "none", and the response echoes "none" back with no client_secret. Register as a public client and rely on PKCE (S256); a DCR-issued secret would add nothing on top of it.

Errors
  • 429 {"error":"too_many_requests", ...} | per-IP limit of 50 registrations / hour exceeded

Scopes

There are no default scopes — token presence is identity, and every resource scope is opt-in.

Scope Grants
sites:read Site reads + nested site reads (show/index, backups list, cache status, CDN status, variants list, tasks, edge-rules list, Shield reads, logs, metrics, transactional-email overview/send-logs/usage)
sites:write Site + nested site writes (create/update/destroy, backups, cache, restart, restore, edge rules, Shield writes, certificates, site-domain CRUD, variant change, transactional-email settings/suspension-removal/DNS re-check) — plus both sides of backup export (POST and GET /api/sites/{site-id}/backups/{volume-id}/export; the status read is deliberately behind the write scope because it hands back a presigned URL to the whole backup)
domains:read Domains list/show/query/available; domain-registration index/show/check/suggestions; contacts/hosts/processes reads
domains:write Domain-registration mutations; domain-contact create/update/destroy/resend; host/process writes
dns:read DNS zones index/show/dns_stats; DNS records index/show
dns:write DNS zone & record create/update/destroy
mailspace:read Mailspace list/show, plus every read inside a mailspace: mailboxes (incl. the address-availability check), app passwords, filter rules, out-of-office replies, aliases, groups, mailing lists, masked emails, extra domains and their DNS records, domain-verification status, delivery logs, archived items (incl. the raw message download) and the recoverable-mailbox list
mailspace:write Mailspace purchase, package resize and soft-delete, plus every write inside a mailspace: mailbox CRUD/restore/force-delete, app passwords, filter rules, out-of-office replies, aliases, groups, mailing lists, masked emails, extra-domain CRUD and DNS re-check, domain verification, mailbox recovery — and the irreversible immediate purge of a soft-deleted mailspace and permanent force-delete of a mailbox
billing:read Orders index/show; subscriptions index/show; carts show
cpanel:read cPanel account list/show and the account's domain list (GET /api/cpanel_accounts, GET /api/cpanel_accounts/{username}, GET /api/cpanel_accounts/{username}/domains) — reads only; every cPanel write is unreachable with any OAuth token (see below). cPanel is enabled per workspace, so these endpoints return 403 cpanel_not_enabled where it is not

GET /api/about accepts any valid token regardless of scope.

Endpoints Unavailable via OAuth

These declare no scope and are fail-closed — they require a session or API-key credential and return 403 endpoint not available via OAuth for any OAuth token.

Absence of a scope is the control

Scope enforcement resolves the required scope per action at request time; if nothing is declared, the request is refused rather than allowed. So there is no scope at all — not even a hypothetical billing:write — that reaches the endpoints below. That is deliberate for the money-moving ones: a third-party app cannot place, resize, or cancel a paid service on your behalf, however broadly you consented.

  • Account CRUD & account roles
  • API keys
  • Users and user_roles
  • The global tasks endpoints (GET /api/tasks/:id)
  • SSO (per-site and top-level)
  • Task result reporting (POST /api/webhooks/task/{task-id})
  • POST /api/webhooks/cdn_cache — the platform-internal CDN purge callback, which authenticates with a system API key from an allow-listed IP
  • Every cPanel account write. There is no cpanel:write scope, so all of these are session/API-key-only:

    cpanel:read covers the list/show reads and the domain list only. (Mailspace, by contrast, does expose its writes — under mailspace:write.) - The domain-order endpoints: POST /api/orders/domain and POST /api/domain_registrations/:id/registrant_change (both place a paid order and require a user-scoped API key — registrant_change is deliberately excluded from the domains:write scope) - All write operations on orders/sites-billing (there is intentionally no billing:write scope)

Scope Enforcement Errors

Errors
  • 401 {"error":"invalid_token","error_description":"token audience is not valid for /api"} | the token carries a resource audience (RFC 8707) minted for another resource (e.g. the MCP server) — /api rejects it before the scope check
  • 403 {"error":"insufficient_scope","error_description":"requires scope: <required>"} | with header WWW-Authenticate: Bearer error="insufficient_scope", scope="<required>"
  • 403 {"error":"insufficient_scope","error_description":"endpoint not available via OAuth"} | endpoint declares no scope