# Sites

Every site type: list, search, create, change, delete; status, nameservers, CDN, SEO.

## List and search sites

`GET /sites` · scope `sites:read`

The same search, filters and order as the sites list in the panel. `total` is the number of matching sites. A key restricted to some sites only sees those.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `search` | query | string | no | Part of the domain or name (www. is ignored). |
| `state` | query | string (one of: ok, waiting, activating, working, error, frozen, notinstalled) | no | ok = live, waiting = waiting for DNS, activating = activating CDN, working = being installed/changed, error, frozen = paused, notinstalled. |
| `type` | query | string (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki, mismatch) | no | Site type; "mismatch" = the files run a different platform than the site type. |
| `group` | query | string | no | Group id, or "none" for sites in no group. |
| `cdn` | query | string | no | CDN name, e.g. Cloudflare, BunnyCDN, KeyCDN, CDN77.COM, Gcore, CloudFront. |
| `php_version` | query | string | no | PHP version value, e.g. "PHP 8.3". |
| `indexed` | query | string (one of: yes, no, pending) | no | Google indexation state. |
| `online` | query | string (one of: online, offline) | no | The online badge: offline = a confirmed problem. |
| `sort` | query | string (one of: newest, oldest, name, name_desc, domain, domain_desc) | no | Order of the list. Default: `newest`. |
| `limit` | query | integer | no | Items per page (1-200). Default: `50`. |
| `cursor` | query | string | no | The next_cursor value of the previous page. Omit for the first page. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites?search=blog&state=ok&limit=20" \
  -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/sites?search=blog&state=ok&limit=20", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites?search=blog&state=ok&limit=20", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": [
    {
      "id": 123,
      "name": "example",
      "domain": "example.com",
      "type": "Wordpress",
      "state": "Ok",
      "state_group": "ok",
      "url": "https://example.com",
      "created_at": "2026-01-05T12:00:00Z",
      "group": {
        "id": 7,
        "name": "Travel"
      },
      "cdn": "Cloudflare",
      "php_version": "PHP 8.3",
      "use_https": true,
      "use_www": false,
      "frozen": false,
      "switched_off_by_staff": false,
      "suspended": false,
      "pending_delete": false,
      "main_cloud": false,
      "indexed": "yes",
      "last_indexed_at": "2026-09-10T03:00:00Z",
      "last_not_indexed_at": null,
      "domain_expires_at": "2027-03-01T00:00:00Z",
      "seo": {
        "trust_flow": 12,
        "citation_flow": 20,
        "backlinks": 340,
        "referring_domains": 41,
        "trust_flow_change": 1,
        "citation_flow_change": 0,
        "backlinks_change": 12,
        "referring_domains_change": 2,
        "measured_at": "2026-09-15T02:00:00Z"
      },
      "platform": {
        "type": "Wordpress",
        "label": "WordPress",
        "detected": "wordpress",
        "mismatch": null,
        "checked_at": "2026-09-19T04:00:00Z"
      },
      "online": {
        "status": "online",
        "label": "Online",
        "reason": null,
        "since": null,
        "checked_at": "2026-09-19T20:00:00Z",
        "text": "Homepage answered normally (last check ...)."
      }
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

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

## Get one site

`GET /sites/{site_id}` · scope `sites:read`

Everything the site page shows: state, URL, settings, why it is paused (freeze.reasons), nameserver status (current vs required, pointed, autopilot), CDN, SEO, indexation, platform, online status and what can be done with it right now (capabilities).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123" \
  -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/sites/123", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "id": 123,
    "name": "example",
    "domain": "example.com",
    "type": "Wordpress",
    "state": "Ok",
    "state_group": "ok",
    "url": "https://example.com",
    "created_at": "2026-01-05T12:00:00Z",
    "group": {
      "id": 7,
      "name": "Travel"
    },
    "cdn": "Cloudflare",
    "php_version": "PHP 8.3",
    "use_https": true,
    "use_www": false,
    "frozen": false,
    "switched_off_by_staff": false,
    "suspended": false,
    "pending_delete": false,
    "main_cloud": false,
    "indexed": "yes",
    "last_indexed_at": "2026-09-10T03:00:00Z",
    "last_not_indexed_at": null,
    "domain_expires_at": "2027-03-01T00:00:00Z",
    "seo": {
      "trust_flow": 12,
      "citation_flow": 20,
      "backlinks": 340,
      "referring_domains": 41,
      "trust_flow_change": 1,
      "citation_flow_change": 0,
      "backlinks_change": 12,
      "referring_domains_change": 2,
      "measured_at": "2026-09-15T02:00:00Z"
    },
    "platform": {
      "type": "Wordpress",
      "label": "WordPress",
      "detected": "wordpress",
      "mismatch": null,
      "checked_at": "2026-09-19T04:00:00Z"
    },
    "online": {
      "status": "online",
      "label": "Online",
      "reason": null,
      "since": null,
      "checked_at": "2026-09-19T20:00:00Z",
      "text": "Homepage answered normally (last check ...)."
    },
    "title": "My blog",
    "subtitle": null,
    "login_url": "wp-login.php",
    "admin_email": "you@example.com",
    "autoupdate_wordpress": true,
    "ssl_mode": "full",
    "mailbox_requested": false,
    "freeze": {
      "frozen": false,
      "reasons": [],
      "temporary_unfreeze": null
    },
    "nameservers": {
      "current": [
        "ada.ns.cloudflare.com",
        "bob.ns.cloudflare.com"
      ],
      "required": [
        "ada.ns.cloudflare.com",
        "bob.ns.cloudflare.com"
      ],
      "pointed": true,
      "main_cloud": false,
      "zone_expired": false,
      "dns_provider": "Cloudflare",
      "checked_at": "2026-09-19T13:25:00Z",
      "autopilot": null
    },
    "cdn_detail": {
      "provider": "Cloudflare",
      "https": true,
      "www": false,
      "own_account": false,
      "https_only": true
    },
    "capabilities": {
      "edit": true,
      "delete": true,
      "backup": true,
      "restore": true,
      "reinstall": true,
      "purge_cache": true,
      "admin_login": true,
      "files": true,
      "error_log": true,
      "dns_records": true
    },
    "backups_count": 4,
    "panel_url": "https://app.pbn.ltd/sites/123",
    "updated_at": "2026-09-19T10:00:00Z"
  }
}
```

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

## Fields and choices for a new site

`GET /sites/options` · scope `sites:write`

The fields the Create site form accepts for this account right now, with their choices (site types, CDNs, PHP versions, templates, blueprints, groups...) and defaults. Build a create screen from it; every field name here is accepted by POST /sites (unknown ones go in form_fields). `required` is the answer FOR THE TYPE in the response's `type` - a WordPress site also needs title, subtitle, login_url and feedback_email, no other type does. `required_for_types` on each field and the `required_by_type` map give the whole picture in one call.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `type` | query | string (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki) | no | Describe the form for this site type. Which fields are REQUIRED depends on the type, so name the type you are going to create. Default: the form's own default type. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/options" \
  -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/sites/options", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/options", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "type": "Wordpress",
    "fields": [
      {
        "name": "type",
        "form_field": "type",
        "label": "Type",
        "required": true,
        "help": null,
        "kind": "typedchoice",
        "choices": [
          {
            "value": "Wordpress",
            "label": "WordPress"
          }
        ],
        "default": "Wordpress",
        "required_for_types": [
          "Drupal",
          "Grav",
          "..."
        ]
      }
    ],
    "required_by_type": {
      "Wordpress": [
        "cdn",
        "feedback_email",
        "login_url",
        "name",
        "php_version",
        "subtitle",
        "title",
        "type"
      ],
      "Static HTML": [
        "cdn",
        "name",
        "php_version",
        "type"
      ]
    }
  }
}
```

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

