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-Accountscopes 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/domainsandGET /api/dns_zonesalways return every domain / zone the key's user can reach, whether or not you sendX-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 acodekey —400{"errors":["Missing X-Auth-Account"]}— so don't branch onmissing_accountthere. Mailspacecreatealso needs an account context and answers400account_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_contactIDs. - 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/ordersGET /api/subscriptionsGET /api/domainsGET /api/dns_zonesGET /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:
202carrying atask_id— PHP version change (PATCH /api/sites/{site-id}/variants/php) and cache enable / disable / purge. Poll the returned id directly.202with 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 withGET /api/sites/{site-id}/tasksinstead. Site-domain add does return anid, 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 bare202. Nothing is created to poll; re-read the account or its roles to confirm the outcome.
Poll a task with
GET /api/tasks/:idorGET /api/sites/:site_id/tasks/:id. Task statuses:PENDING,RUNNING,OK,FAILED,CANCELLED,PAUSED. - Carts — site creation (POST /api/orders), site resize / plan-change (PATCH /api/sites/:idwithplan), 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