Staging
Staging sites: a private copy of a live WordPress or Static HTML site to try changes on, then copy to the live site (or refresh from it).
Staging: add-on, slots and price
GET/api/v1/stagingScope staging:read
Whether staging is on for your account, until when it is paid, how many staging sites you have and may have at once, the size limits, and the monthly price (before VAT, from the live price row) with the page where you buy or renew it. Buying happens in the browser: the API never starts a payment.
Example
curl -s "https://app.pbn.ltd/api/v1/staging" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/staging", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"available": true,
"active": true,
"included_with_staff_account": false,
"paid_until": "2026-12-01",
"used": 2,
"limit": 10,
"room": 8,
"limits": {
"max_files_gb": 10.0,
"max_db_gb": 2.0
},
"price_usd_per_month": "9.00",
"vat_percent": 0.0,
"months_offered": [
1,
3,
6,
12
],
"payment_methods": [
"paypal",
"crypto"
],
"can_buy": true,
"checkout_url": "https://app.pbn.ltd/staging/",
"supported_site_types": [
"Wordpress",
"Static HTML"
]
}
}
Errors: rate_limited, scope_missing, unauthorized
List staging sites
GET/api/v1/staging/sitesScope staging:read
Every staging site in your account that exists now (being created, ready, copying, paused, needs attention or being removed), newest first, with its last copy job. Poll it while a job runs: the state goes back to "ready" when a copy is done.
Example
curl -s "https://app.pbn.ltd/api/v1/staging/sites" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/staging/sites", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": [
{
"id": 71,
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"label": "redesign",
"state": "ready",
"state_label": "Ready",
"url": "https://stg71-3f9a1c2b4d5e.srv7.previewhost.net",
"open_url": "https://app.pbn.ltd/staging/71/open",
"admin_login_url": "https://app.pbn.ltd/staging/71/wp-login",
"sftp_on": false,
"files_mb": 412,
"db_mb": 38,
"last_copy_at": "2026-09-24T10:00:00Z",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"last_job": {
"kind": "to_staging",
"what": "full",
"state": "done",
"error": "",
"created_at": "2026-09-24T09:58:00Z",
"finished_at": "2026-09-24T10:00:00Z"
}
}
]
}
Errors: rate_limited, scope_missing, unauthorized
One staging site
GET/api/v1/staging/sites/{staging_id}Scope staging:read
One staging site with its last ten jobs (create, copy to live, copy from live, remove).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staging_id | path | integer | yes | The staging site id (see staging.list). |
Example
curl -s "https://app.pbn.ltd/api/v1/staging/sites/staging_id" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/staging/sites/staging_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites/staging_id", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"id": 71,
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"label": "redesign",
"state": "ready",
"state_label": "Ready",
"url": "https://stg71-3f9a1c2b4d5e.srv7.previewhost.net",
"open_url": "https://app.pbn.ltd/staging/71/open",
"admin_login_url": "https://app.pbn.ltd/staging/71/wp-login",
"sftp_on": false,
"files_mb": 412,
"db_mb": 38,
"last_copy_at": "2026-09-24T10:00:00Z",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"last_job": {
"kind": "to_staging",
"what": "full",
"state": "done",
"error": "",
"created_at": "2026-09-24T09:58:00Z",
"finished_at": "2026-09-24T10:00:00Z"
},
"jobs": [
{
"kind": "to_staging",
"what": "full",
"state": "done",
"error": "",
"created_at": "2026-09-24T09:58:00Z",
"finished_at": "2026-09-24T10:00:00Z"
}
]
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized
Which sites can be staged
GET/api/v1/staging/eligibleScope staging:read
Your live sites and, for each, whether a staging copy can be made of it now and, when not, the reason in plain words (only WordPress and Static HTML sites that are running).
Example
curl -s "https://app.pbn.ltd/api/v1/staging/eligible" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/staging/eligible", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/eligible", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": [
{
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"ok": true,
"why": ""
}
]
}
Errors: rate_limited, scope_missing, unauthorized
Create a staging site
POST/api/v1/staging/sitesScope staging:write
Makes a private staging copy of one of your live sites (files and database). It takes a few minutes for a typical site: poll staging.get until the state is "ready". Needs the staging add-on (staging.status) and a free slot; the live site is not changed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
site_id | body | integer | yes | The live site to copy (one of yours; see staging.eligible). |
label | body | string | no | An optional name for this copy (60 characters). |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/staging/sites", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
202
{
"data": {
"id": 71,
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"label": "redesign",
"state": "creating",
"state_label": "Being created",
"url": null,
"open_url": null,
"admin_login_url": null,
"sftp_on": false,
"files_mb": 412,
"db_mb": 38,
"last_copy_at": "2026-09-24T10:00:00Z",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"last_job": {
"kind": "create",
"what": "full",
"state": "queued",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"finished_at": null
}
}
}
Errors: forbidden, rate_limited, scope_missing, unauthorized, unavailable, validation_failed
Copy staging to the live site
POST/api/v1/staging/sites/{staging_id}/copy-to-liveScope staging:write · destructive
Publishes the staging copy: OVERWRITES the live site's files and/or database with the staging ones. An undo copy of what it replaced is kept for 7 days (ask support to put it back). Needs "confirm": true. Poll staging.get until the state is "ready" again.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staging_id | path | integer | yes | The staging site id (see staging.list). |
what | body | string (one of: full, db, files) | no | What to copy: full (files and database), db (database only, WordPress) or files (files only). Default: full. |
confirm | body | boolean | yes | Must be true. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-to-live" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-to-live", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-to-live", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
202
{
"data": {
"id": 71,
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"label": "redesign",
"state": "busy",
"state_label": "Copying",
"url": "https://stg71-3f9a1c2b4d5e.srv7.previewhost.net",
"open_url": "https://app.pbn.ltd/staging/71/open",
"admin_login_url": "https://app.pbn.ltd/staging/71/wp-login",
"sftp_on": false,
"files_mb": 412,
"db_mb": 38,
"last_copy_at": "2026-09-24T10:00:00Z",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"last_job": {
"kind": "to_staging",
"what": "full",
"state": "done",
"error": "",
"created_at": "2026-09-24T09:58:00Z",
"finished_at": "2026-09-24T10:00:00Z"
}
}
}
Errors: forbidden, not_found, rate_limited, scope_missing, unauthorized, validation_failed
Refresh staging from the live site
POST/api/v1/staging/sites/{staging_id}/copy-from-liveScope staging:write · destructive
Copies the live site over the staging copy (files and/or database), replacing any changes made on staging. The live site is not changed. Needs "confirm": true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staging_id | path | integer | yes | The staging site id (see staging.list). |
what | body | string (one of: full, db, files) | no | What to copy: full (files and database), db (database only, WordPress) or files (files only). Default: full. |
confirm | body | boolean | yes | Must be true. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-from-live" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-from-live", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-from-live", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
202
{
"data": {
"id": 71,
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"label": "redesign",
"state": "busy",
"state_label": "Copying",
"url": "https://stg71-3f9a1c2b4d5e.srv7.previewhost.net",
"open_url": "https://app.pbn.ltd/staging/71/open",
"admin_login_url": "https://app.pbn.ltd/staging/71/wp-login",
"sftp_on": false,
"files_mb": 412,
"db_mb": 38,
"last_copy_at": "2026-09-24T10:00:00Z",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"last_job": {
"kind": "to_staging",
"what": "full",
"state": "done",
"error": "",
"created_at": "2026-09-24T09:58:00Z",
"finished_at": "2026-09-24T10:00:00Z"
}
}
}
Errors: forbidden, not_found, rate_limited, scope_missing, unauthorized, validation_failed
Remove a staging site
DELETE/api/v1/staging/sites/{staging_id}Scope staging:write · destructive
Removes the staging copy (its files, database and address). The live site is not touched and the slot is free again straight away. Needs "confirm": true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staging_id | path | integer | yes | The staging site id (see staging.list). |
confirm | body | boolean | yes | Must be true. |
Example
curl -s -X DELETE "https://app.pbn.ltd/api/v1/staging/sites/staging_id" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/staging/sites/staging_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites/staging_id", {
method: "DELETE",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
202
{
"data": {
"id": 71,
"site_id": 12345,
"domain": "example.com",
"type": "Wordpress",
"label": "redesign",
"state": "destroying",
"state_label": "Being removed",
"url": "https://stg71-3f9a1c2b4d5e.srv7.previewhost.net",
"open_url": "https://app.pbn.ltd/staging/71/open",
"admin_login_url": "https://app.pbn.ltd/staging/71/wp-login",
"sftp_on": false,
"files_mb": 412,
"db_mb": 38,
"last_copy_at": "2026-09-24T10:00:00Z",
"error": "",
"created_at": "2026-09-24T09:40:00Z",
"last_job": {
"kind": "to_staging",
"what": "full",
"state": "done",
"error": "",
"created_at": "2026-09-24T09:58:00Z",
"finished_at": "2026-09-24T10:00:00Z"
}
}
}
Errors: forbidden, not_found, rate_limited, scope_missing, unauthorized, validation_failed
Switch on SFTP / reset its password
POST/api/v1/staging/sites/{staging_id}/sftpScope staging:write
SFTP for one staging site. The first call switches it on and returns the login WITH a new password; later calls return the login with "password": "" (it is shown only once and never stored). Send "reset": true to set a new password (the old one stops working).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staging_id | path | integer | yes | The staging site id (see staging.list). |
reset | body | boolean | no | true = make a NEW password (the old one stops working). Default: False. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/sftp" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/staging/sites/staging_id/sftp", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites/staging_id/sftp", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"host": "sftp.example.com",
"port": 2222,
"user": "zg71",
"password": "shown-once-only",
"shown_once": true,
"note": "Copy it now: for your security it is shown only this once."
}
}
Errors: conflict, forbidden, not_found, rate_limited, scope_missing, unauthorized, unavailable, validation_failed
The share login of a staging site
POST/api/v1/staging/sites/{staging_id}/accessScope staging:write
The username and password that protect the staging site, to share it with someone who has no account (the same "Password to share" the staging page shows the account holder). Account holder only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staging_id | path | integer | yes | The staging site id (see staging.list). |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/access" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/staging/sites/staging_id/access", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/sites/staging_id/access", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"url": "https://stg71-3f9a1c2b4d5e.srv7.previewhost.net",
"user": "staging",
"password": "the-share-password"
}
}
Errors: forbidden, not_found, rate_limited, scope_missing, unauthorized
Cancel the staging add-on
POST/api/v1/staging/cancelScope staging:write · destructive
Ends the staging add-on now and removes EVERY staging site of the account. Months already paid are not refunded. Your live sites are not affected. Needs "confirm": true.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
confirm | body | boolean | yes | Must be true. |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/cancel" \
-H "Authorization: Bearer $PBN_API_KEY"
import os
import requests
headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/staging/cancel", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/staging/cancel", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"cancelled": 1,
"message": "Staging is cancelled and your staging sites are being removed."
}
}
Errors: forbidden, rate_limited, scope_missing, unauthorized, validation_failed