## Create a site (any type)

`POST /sites` · scope `sites:write` · returns a job

Creates a site exactly as the Add new site form does - same validation, same slot limit, same checks - for every type: WordPress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav and MediaWiki. A WordPress site also needs title, subtitle, login_url and feedback_email; no other type does - GET /sites/options?type=... lists exactly what the type you are creating requires. Installation runs in the background: follow the returned job (GET /jobs/{job_id}) or the site state. Once installed, point the domain at nameservers.required from GET /sites/{site_id}.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `type` | body | string (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki) | yes | Site type. |
| `name` | body | string | yes | A unique short name (letters, digits, dashes). |
| `domain` | body | string | yes | The domain (or subdomain of one of your sites). |
| `cdn` | body | string | no | CDN (see GET /sites/options for the choices open to you). |
| `php_version` | body | string | no | PHP version value, e.g. "PHP 8.3" (see GET /sites/options). |
| `use_https` | body | boolean | no | Serve over HTTPS. |
| `use_www` | body | boolean | no | Use www. as the primary host. |
| `title` | body | string | no | Site title (WordPress and the ready-installed applications). REQUIRED when type is Wordpress. |
| `subtitle` | body | string | no | Tagline. REQUIRED when type is Wordpress; ignored for other types. |
| `admin_email` | body | string | no | Administrator e-mail (WordPress and the applications). |
| `feedback_email` | body | string | no | Contact-form e-mail address, where the site's contact form sends its messages. REQUIRED when type is Wordpress; ignored for other types. |
| `login_url` | body | string | no | WordPress login path (default wp-login.php). REQUIRED when type is Wordpress. |
| `template_id` | body | integer | no | WordPress template id (omit for random). |
| `blueprint_id` | body | integer | no | Deploy from one of your blueprints (WordPress). |
| `group_id` | body | integer | no | Put the site in this group. |
| `create_mailbox` | body | boolean | no | Create an e-mail account for the domain. |
| `autoupdate_wordpress` | body | boolean | no | Auto-update WordPress core and plugins. |
| `form_fields` | body | object | no | Any other Create site form field by its form name (see form_field in GET /sites/options), e.g. the WordPress theme/plugin pickers. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "Wordpress", "name": "myblog", "domain": "myblog-example.com", "cdn": "Cloudflare", "use_https": true, "title": "My blog", "subtitle": "Notes from the workshop", "admin_email": "you@example.com", "feedback_email": "you@example.com"}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites", headers=headers, json={"type": "Wordpress", "name": "myblog", "domain": "myblog-example.com", "cdn": "Cloudflare", "use_https": True, "title": "My blog", "subtitle": "Notes from the workshop", "admin_email": "you@example.com", "feedback_email": "you@example.com"}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"type": "Wordpress", "name": "myblog", "domain": "myblog-example.com", "cdn": "Cloudflare", "use_https": true, "title": "My blog", "subtitle": "Notes from the workshop", "admin_email": "you@example.com", "feedback_email": "you@example.com"})
});
console.log(res.status, await res.json());
```

### Response

`202`

```json
{
  "data": {
    "id": 124,
    "domain": "new-example.com",
    "state": "New",
    "...": "(the site, as GET /sites/{site_id})"
  },
  "job": {
    "id": "job_4f1c0a9e2b7d6c5a3e10",
    "kind": "...",
    "status": "running",
    "url": "https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10"
  }
}
```

Errors: `conflict`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Fields and choices to edit a site

`GET /sites/{site_id}/options` · scope `sites:write`

The fields the Edit site form offers for THIS site right now (they depend on type, CDN and state), with choices and current values.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/options" \
  -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/sites/123/options", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/options", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "fields": [
      {
        "name": "php_version",
        "label": "Php version",
        "required": true,
        "choices": [
          {
            "value": "PHP 8.3",
            "label": "PHP 8.3"
          }
        ],
        "default": "PHP 8.3"
      }
    ]
  }
}
```

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

