Skip to content

API overview

Authentication

All /api/* requests authenticate with an HTTP token in the Authorization header. Both the Token and Bearer schemes are accepted:

Authorization: Token {{ CloudPress API Token }}
Authorization: Bearer {{ CloudPress API Token }}

There are three credential types, all presented the same way. 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 the accounts that user belongs to) If the key is flagged admin No (bypasses scope checks) Optional IP access list.
System API key An account (system-managed only) If flagged admin No Account comes from the key's bearer — does not require X-Auth-Account; IP must be on the system access list.
OAuth 2.1 access token A user, bound to one (account, brand) Never Yes — fail-closed Issued via the OAuth 2.1 authorization server.

Trial accounts cannot use the API

An OAuth token is always rejected when its bound account is in trial. A user API key is rejected only when X-Auth-Account resolves to a trial account — sent without that header there is no account to test, so the request proceeds. A system API key is resolved before the trial check runs and is never refused on these grounds.


IP Restriction

You may restrict access by IP to a particular API Key in the CloudPress API Key Manager. Note that when you attempt to connect from a non-authorized IP, you will receive the same response as if you supplied an invalid API Key.

By default there is no IP restriction in place.


Setting Account Scope

Some API endpoints require an Account Scope. To specify which account you would like to be working under, include the header X-Auth-Account set to your Account ID:

X-Auth-Account: {{ CloudPress Account ID }}

Behavior:

  • For a user API key, X-Auth-Account scopes the request to a single account and sets the account context. Without it, list endpoints return resources across all accounts the key's user can access.
  • Two lists ignore the header entirely. GET /api/domains and GET /api/dns_zones always return every domain / zone the key's user can reach, whether or not you send X-Auth-Account. Filter client-side if you need one account's subset.
  • Some endpoints require it and return 400 {"errors":["Missing X-Auth-Account"],"code":"missing_account"} when it is absent — Orders, Carts, Subscriptions, Users, SSO, cPanel accounts, and all Domain Registration endpoints. DNS-zone create applies the same rule but renders its own body without a code key400 {"errors":["Missing X-Auth-Account"]} — so don't branch on missing_account there. Mailspace create also needs an account context and answers 400 account_required.
  • OAuth tokens always carry an account, so the header is ignored for them.

Authentication Failures

A failed authentication returns 401 with the header WWW-Authenticate: Token realm="Application" and an empty body. Causes include no/invalid token, an IP not on the key's access list, a trial account, or an X-Auth-Account value that doesn't match an account the token can access.

OAuth audience binding (RFC 8707)

An OAuth token minted with a resource indicator (e.g. one issued for the MCP server at /mcp) is rejected at /api/* with HTTP 401 and a JSON body — not the empty-body WWW-Authenticate form above:

{ "error": "invalid_token", "error_description": "token audience is not valid for /api" }

/api never issues resource-bound tokens, so any token carrying a resource was minted for a different audience and cannot reach /api.


Conventions

  • All API routes are under /api/. All responses are JSON.
  • Resource IDs are GUIDs, 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 (see Async Operations).

Pagination

Pagination is not a site-wide convention. Exactly five index endpoints accept page and per_page:

  • GET /api/orders
  • GET /api/subscriptions
  • GET /api/domains
  • GET /api/dns_zones
  • GET /api/cpanel_accounts
Param Type Default Notes
page Integer 1 Page number
per_page Integer 50 Records per page, clamped to a max of 100

Every other index action — sites, tasks, users, accounts, API keys, DNS records, domain registrations, domain contacts, Mailspace, a cPanel account's domains, and all site sub-resources — ignores both parameters and returns the full set in a single response.

Rate Limiting

Requests are limited to 600 per 10 minutes, counted per client IP address and per controller — not per credential. Two consequences follow:

  • One credential gets a separate 600-request budget against each group of endpoints (sites, domains, DNS zones, orders, …), so spreading work across resources is not capped at a single shared 600.
  • Two credentials calling from the same source IP — behind one NAT or egress gateway — share a bucket and can exhaust each other's budget.

Exceeding the limit returns 429 with an empty body.


Error Responses

Status When it occurs
400 Missing required header (missing_account, or account_required on Mailspace create); invalid/no-op params; order & site-resize validation errors (unknown variant/location/term/product); no_default_payment_method; an app-password update sent without the required allowed_ips key (allowed_ips_missing); a days value that is present but not a whole number on site transactional-email metrics (invalid_days); OAuth picker/DCR errors
401 Authentication failed (no/invalid token, IP blocked, trial account, account mismatch); admin-only endpoint with a non-admin credential
402 Service suspended for an unpaid invoice (service_suspended); registrar fee gate (payment_required)
403 Insufficient role ({"errors":["Not Authorized"]}, or forbidden on DNS zone / DNS record writes); OAuth scope failure (insufficient_scope); Shield not in plan / premium required; site pending deletion (pending_delete); cPanel not enabled for the workspace (cpanel_not_enabled)
404 Resource not found or not accessible to this token (e.g. unknown_task for a task id that does not belong to the site)
409 Conflict — Bunny resource not active (cdn_not_active, shield_not_active); registrar process already in flight (registration_busy); a package change already being applied (resize_in_flight)
422 Validation failure or permission restriction (e.g. inherited role, reseller-only, resize constraints); billing-settle guard on a freshly created site (billing_settling); cart payment could not be initiated (cart_pay_failed)
429 Rate limit exceeded (600 / 10 min)
502 Upstream failure (Bunny CDN/Shield, or domain registrar)
503 Feature disabled (feature_disabled, the domain-registration flag); no registrar configured for a TLD (registrar_unavailable); mail hosting not configured (stalwart_unavailable); cPanel temporarily unreachable (cpanel_unavailable); a Mailspace read the mail server could not perform (archived_items_unavailable, purged_mailboxes_unavailable, group_members_unavailable — see Mailspace)

Pending-delete sites are locked (403 pending_delete)

A soft-deleted site is not gone — it stays addressable for its retention window but is locked. GET /api/sites still lists it (with pending_delete: true and delete_scheduled_at), and GET /api/sites/{id} returns a reduced payload carrying status "pending_delete" instead of the live-site detail. Anything that would change it is refused with 403:

{
  "errors": ["This site is pending deletion and is locked. Restore it to make changes."],
  "code": "pending_delete",
  "delete_scheduled_at": "2026-09-01T14:22:05Z"
}

This covers PATCH and DELETE /api/sites/{id} and every nested site sub-resource — domains and certificates, backups and exports, restores, cache, CDN, edge rules, Shield, variants, tasks, metrics, logs, and SSO. The nested endpoints are guarded for reads as well as writes, so a locked site's sub-resources answer 403 even on GET. Restore the site to lift the lock.

Feature & plan gating

The only feature flag that gates API endpoints is domain-registration (returns 503 feature_disabled when off). Shield is plan-gated (403 shield_not_in_plan, or shield_premium_required for premium-only writes). CDN/Shield not yet provisioned on Bunny returns 409 (cdn_not_active / shield_not_active).


Async Operations

Most provisioning operations are asynchronous and return 202 Accepted with a reference to poll.

  • Tasks — several operations open a Task, but only some of them hand the id back. Three cases, and the difference matters when you plan your polling:

    • 202 carrying a task_id — PHP version change (PATCH /api/sites/{site-id}/variants/php) and cache enable / disable / purge. Poll the returned id directly.
    • 202 with no task id — restart, backup create and delete, restore, and site-domain promote / remove. A Task is created, but the response body is empty, so locate it with GET /api/sites/{site-id}/tasks instead. Site-domain add does return an id, but it is the new domain's guid, not a task id.
    • No Task at all — account-role removal (DELETE /api/accounts/{account-id}/roles/{id}) and account deletion (DELETE /api/accounts/{account-id}) answer with a bare 202. Nothing is created to poll; re-read the account or its roles to confirm the outcome.

    Poll a task with GET /api/tasks/:id or GET /api/sites/:site_id/tasks/:id. Task statuses: PENDING, RUNNING, OK, FAILED, CANCELLED, PAUSED. - Cartssite creation (POST /api/orders), site resize / plan-change (PATCH /api/sites/:id with plan), and domain orders (POST /api/orders/domain) are polled via the cart, not a task: they return a cart envelope; poll the cart for materialized orders. - Registrar processes — domain-registration mutations may open a process; poll it for completion.

Completion Callback

Order creation accepts an optional callback so you receive a webhook when the async work finishes instead of polling:

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

See Callbacks for the full contract.


About Endpoint

CloudPress offers an 'about' endpoint that provides information about who you are logged in as, as well as information about available resources. It accepts any valid token regardless of OAuth scope.

GET /api/about

Returned Params
  • version: String | Api Version Information
  • logged_in_as: String | Your user ID
  • account_scoped: String | If you authenticated with an account, this is your ID.
  • locations: Array
    • id: String
    • name: String
  • products: Array
    • id: String
    • name: String
    • description: String
  • php: Array | [] when no versions are available
    • id: Integer
    • label: String
    • is_default: Boolean
Example
curl -H "Authorization: Bearer $CLOUDPRESS_TOKEN" \
  https://your-instance/api/about