---
name: cloudpress-api
description: >-
  Complete CloudPress REST API reference for developers building integrations.
  Covers authentication (session tokens, API keys, OAuth 2.1 access tokens and
  scopes), all endpoints (sites, domains, DNS, orders/carts, backups, SSO,
  CDN, cache, edge rules, logs, Shield/WAF, metrics, tasks, accounts, users,
  domain registration), request parameters, response shapes, error handling,
  async task and registrar-process patterns, outbound billing webhooks (site
  subscription and domain-registration lifecycle events), rate limiting,
  pagination, and the authorization and account/role/brand model. Also covers
  the CloudPress MCP (Model Context Protocol) server — connecting an MCP client
  such as Claude Desktop/Cowork, the OAuth audience binding, JSON-RPC protocol,
  and tool catalog. Activate when the user asks about the CloudPress API, how to
  call an endpoint, what parameters to pass, what a response looks like, OAuth
  scopes, how to set up the CloudPress MCP server, or how to integrate with
  CloudPress programmatically.
user-invocable: false
---
<!-- NOTE: the `description` above is 1013 of the 1024-character limit. Do not
     append to it — a longer value silently breaks skill activation. Any
     addition must be balanced by a cut elsewhere in the same field. -->

<!-- API SNAPSHOT: the line below is the only place this file records which
     CloudPress release its contents were verified against. The skill's own
     release version is a publish date and does not encode it. Bump this in the
     same pass that bumps package.json, .claude-plugin/plugin.json, and
     CHANGELOG.md in the cloud-press/skills repo. -->

*Verified against CloudPress platform release **2026.07.01**.*

# When to Use This Skill

Activate this skill when:
- User asks how to call a specific CloudPress API endpoint
- User asks what parameters an endpoint accepts or what a response looks like
- User asks about authentication, API keys, OAuth, scopes, or the `X-Auth-Account` header
- User asks about permissions, roles, or what their token can do
- User is debugging a 400, 401, 402, 403, 404, 409, 422, 429, 502, or 503 from the API
- User asks about async operations, task polling, registrar processes, or completion callbacks
- User asks about **billing webhooks** (outbound site-subscription lifecycle webhooks configured on a billing plan) — what events fire, the payload shape, or why a domain/registrar event didn't produce one
- User asks how to set up, configure, or connect the CloudPress **MCP server** (e.g. in Claude Desktop/Cowork), what MCP tools exist, or why an MCP token is rejected
- User is building an integration with CloudPress

---

# CloudPress REST API

---

## Authentication

All `/api/*` requests authenticate via an HTTP Bearer token:

```bash
Authorization: Bearer <token>
```

There are **three credential types**, all presented as a Bearer token. The server resolves which kind it is (session/API key first, then OAuth).

| Credential | Identity | Admin-capable? | OAuth scopes apply? | Notes |
|---|---|---|---|---|
| **User API key** | A user (accesses accounts that user belongs to) | If the key's `is_admin` is set | No (bypasses scope checks) | Optional IP access list (`allowed_ips`). |
| **System API key** | An account (system-managed only) | If `is_admin` | No | `system_managed` keys only; IP must be on the system access list. The account is resolved from the key's `bearer` — `X-Auth-Account` is **not** required (and is ignored). |
| **OAuth 2.1 access token** | A user, bound to one `(account, brand)` | **Never** (OAuth tokens are never admin) | **Yes — fail-closed** | Issued by the OAuth 2.1 authorization server. See [OAuth 2.1](#oauth-21). |

**Account scoping — `X-Auth-Account`:**

```bash
X-Auth-Account: <account_guid>
```

Scopes a user API key to a single account and sets `Current.account`. Without it, list endpoints return resources across all accounts the token's user can access. Some endpoints **require** it (Orders, Subscriptions, Users, Carts, Webhooks, DNS-zone create) and return **`400`** `{"errors":["Missing X-Auth-Account"]}` when absent. OAuth tokens always carry an account, so the header is irrelevant for them.

**Authentication failures** return **`401`** with header `WWW-Authenticate: Token realm="Application"` and an empty body. Causes: no/invalid token, IP not on the access list, the resolved account is on trial, or the `X-Auth-Account` GUID doesn't match an account the token can access. **Trial accounts cannot use the API at all** (both session and OAuth auth reject them; a user-API-key call with no `X-Auth-Account` resolves no account and so isn't trial-blocked at auth time).

