Skip to content

Sites

OAuth scopes: reads require sites:read, writes require sites:write. Two exceptions, in both directions: the backup export endpoints require sites:write including the status read, while the metrics endpoints require only sites:read even though they are POSTs — they retrieve data rather than change anything. The metrics POSTs do still require site edit permission, though — a read-only collaborator gets 403 {"errors":["Not Authorized"]} even when holding sites:read. A dunning-suspended site returns 402 service_suspended on its show/update/destroy and tool endpoints (list endpoints instead surface dunning_suspended: true). A pending-delete site — one that has been deleted and is awaiting purge — returns 403 pending_delete on update and delete and on every nested site endpoint, reads included, while still appearing in list results and still resolving on GET /api/sites/:id. See Delete a Site.

Related site tooling lives on its own pages: CDN, Cache & Logs and Shield.

List Sites

If you omit the account ID header in this api call, it will return all sites available to your user. If you include the account ID header, it will only return sites for this account.

GET /api/sites

Deleted sites still appear here

Deletion is recoverable, so a deleted site stays in this list with pending_delete: true until it is purged — it is no longer dropped at delete time. If your integration treats this list as "live sites only", filter on pending_delete.

Returned Params
  • sites: Array
    • id: String
    • name: String
    • primary_domain: String
    • location: String (Geographic Region)
    • account: Object
      • id: String
      • name: String
    • package: String
    • dunning_suspended: Boolean
    • pending_delete: Boolean | true while the site is awaiting purge
    • delete_scheduled_at: DateTime | frozen purge date; null for live sites
    • created_at
    • updated_at

View Site

GET /api/sites/:id

A pending-delete site still resolves here — it does not 404 — but returns a deliberately reduced payload (second block below).

Returned Params
  • site: Object
    • id: String
    • name: String
    • primary_domain: String
    • location: String (Geographic Region)
    • region: String (Availability Zone)
    • package: String
    • pending_delete: Boolean | false here
    • php_version: String
    • domain_cname: String
    • sftp_base_path: String
    • dunning_suspended: Boolean | always false on this endpoint — a suspended site returns 402 instead
    • ssh: Object
      • ipaddr: String
      • username: String
      • password: String
      • port: Integer
    • domains: Array
      • See domains api endpoint
    • subscription: Object
    • id: String
    • status: String
    • created_at: DateTime
    • updated_at: DateTime
    • price: Object
      • amount_cents: Integer
      • term: String
    • run_rate: Object | the subscription's recurring charge, serialized as a money object with a cents (Integer) key and a currency_iso (String) key taken from the account's billing plan. It is not a bare integer
    • sites: Array | every site on this subscription, each a full site summary object (the same shape as an entry in List Sites) — including the site you requested. A shared subscription makes this a large nested payload
    • product: Object
      • id: String
      • name: String
    • account: Object
      • id: String
      • name: String
    • created_at
    • updated_at
Returned Params (pending-delete site)

While a site is awaiting purge its container is powered off, so the response carries only identity plus delete status — region, php_version, domain_cname, sftp_base_path, dunning_suspended, ssh, domains and subscription are all omitted.

  • site: Object
    • id: String
    • name: String
    • primary_domain: String
    • location: String (Geographic Region)
    • package: String
    • pending_delete: Boolean | true
    • status: String | "pending_delete" (this field is absent for live sites)
    • delete_scheduled_at: DateTime | when the site will be permanently purged
    • account: Object
      • id: String
      • name: String
    • created_at
    • updated_at

Update Site / Resize

PATCH /api/sites/:id

Handles two intents:

  1. Rename (synchronous) — pass name. Returns 202 with an empty body.
  2. Plan change (cart-mediated, async) — pass plan (a Product short_name). The price difference is invoiced against the account's default payment method off-session, and the response is the shared cart envelope (see Orders / Carts) — poll the cart for completion. It does not return a task_id. Requires a single-site account and a default payment method on file; the billing term is preserved.

When both fields are present, the rename runs first; on rename validation failure the resize is skipped entirely and the response is 422.

A pending-delete site cannot be updated by either intent: the request is rejected with 403 pending_delete and a delete_scheduled_at field, and nothing is changed. Restore the site first.

Params
  • name: String | site rename
  • plan: String | Product short_name (e.g. "basic")
Returned Params (plan-change path, 202 Accepted)
  • status: String | "accepted"
  • cart: Object | { token, status, rollup_status, poll_url }
  • payment: Object | { status, method_type, hosted_invoice_url }
  • orders: Array | [{ id, status, poll_url }] (empty during the async window)
