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_codeandrefresh_tokenonly. - PKCE is mandatory (
S256only). - 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:writescope, so all of these are session/API-key-only:POST /api/cpanel_accounts,PATCH /api/cpanel_accounts/{username}andDELETE /api/cpanel_accounts/{username}— ordering, resizing and cancelling move money.PATCH /api/cpanel_accounts/{username}/password— setting the password hands over full control of the hosting account.POST /api/cpanel_accounts/{username}/purge— irreversible destruction of the customer's data.POST /api/cpanel_accounts/{username}/domainsandDELETE /api/cpanel_accounts/{username}/domains/{domain}.POST /api/cpanel_accounts/{username}/session— a cPanel session is full interactive control of the account, so it sits with the other SSO endpoints above.
cpanel:readcovers the list/show reads and the domain list only. (Mailspace, by contrast, does expose its writes — undermailspace:write.) - The domain-order endpoints:POST /api/orders/domainandPOST /api/domain_registrations/:id/registrant_change(both place a paid order and require a user-scoped API key —registrant_changeis deliberately excluded from thedomains:writescope) - All write operations on orders/sites-billing (there is intentionally nobilling:writescope)
Scope Enforcement Errors
Errors
- 401
{"error":"invalid_token","error_description":"token audience is not valid for /api"}| the token carries aresourceaudience (RFC 8707) minted for another resource (e.g. the MCP server) —/apirejects it before the scope check - 403
{"error":"insufficient_scope","error_description":"requires scope: <required>"}| with headerWWW-Authenticate: Bearer error="insufficient_scope", scope="<required>" - 403
{"error":"insufficient_scope","error_description":"endpoint not available via OAuth"}| endpoint declares no scope