**Audience-bound OAuth tokens:** an OAuth access token minted with a `resource` (RFC 8707 audience, e.g. for `/mcp`) is rejected at `/api/*` with **`401`** `{"error":"invalid_token","error_description":"token audience is not valid for /api"}`. Only un-audienced (or `/api`-audienced) tokens work here.

**Rate limiting:** 600 requests / 10 minutes, per credential. Exceeding it returns **`429`** with an empty body.

---

## 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 integrations.

**Authorization server metadata** (per-brand, derived from request host):

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

```json
{
  "issuer": "https://<host>",
  "authorization_endpoint": "https://<host>/oauth/authorize",
  "token_endpoint": "https://<host>/oauth/token",
  "revocation_endpoint": "https://<host>/oauth/revoke",
  "introspection_endpoint": "https://<host>/oauth/introspect",
  "registration_endpoint": "https://<host>/oauth/registration",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
  "scopes_supported": ["sites:read","sites:write","domains:read","domains:write","dns:read","dns:write","billing:read"],
  "service_documentation": "https://<host>/docs/oauth"
}
```

- **Grant types:** `authorization_code` and `refresh_token` only. **PKCE is mandatory** (`S256` only). Refresh tokens rotate (previous token revoked on use).
- **No OpenID Connect** — there is no `/.well-known/openid-configuration`, no userinfo endpoint, and no ID tokens. The only OIDC-adjacent surface CloudPress exposes is Dynamic Client Registration (below).
- **Dynamic Client Registration (DCR, RFC 7591):** `POST /oauth/registration`. **Only public PKCE clients** are allowed — `token_endpoint_auth_method` must be `"none"`, else **`400`** `{"error":"invalid_client_metadata", ...}`. Per-IP limit: 50 registrations/hour → **`429`** `{"error":"too_many_requests", ...}`.
- **Brand isolation:** a token is bound to the brand (hostname) it was issued under and is rejected on other brands. The authorize screen only lists the user's non-trial accounts on the current brand.

### Scopes

There are **no default scopes** — every resource scope is opt-in. The complete vocabulary:

| Scope | Grants |
|---|---|
| `sites:read` | Site read + nested site reads (show/index, backups list, cache status, CDN status, variants list, tasks, edge-rules list, Shield reads, logs, metrics) |
| `sites:write` | Site + nested site writes (create/update/destroy, backups, cache, restart, restore, edge rules, Shield writes, certificates, site-domain CRUD, variant change, CDN metrics) |
| `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 |
| `billing:read` | Orders index/show; subscriptions index/show; carts show |

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

**Endpoints unavailable via OAuth** (no scope declared → fail-closed): account CRUD & roles, API keys, users, `user_roles`, global `tasks`, SSO (site and top-level), webhooks, the domain-order endpoints (`POST /api/orders/domain`, `POST /api/domain_registrations/:id/registrant_change`), and all **write** operations on orders/sites-billing (there is intentionally no `billing:write` scope). These require a session or API-key credential.

### Scope enforcement

Session and API-key credentials **bypass** scope checks entirely. For OAuth tokens:
- Missing the required scope → **`403`**, header `WWW-Authenticate: Bearer error="insufficient_scope", scope="<required>"`, body `{"error":"insufficient_scope","error_description":"requires scope: <required>"}`.
- Endpoint declares no scope (OAuth-blocked) → **`403`** `{"error":"insufficient_scope","error_description":"endpoint not available via OAuth"}`.

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

---

## Base URL & Conventions

- **Host:** `https://my.cloudpress.com` is the default CloudPress host, and every example in this document uses it. White-label brands are served on their own hostname (e.g. `brand.example.com`) with **identical paths** — substitute your brand's host throughout, including the `/mcp` and `/oauth/*` URLs, because credentials are brand-locked and a token issued on one brand is rejected on another.
- All API routes are under `/api/`. All responses are JSON.
- Resource IDs are GUIDs (UUID format), except numeric task IDs, integer volume IDs, integer DNS record-type codes, and numeric `domain_contact` IDs.
- Timestamps are ISO 8601, UTC.
- Async operations return **`202 Accepted`** — poll a task, registrar process, or cart to check completion.
- **Pagination:** index endpoints accept `page` and `per_page` (default `50`, max `100`).

