# 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 /staging` · scope `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

```bash
curl -s "https://app.pbn.ltd/api/v1/staging" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/sites` · scope `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

```bash
curl -s "https://app.pbn.ltd/api/v1/staging/sites" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s "https://app.pbn.ltd/api/v1/staging/sites/staging_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/eligible` · scope `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

```bash
curl -s "https://app.pbn.ltd/api/v1/staging/eligible" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": [
    {
      "site_id": 12345,
      "domain": "example.com",
      "type": "Wordpress",
      "ok": true,
      "why": ""
    }
  ]
}
```

Errors: `rate_limited`, `scope_missing`, `unauthorized`

## Create a staging site

`POST /staging/sites` · scope `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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/sites/{staging_id}/copy-to-live` · scope `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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-to-live" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/sites/{staging_id}/copy-from-live` · scope `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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/copy-from-live" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /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

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/staging/sites/staging_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/sites/{staging_id}/sftp` · scope `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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/sftp" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/sites/{staging_id}/access` · scope `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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/sites/staging_id/access" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "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 /staging/cancel` · scope `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

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/staging/cancel" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
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())
```

```javascript
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`

```json
{
  "data": {
    "cancelled": 1,
    "message": "Staging is cancelled and your staging sites are being removed."
  }
}
```

Errors: `forbidden`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`
