PBN.LTD API docs
View as Markdown

Sites

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

Malware scan results

GET/api/v1/sites/{site_id}/malware

Scope 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

NameInTypeRequiredDescription
site_idpathintegeryesThe 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/rescan

Scope 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

NameInTypeRequiredDescription
site_idpathintegeryesThe 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/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

NameInTypeRequiredDescription
searchquerystringnoPart of the domain or name (www. is ignored).
statequerystring (one of: ok, waiting, activating, working, error, frozen, notinstalled)nook = live, waiting = waiting for DNS, activating = activating CDN, working = being installed/changed, error, frozen = paused, notinstalled.
typequerystring (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki, mismatch)noSite type; "mismatch" = the files run a different platform than the site type.
groupquerystringnoGroup id, or "none" for sites in no group.
cdnquerystringnoCDN name, e.g. Cloudflare, BunnyCDN, KeyCDN, CDN77.COM, Gcore, CloudFront.
php_versionquerystringnoPHP version value, e.g. "PHP 8.3".
indexedquerystring (one of: yes, no, pending)noGoogle indexation state.
onlinequerystring (one of: online, offline)noThe online badge: offline = a confirmed problem.
sortquerystring (one of: newest, oldest, name, name_desc, domain, domain_desc)noOrder of the list. Default: newest.
limitqueryintegernoItems per page (1-200). Default: 50.
cursorquerystringnoThe 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

NameInTypeRequiredDescription
site_idpathintegeryesThe 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/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

NameInTypeRequiredDescription
typequerystring (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki)noDescribe 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/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

NameInTypeRequiredDescription
typebodystring (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki)yesSite type.
namebodystringyesA unique short name (letters, digits, dashes).
domainbodystringyesThe domain (or subdomain of one of your sites).
cdnbodystringnoCDN (see GET /sites/options for the choices open to you).
php_versionbodystringnoPHP version value, e.g. "PHP 8.3" (see GET /sites/options).
use_httpsbodybooleannoServe over HTTPS.
use_wwwbodybooleannoUse www. as the primary host.
titlebodystringnoSite title (WordPress and the ready-installed applications). REQUIRED when type is Wordpress.
subtitlebodystringnoTagline. REQUIRED when type is Wordpress; ignored for other types.
admin_emailbodystringnoAdministrator e-mail (WordPress and the applications).
feedback_emailbodystringnoContact-form e-mail address, where the site's contact form sends its messages. REQUIRED when type is Wordpress; ignored for other types.
login_urlbodystringnoWordPress login path (default wp-login.php). REQUIRED when type is Wordpress.
template_idbodyintegernoWordPress template id (omit for random).
blueprint_idbodyintegernoDeploy from one of your blueprints (WordPress).
group_idbodyintegernoPut the site in this group.
create_mailboxbodybooleannoRetired and ignored: e-mail accounts at your domain are created on the site's Mail tab.
autoupdate_wordpressbodybooleannoAuto-update WordPress core and plugins.
form_fieldsbodyobjectnoAny other Create site form field by its form name (see form_field in GET /sites/options), e.g. the WordPress theme/plugin pickers.
install_woocommercebodybooleannoWordPress 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}/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

NameInTypeRequiredDescription
site_idpathintegeryesThe 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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
namebodystringnoA unique short name (letters, digits, dashes).
domainbodystringnoThe domain (or subdomain of one of your sites).
php_versionbodystringnoPHP version value, e.g. "PHP 8.3" (see GET /sites/options).
use_httpsbodybooleannoServe over HTTPS.
use_wwwbodybooleannoUse www. as the primary host.
admin_emailbodystringnoAdministrator e-mail (WordPress and the applications).
login_urlbodystringnoWordPress login path (default wp-login.php). REQUIRED when type is Wordpress.
group_idbodyintegernoPut the site in this group.
create_mailboxbodybooleannoRetired and ignored: e-mail accounts at your domain are created on the site's Mail tab.
autoupdate_wordpressbodybooleannoAuto-update WordPress core and plugins.
form_fieldsbodyobjectnoAny other Create site form field by its form name (see form_field in GET /sites/options), e.g. the WordPress theme/plugin pickers.
ssl_modebodystring (one of: full, flexible)noCloudflare 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

NameInTypeRequiredDescription
site_idpathintegeryesThe 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