---

## Error Responses

| Status | When it occurs | Body shape |
|--------|---------------|------------|
| `400` | Missing required header; invalid/no-op params; **order & site-resize validation errors** (unknown variant/location/term/product); `no_default_payment_method`; OAuth picker/DCR errors | `{"errors":["..."]}` often with a `"code"`; OAuth: `{"error":"...","error_description":"..."}` |
| `401` | Authentication failed (no/invalid token, IP blocked, trial account, account mismatch); admin-only endpoint with non-admin credential; OAuth token bound to a non-`/api` audience; domain-order endpoint called without a user credential (`user_required`) | Empty + `WWW-Authenticate: Token realm="Application"`; or `{"error":"invalid_token",...}` for the audience case |
| `402` | Service suspended for unpaid invoice (dunning); registrar fee gate (privacy/registrant change) | `{"errors":["..."],"code":"service_suspended","invoice":{...}}` or `{...,"code":"payment_required","price":{...},"portal_url":"..."}` |
| `403` | Insufficient role (`{"errors":["Not Authorized"]}`); OAuth scope failure; Shield not in plan / premium required | `{"errors":["Not Authorized"]}` or `{"error":"insufficient_scope"/"shield_not_in_plan"/"shield_premium_required"}` |
| `404` | Resource not found or not accessible to this token | Usually empty (`head`); some render `{"errors":[...]}` |
| `409` | Conflict — Bunny resource not active (`cdn_not_active`, `shield_not_active`); registrar process already in flight (`registration_busy`) | `{"error":"cdn_not_active"}` / `{"errors":[...],"code":"registration_busy",...}` |
| `422` | Validation failure or permission restriction (e.g. inherited role, reseller-only, resize constraints); billing still settling (`billing_settling`); payment could not be initiated for an order/plan-change/domain order (`cart_pay_failed`) | `{"errors":["..."]}`, often with a `"code"` |
| `429` | Rate limit exceeded (600/10min) | Empty |
| `502` | Upstream failure (Bunny CDN/Shield, or domain registrar) | `{"error":"<message>"}` |
| `503` | Feature disabled (`domain-registration` flag) or no registrar configured for a TLD | `{"errors":["..."],"code":"feature_disabled"/"registrar_unavailable"/"missing_api_key"}` |

> **Note:** there is **no longer** a generic `domains`/`backups` "feature flag → 503" model. DNS, domains, backups, and restores are no longer feature-flag gated. The only `FeatureFlag` 503 in the API is the `domain-registration` flag. CDN/Shield availability is signaled by **`409`** (`cdn_not_active`/`shield_not_active`), and Shield plan access by **`403`** (`shield_not_in_plan`/`shield_premium_required`).

**Common error messages:**

| Error body | Cause |
|-----------|-------|
| `{"errors":["Missing X-Auth-Account"]}` | Header required but absent (400) |
| `{"errors":["Account unable to create orders."]}` | Account state prevents orders (e.g. trial with 2+ sites) (400) |
| `{"errors":["Must have reseller permissions"]}` | Reseller-only endpoint (422) |
| `{"errors":["Unable to remove an inherited role."]}` | Deleting an inherited account role (422) |
| `{"errors":["..."],"code":"no_default_payment_method"}` | Order/resize with no saved payment method (400) |

---

## Authorization & Account Model

Many 401/403 errors come from token scope or role gaps.

### Account Types

| Type | How to identify | What it can do |
|------|----------------|----------------|
| Regular | Default | Manage own resources per role flags |
| Reseller | `reseller: true` on account | Create sub-accounts, create users, manage sub-account billing |
| Admin | `is_admin` on the **user/API key** (not the account) | Bypasses all permission checks; can create top-level accounts. **OAuth tokens are never admin.** |
| Trial | `is_trial: true` | **Blocked from the API entirely** (auth returns 401) |