## Change site settings

`PATCH /sites/{site_id}` · scope `sites:write`

Change any setting the Edit site form offers: name, domain, PHP version, HTTPS, www, group, admin e-mail, login URL, auto-updates, mailbox. Send only what changes. Changes that touch the server (domain, PHP, HTTPS/www) run in the background: a job is returned then.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `name` | body | string | no | A unique short name (letters, digits, dashes). |
| `domain` | body | string | no | The domain (or subdomain of one of your sites). |
| `php_version` | body | string | no | PHP version value, e.g. "PHP 8.3" (see GET /sites/options). |
| `use_https` | body | boolean | no | Serve over HTTPS. |
| `use_www` | body | boolean | no | Use www. as the primary host. |
| `admin_email` | body | string | no | Administrator e-mail (WordPress and the applications). |
| `login_url` | body | string | no | WordPress login path (default wp-login.php). REQUIRED when type is Wordpress. |
| `group_id` | body | integer | no | Put the site in this group. |
| `create_mailbox` | body | boolean | no | Create an e-mail account for the domain. |
| `autoupdate_wordpress` | body | boolean | no | Auto-update WordPress core and plugins. |
| `form_fields` | body | object | no | Any other Create site form field by its form name (see form_field in GET /sites/options), e.g. the WordPress theme/plugin pickers. |
| `ssl_mode` | body | string (one of: full, flexible) | no | Cloudflare SSL mode (only offered when the site has its own address records). |