Errors (plan change)
  • 400 unknown_product | plan doesn't match any Product short_name
  • 400 product_unavailable | Product exists but not offered on the account's billing plan
  • 400 no_default_payment_method | billing account not ready to charge
  • 422 no_price_for_plan | no price matching the account's term
  • 422 multi_site_resize_unsupported | account has more than one active site
  • 422 subscription_not_proratable | subscription is missing Stripe metadata
  • 422 cart_pay_failed | the off-session charge could not be initiated
  • 422 billing_settling | the site's billing is still settling (a freshly created site's initial billing window)

billing_settling is 422, not 402

The billing-settle guard on a plan change (and on site delete) returns 422 billing_settling. This is distinct from dunning suspension, which returns 402 service_suspended (see the page header).


Restart a Site

Restart the WordPress container for a site. Does not affect other containers such as the database or redis. Returns 202.

POST /api/sites/:id/restart

curl -X POST -H "Authorization: Bearer $CLOUDPRESS_TOKEN" -H "X-Auth-Account: $ACCOUNT_ID" \
  https://your-instance/api/sites/$SITE_ID/restart

Delete a Site

DELETE /api/sites/:id

Deletes the site recoverably, asynchronously. Returns 200 as soon as the request is accepted.

The site does not vanish — it enters a pending-delete state for a retention window, during which:

  • Its container is powered off; its data, backups and infrastructure are retained.
  • Billing is cancelled immediately — renewals stop and unused time is credited to the account's balance.
  • Transactional Email for the site is suspended for the duration of the window.
  • The site stays listed in GET /api/sites with pending_delete: true, and GET /api/sites/:id still returns it (reduced payload, status: "pending_delete").
  • Everything else about the site is locked: PATCH /api/sites/:id, a second DELETE, and every nested site endpoint — reads included — return 403 pending_delete with the site's delete_scheduled_at.
  • delete_scheduled_at is frozen at delete time as now + the account's site retention window, which defaults to 7 days unless the account or its billing plan sets a different value.

When the window elapses the site is purged, which is permanent and irreversible: the CDN pull zone is destroyed, the container project and its volumes — the site's data — are destroyed, and the site's domain records are removed, releasing each hostname. Until the purge those hostnames are still held by the pending-delete site, so attaching one to a different site in the same billing family returns 422 hostname_in_use.

sites:write alone no longer authorizes a delete

Deleting a site now requires the token's user to be a member of the site's account holding the billing-management role — an account admin, or a role carrying both billing and edit permission. For an account that inherits billing, the role is checked on its billing account. Elevated CloudPress staff privileges no longer qualify on their own: a caller who is not a member of the account gets 403 {"errors":["Not Authorized"]} where the same token may have succeeded previously.

Restore and early purge are not available over the API

There is no API endpoint to restore a pending-delete site or to purge it ahead of schedule; both live on the site's page in the CloudPress dashboard. Restore is a paid re-purchase of the same plan (the subscription was cancelled at delete time), not a free undo, and purging immediately waives the rest of the retention window.

Errors
  • 403 | caller is not an account member with the billing-management role ({"errors":["Not Authorized"]})
  • 403 pending_delete | the site is already pending deletion; body includes delete_scheduled_at
  • 402 service_suspended | the site is dunning-suspended (see the page header)
  • 422 billing_settling | the site's billing is still settling (a freshly created site's initial billing window)

Removing a site may affect volume discounts on existing sites.


Site SSO

This is a hybrid endpoint. It supports logging in for all SSO operations on a site. Currently that is WordPress and phpMyAdmin (database).

Not available via OAuth — SSO requires a session or API-key credential. Without the wp_login role flag, you can only generate URLs for WordPress users the token's user is explicitly linked to; with wp_login, any user.

List All WP Users

This returns raw user details from the WordPress site. The associated ID is WordPress' internal ID, not a CloudPress user.

GET /api/sites/:id/sso

Returned Params
  • users: Array
    • ID: Integer
    • user_login: String
    • display_name: String
    • user_email: String
    • user_registered: DateTime
    • roles: String
    • url: String

Login as User

POST /api/sites/:id/sso

Success returns 200 { "url": "..." }; failure returns 400. Returns 422 {"errors":["Missing username"]} when username is required but blank.

Params
  • kind: String | wordpress (default) or database
  • username: String | required unless kind is exactly database. The guard tests kind != "database", so omitting kind altogether still requires a username — send it whenever you are not doing a database login
Returned Params
  • url: String

Tasks

List all tasks for a given site.

GET /api/sites/{site-id}/tasks

GET /api/sites/{site-id}/tasks/{filter-name}/filter

Filter is optional, can be one of:

  • OK
  • PENDING
  • RUNNING
  • CANCELLED
  • PAUSED
  • FAILED
  • TODAY <-- Special filter to show all events from today.