**Brands:** resellers can white-label via an `AccountBrand` (hostname, name, logo, theme, SMTP, support contacts). Each account belongs to a brand; the brand is resolved from the request hostname and isolates OAuth tokens.

### Role Permission Flags

Every user has a role on each account they belong to. Flags on the role:

| Flag | What it controls in the API |
|------|-----------------------------|
| `is_admin` | Full access to all account operations — overrides all other flags |
| `can_edit` | PATCH/update on accounts, sites, domains, zones, registrations |
| `can_create` | POST orders / create resources |
| `can_destroy` | DELETE sites, domains, zones, registrations, account. A non-admin user cannot delete their **only** remaining account even with this flag (returns `403`/`Not Authorized`). |
| `billing` | View billing/pricing data; with `can_edit`, manage billing |
| `wp_login` | Generate SSO URLs for **any** WP user — without it, SSO is limited to WP users the token's user is explicitly linked to |

A suspended user fails every permission check. List available roles: `GET /api/user_roles`.

### Role Inheritance

A user granted a role on a parent account automatically receives the same role on all child accounts (cascaded asynchronously). Inherited roles carry `inherited_from` in the response and cannot be removed at the child level — change the parent role to update all descendants (deleting an inherited role returns **`422`**).

### Permission Check Flow

1. Is the token valid, IP allowed, account not on trial, not rate-limited?
2. For OAuth tokens: does the token's scope cover this action? (Session/API keys skip this.)
3. Is `X-Auth-Account` required by this endpoint?
4. Does the token's user have the required role flag on the target account?
5. For reseller-only operations: is the account a reseller?
6. For per-site tool endpoints: is the site dunning-suspended? (→ 402)

### Reseller Capabilities

With a reseller account and `X-Auth-Account` set to it:
- `POST /api/accounts` — create sub-accounts
- `POST /api/users` — create managed users (returned with an auto-generated password and API token)

Sub-accounts inherit the reseller's billing plan.

### Dunning Suspension

When a site's invoice is unpaid, per-site **show/update/destroy and tool endpoints** return **`402`**:

```json
{
  "errors": ["This service is suspended because an invoice is unpaid."],
  "code": "service_suspended",
  "invoice": { "number": "INV-123", "hosted_url": "https://..." }
}
```

List endpoints are not blocked — they surface `dunning_suspended: true` on the site instead.

---

## Async Patterns

### Tasks

Most provisioning operations are async and return **`202`** with a task reference.