### Example

```bash
curl -s -X PATCH "https://app.pbn.ltd/api/v1/sites/123" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"php_version": "PHP 8.3", "use_www": true}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.patch("https://app.pbn.ltd/api/v1/sites/123", headers=headers, json={"php_version": "PHP 8.3", "use_www": True}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123", {
  method: "PATCH",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"php_version": "PHP 8.3", "use_www": true})
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "id": 123,
    "name": "example",
    "domain": "example.com",
    "type": "Wordpress",
    "state": "Ok",
    "state_group": "ok",
    "url": "https://example.com",
    "created_at": "2026-01-05T12:00:00Z",
    "group": {
      "id": 7,
      "name": "Travel"
    },
    "cdn": "Cloudflare",
    "php_version": "PHP 8.3",
    "use_https": true,
    "use_www": false,
    "frozen": false,
    "switched_off_by_staff": false,
    "suspended": false,
    "pending_delete": false,
    "main_cloud": false,
    "indexed": "yes",
    "last_indexed_at": "2026-09-10T03:00:00Z",
    "last_not_indexed_at": null,
    "domain_expires_at": "2027-03-01T00:00:00Z",
    "seo": {
      "trust_flow": 12,
      "citation_flow": 20,
      "backlinks": 340,
      "referring_domains": 41,
      "trust_flow_change": 1,
      "citation_flow_change": 0,
      "backlinks_change": 12,
      "referring_domains_change": 2,
      "measured_at": "2026-09-15T02:00:00Z"
    },
    "platform": {
      "type": "Wordpress",
      "label": "WordPress",
      "detected": "wordpress",
      "mismatch": null,
      "checked_at": "2026-09-19T04:00:00Z"
    },
    "online": {
      "status": "online",
      "label": "Online",
      "reason": null,
      "since": null,
      "checked_at": "2026-09-19T20:00:00Z",
      "text": "Homepage answered normally (last check ...)."
    }
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`

## Delete a site

`DELETE /sites/{site_id}` · scope `sites:delete` · **destructive**

Deletes the site for good (files, database, DNS zone, CDN), exactly like Delete site in the panel. Refused while the site is busy or paused for non-payment/malware, while it has subdomain sites, or while a blueprint is being made from it. Runs in the background: a job is returned.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123" \
  -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/sites/123", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`202`

```json
{
  "data": {
    "id": 123,
    "deleting": true
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`