All responses will be limited to the 100 most recent tasks.

Returned Params

Returns an array of objects:

  • id: Integer
  • name: String
  • data: String | Raw data from the event
  • labels: Object | Callback bookkeeping is stripped — any callback_* key you set when placing an order is redacted here
  • status: String | OK, PENDING, RUNNING, CANCELLED, PAUSED, FAILED
  • start_on: Timestamp | When the task was started. Omitted entirely (not null) when unset
  • end_on: Timestamp | Not consistently used, but may be when the task was completed. Omitted entirely (not null) when unset
  • created_at: Timestamp
  • updated_at: Timestamp
  • performed_by: Object | Omitted entirely when no user is attributed
    • id: String | null | The user's ID, or null when the actor is masked
    • name: String | Full name of the user, or a masked stand-in
    • email: String | null | null when the actor is masked

performed_by is masked for non-customer actors

Tasks performed by the platform itself report {"id": null, "name": "System", "email": null}, and tasks performed by support staff report {"id": null, "name": "Support", "email": null}. Only actions taken by a user in your own workspace return a real ID, name and email address. Treat name as a display string, not an identifier — match on id and expect it to be null.

View a single site task

GET /api/sites/{site-id}/tasks/{id}

Returns a single task object (same fields as above).

Errors
  • 404 unknown_task | no task with that id belongs to this site

Metrics

Realtime Resources

This endpoint will return the realtime status for a site.

GET /api/sites/{site-id}/metrics/resources

Returned Params
  • total_storage: Decimal
  • {Image Name}: Object | i.e. "WordPress"
    • cpu: Decimal | Percentage of total plan
    • storage: Decimal | GB
    • memory: Decimal | MB

Raw Metrics

This is an advanced endpoint that shows raw container metrics for a given site. Due to the nature of the CloudPress platform, new sites may return metric data created prior to the creation of your site. This is expected behavior, as the site may already exist waiting for a customer order.

Furthermore, it may return additional containers that the customer may not directly be able to interface with. For example, 'CloudPress Lite' may not include redis, but you may still see a redis container in these results. This is also an expected behavior of our platform.

POST /api/sites/{site-id}/metrics/resources

kind can be one or more of:

  • cpu
  • cpu_throttled
  • memory
  • memory_throttled
  • storage
Params
  • kind: Array | ['storage', 'memory'] (example)
  • period_start: Integer | unix timestamp (must be within the last month)
  • period_end: Integer | unix timestamp (after period_start, within the last month)
  • step: String | defaults to '1m'.

Period and Step parameters have no affect on storage metric. That will always return the current value.

Returned Params
  • service_name: Object
    • id: Integer
    • name: String
    • image: String
    • resources: Object | Dependent on which values selected
      • cpu: Array<time, value>
      • cpu_throttled: Array<time, value>
      • memory: Array<time, value>
      • memory_throttled: Array<time, value>
      • storage: Decimal
Errors

All error bodies use the {"errors": [...]} array shape.

  • 422 | empty or invalid kind, or period_start/period_end outside the last month / out of order
  • 502 | upstream metric service unavailable
  • 400 | unexpected failure

CDN Metrics

Retrieve RAW cdn metrics. Full description of the returned data can be found on Bunny's API Documentation Site. Note: Click the 200 under Responses to see field explanation.

POST /api/sites/{site-id}/metrics/cdn

Params
  • period_start: Integer | Unix timestamp. Takes precedence over period. (>3 months old → 422)
  • step: String | hourly or monthly. Only consulted with period_start.
  • period: String | preset window — one of 12h, 24h, 7d, 30d. Used only when period_start is absent. Unknown values → 422.
Returned Params
  • TotalBandwidthUsed: Integer | For the given period
  • TotalOriginTraffic: Integer | For the given period
  • TotalRequestsServed: Integer | For the given period
  • CacheHitRate: Integer
  • OriginResponseTimeChart: Object | "2025-11-20T00:00:00Z": 0 (ISO8601 timestamp → Integer)
  • BandwidthUsedChart: Object
  • BandwidthCachedChart: Object
  • CacheHitRateChart: Object
  • RequestsServedChart: Object
  • PullRequestsPulledChart: Object
  • OriginTrafficChart: Object
  • GeoTrafficDistribution: Object | e.g. "EU: Stockholm, SE": 0
  • Error3xxChart: Object
  • Error4xxChart: Object
  • Error5xxChart: Object

The response is Bunny's raw statistics payload rendered verbatim (only UserBalanceHistoryChart is stripped), so it carries additional fields beyond those listed — the request enables origin-response-time and origin-shield-bandwidth series. Read those key names from a live response; the names above are the ones this platform itself reads, and are the only ones it can guarantee.