**Async operations include:** PHP version change, backup creation, restore, restart, domain add/remove, account-role removal, cache enable/disable/purge, account delete. (Site creation, site resize/plan-change, and domain orders are also async but are polled via the **cart**, not a task — see [Carts](reference/endpoints-orders.md#carts) below.)

**Polling task status:**

```bash
GET /api/tasks/:id
GET /api/sites/:site_id/tasks/:id
```

**Task statuses:**

| Status | Meaning |
|--------|---------|
| `PENDING` | Queued, not yet started |
| `RUNNING` | Currently executing |
| `OK` | Completed successfully |
| `FAILED` | Failed |
| `CANCELLED` | Cancelled |
| `PAUSED` | Paused |

**Completion callback** — attach a `callback` to an order (`POST /api/orders` or `POST /api/orders/domain`) and CloudPress POSTs to your URL when that order's task finishes, instead of you polling:

```json
{ "callback": { "url": "https://your-app.com/webhook", "authorization": "Bearer your-secret" } }
```

- **Permitted keys:** `url` (required) and `authorization` (optional, sent **verbatim** as the `Authorization` header). Any other key in the `callback` object is discarded. Only the order endpoints consume a `callback`, so callbacks attach to **order** tasks only — it is ignored if sent anywhere else.
- **What CloudPress sends:** `POST` to your `url` with body `{ "timestamp": <epoch int>, "success": <bool>, "data": <task.data string> }` and headers `Authorization` (verbatim) + `Accept: application/json`. Same outbound delivery layer as billing webhooks — 30s timeout, 2xx = success, retry backoff 2/5/15 min, give up after 4h, so it's at-least-once (de-dup on `timestamp`).
- **Mechanism:** the `callback` is stashed on the cart and stamped into the order task's `labels` (`callback_url`/`callback_auth`); whenever that task's status is updated, a delivery is queued if the task carries a registered callback.
- **Utility endpoints** (distinct from your receiver — these are *inbound to CloudPress*): `GET /api/webhooks/task` returns `{ip_address}` for a reachability test; `POST /api/webhooks/task/:id` is how a task result is reported **into** CloudPress (used by infra; API-key + `X-Auth-Account`, **OAuth-blocked**; idempotent via a `data`+`success` digest), and posting that result is what triggers the outbound callback.

### Carts (order/billing async window)

`POST /api/orders` and site plan-change (`PATCH /api/sites/:id`) return a **cart envelope** (status `accepted`). Poll the cart for materialized orders. See [Orders](reference/endpoints-orders.md#orders) and [Carts](reference/endpoints-orders.md#carts).

### Registrar processes

Domain-registration mutations may complete synchronously (200) or open a registrar **process** (202 with `pending_process`). Poll via the [processes](reference/endpoints-domains.md#domain-registration-processes) endpoints. A registration with an in-flight process is "busy" and rejects further mutations with **`409`** `registration_busy`.

---

## Feature & Plan Gating

- **`domain-registration` feature flag** — when off, all domain-registration and domain-contact endpoints return **`503`** `{"errors":[...],"code":"feature_disabled"}`.
- **Shield plan entitlement** — Shield endpoints require the site's product to include Shield, else **`403`** `{"error":"shield_not_in_plan"}`. Premium-only writes (bot-detection update, custom WAF rule create) need a premium plan, else **`403`** `{"error":"shield_premium_required"}`.
- **Bunny activation** — CDN/Shield endpoints return **`409`** `cdn_not_active` / `shield_not_active` when the underlying Bunny pull zone / shield zone hasn't been provisioned yet.
- **Registrar availability** — registrar operations on a TLD with no configured registrar return **`503`** `registrar_unavailable`.

---

## Endpoint Reference

Per-endpoint documentation — routes, parameters, response shapes, status codes —
lives in the reference files below.

**Read the reference file for the relevant topic before answering an
endpoint-specific question or writing integration code.** Do not answer endpoint
questions from this router alone: it deliberately carries only the cross-cutting
concepts, and guessing a parameter name or response shape is worse than reading
the file.

| Topics | Reference file |
|---|---|
| GET /api/about, Accounts, Account Roles, API Keys, Subscriptions, Users, User Roles | [`reference/endpoints-accounts.md`](reference/endpoints-accounts.md) |
| Sites, Sites / Backups, Sites / Restores, Sites / Restart, Sites / Domains, Sites / SSO, Sites / Variants (PHP version) | [`reference/endpoints-sites.md`](reference/endpoints-sites.md) |
| Orders, Carts | [`reference/endpoints-orders.md`](reference/endpoints-orders.md) |
| Domains, Domain Registration | [`reference/endpoints-domains.md`](reference/endpoints-domains.md) |
| DNS Zones, DNS Zone Records | [`reference/endpoints-dns.md`](reference/endpoints-dns.md) |
| Sites / CDN, Sites / Cache, Sites / Edge Rules, Sites / Logs, Sites / Metrics / CDN | [`reference/endpoints-cdn.md`](reference/endpoints-cdn.md) |
| Sites / Shield (WAF, DDoS, bots), Sites / Metrics / Shield | [`reference/endpoints-shield.md`](reference/endpoints-shield.md) |
| Tasks, Sites / Metrics / Resources, Webhooks / Tasks | [`reference/endpoints-tasks.md`](reference/endpoints-tasks.md) |
| MCP Server (Model Context Protocol) | [`reference/mcp-server.md`](reference/mcp-server.md) |
| Billing Webhooks | [`reference/webhooks.md`](reference/webhooks.md) |
