Sites
Every site type: list, search, create, change, delete; status, nameservers, CDN, SEO.
Malware scan results
GET/api/v1/sites/{site_id}/malwareScope sites:read
What the nightly malware scan (and any "scan again") found on this site, in the same words as the site's Malware scan tab: open findings, files in quarantine, and the date the site would be paused if the findings stay. Files identical to the official published copies are never listed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
site_id | path | integer | yes | The site id (see GET /sites). |
Example
curl -s "https://app.pbn.ltd/api/v1/sites/123/malware" \
-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/sites/123/malware", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/malware", {
method: "GET",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
200
{
"data": {
"flagged": true,
"paused_for_malware": false,
"pause_on": "2026-09-28",
"open": [
{
"id": "log-771",
"path": "wp-content/uploads/x.php",
"kind": "Suspicious code",
"title": "Code that looks like malware",
"first_seen": "2026-09-20T02:10:00Z",
"last_seen": "2026-09-21T02:10:00Z",
"signatures": [
"{HEX}php.cmdshell.remview.401"
]
}
],
"being_reviewed": 0,
"quarantined": [
{
"id": 12,
"path": "wp-content/uploads/x.php",
"since": "2026-09-21T09:00:00Z"
}
],
"cleared_as_official_copies": 3,
"last_nightly_scan": "2026-09-21T03:47:00Z",
"last_rescan": {
"state": "done",
"finished_at": "2026-09-21T09:05:00Z"
}
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized
Scan the site again
POST/api/v1/sites/{site_id}/malware/rescanScope sites:write
Starts a malware scan of this site now (usually one to five minutes). Poll GET /sites/{site_id}/malware for last_rescan. One scan per site at a time, ten per account an hour.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
site_id | path | integer | yes | The site id (see GET /sites). |
Example
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/malware/rescan" \
-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/sites/123/malware/rescan", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/malware/rescan", {
method: "POST",
headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
Response
202
{
"data": {
"state": "pending"
}
}
Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed
List and search sites
GET/api/v1/sitesScope 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
curl -s "https://app.pbn.ltd/api/v1/sites?search=blog&state=ok&limit=20" \
-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/sites?search=blog&state=ok&limit=20", headers=headers, timeout=120)
print(r.status_code, r.json())
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
{
"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",
"state_label": "Ok",
"dns_managed_elsewhere": false,
"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/api/v1/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
curl -s "https://app.pbn.ltd/api/v1/sites/123" \
-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/sites/123", headers=headers, timeout=120)
print(r.status_code, r.json())
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
{
"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",
"state_label": "Ok",
"dns_managed_elsewhere": false,
"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": "[email protected]",
"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/api/v1/sites/optionsScope 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
curl -s "https://app.pbn.ltd/api/v1/sites/options" \
-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/sites/options", headers=headers, timeout=120)
print(r.status_code, r.json())
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
{
"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/api/v1/sitesScope 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 | Retired and ignored: e-mail accounts at your domain are created on the site's Mail tab. |
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. |
install_woocommerce | body | boolean | no | WordPress only: once the site is built, install WooCommerce and set the shop up (shop, cart, checkout and account pages, store open, pretty permalinks). Free. Ignored for other site types. Default: False. |
Example
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": "[email protected]", "feedback_email": "[email protected]"}'
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": "[email protected]", "feedback_email": "[email protected]"}, timeout=120)
print(r.status_code, r.json())
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": "[email protected]", "feedback_email": "[email protected]"})
});
console.log(res.status, await res.json());
Response
202
{
"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/api/v1/sites/{site_id}/optionsScope 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
curl -s "https://app.pbn.ltd/api/v1/sites/123/options" \
-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/sites/123/options", headers=headers, timeout=120)
print(r.status_code, r.json())
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
{
"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/api/v1/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 | Retired and ignored: e-mail accounts at your domain are created on the site's Mail tab. |
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
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}'
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())
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
{
"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",
"state_label": "Ok",
"dns_managed_elsewhere": false,
"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/api/v1/sites/{site_id}Scope sites:delete · destructive
Deletes the site for good (files, database, DNS zone, CDN), exactly like Delete site in the panel. It ALSO deletes the email of the site's domain on our mail service - every mailbox and forwarder and all their mail - when it has any: read GET /sites/{site_id}/mail first and tell the person, with the counts, before deleting; the response repeats it in email_deleted and warning. 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
curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123" \
-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/sites/123", headers=headers, timeout=120)
print(r.status_code, r.json())
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
{
"data": {
"id": 123,
"deleting": true,
"email_deleted": {
"domain": "example.com",
"mailboxes": 2,
"forwarders": 1
},
"warning": "This also deletes the email for example.com: 2 mailboxes and 1 forwarder, and all their mail - this cannot be undone."
}
}
Errors: conflict, not_found, rate_limited, scope_missing, unauthorized