Units: bandwidth and traffic totals are bytes, exactly as returned upstream; CacheHitRate is a percentage on a 0–100 scale, passed through unrounded. The control panel converts bandwidth to KB and rounds the hit rate — that conversion is not applied here.

Errors
  • 409 | CDN not active for this site
  • 422 | missing/zero/too-old period_start, or unknown period
  • 502 | upstream statistics fetch failed
Example: Show Monthly Bandwidth
curl -X POST -d '{"period_start": 1760983782, "step": "monthly"}'

This will return data for the past month, in daily increments, but more importantly, the TotalBandwidthUsed value will be for the entire 30 day period.

See also Shield metrics.


Backups

Please use the Tasks endpoint to monitor the status of these actions.

List All Backups

Return a list of backups for a given site, grouped by volume. Returns [] when there are no volumes; a volume with no archives renders as {}.

GET /api/sites/{site-id}/backups

Returned Params
  • {label}: Object | i.e. wordpress, or mysql
    • id: Integer | ID of the volume (required for backup operations)
    • size: Decimal | GB - total raw backup size (not the volume's provisioned capacity)
    • usage: Decimal | GB - compressed and deduplicated backup size (actual usage on disk)
    • archives: Array
      • id: String
      • name: String
      • created: String | in iso8601 format

Create a backup

PATCH /api/sites/{site-id}/backups/{volume-id}

Returns 202. An unknown volume returns 404 {"errors":["Volume not found"]}.

Params
  • name: String | Backup Name

Delete a backup

DELETE /api/sites/{site-id}/backups/{volume-id}

Returns 202.

Params
  • backup_id: String | Backup ID

Restore a backup

PATCH /api/sites/{site-id}/restores/{volume-id}

Returns 202.

Params
  • backup_id: String | Backup ID

Request a backup export

POST /api/sites/{site-id}/backups/{volume-id}/export

Ask for a downloadable .tar.gz of one backup archive. Asynchronous: the archive is materialized onto object storage and a short-lived presigned download URL is minted. Returns 202 with {"status": "preparing", "backup_id": "..."} — then poll Export status for the URL.

Both export endpoints require sites:write

Reading export status requires sites:write too — a sites:read token cannot even poll it. The status response carries a URL that grants read access to the whole backup, so both actions are treated as writes and both additionally require edit permission on the site.

Params
Errors
  • 422 | backup_id missing ({"errors":["backup_id is required"]})
  • 404 | unknown volume ({"errors":["Volume not found"]})
  • 403 | the token's user has no edit permission on the site ({"errors":["Not Authorized"]})

A 202 does not guarantee a new export started. Exports are prepared one at a time per volume, and a request made while one is already in flight is dropped. Treat the status endpoint — not a task id — as the source of truth for what the archive is actually doing.


Export status and download

GET /api/sites/{site-id}/backups/{volume-id}/export?backup_id={archive-id}

Returns the export state of a single archive, plus the presigned download URL once it is ready. Fetch that URL with a plain HTTPS GET — it needs no CloudPress credentials of its own.

Params
  • backup_id: String (required) | Archive ID, passed in the query string
Returned Params
  • status: String | one of none, in_progress, ready, expired, failed
  • url: String | only when ready — presigned HTTPS URL to the .tar.gz
  • expires_at: String | only when ready — iso8601, when the URL lapses
  • size: Integer | only when ready — size of the export in bytes
  • error: String | only when failed — human-readable failure reason
Errors
  • 422 | backup_id missing ({"errors":["backup_id is required"]})
  • 404 | unknown volume ({"errors":["Volume not found"]}), or no archive matches backup_id ({"errors":["Backup not found"]})
  • 403 | the token's user has no edit permission on the site ({"errors":["Not Authorized"]})

Treat url as a bearer secret

Anyone who has it can download the entire backup until it expires. It is returned only while status is ready, the response is sent with Cache-Control: no-store, and it should never be logged, forwarded or stored. Once status becomes expired, request a fresh export.


Change PHP Version

List Available

GET /api/sites/{site-id}/variants

Returned Params
  • php: Array<Object> | Currently only php is returned.
    • id: Integer | variant_id needed for changing the version
    • label: String
    • is_default: Boolean | When deploying a new site, this will be used.
    • active: Boolean | Current version the site is using

Change

When changing a site's php version, the site will be restarted with a different container image. Please allow time for the site to restart. Returns 202. The URL segment must be php — any other variant returns 422 {"errors":["only php version changes are allowed"]}.

PATCH /api/sites/{site-id}/variants/php

Params
  • variant_id: Integer
Returned Params (202 Accepted)
  • task_id: Integer | The ID of the task so you can follow along.