# PBN.LTD API The PBN.LTD API lets you run your hosting account from your own dashboards, scripts and AI tools: list and search sites, create sites of every type, change settings and PHP versions, manage DNS records, take and restore backups, browse and upload files, read error and access logs, check health, see billing and invoices, and open support tickets. Base URL: `https://app.pbn.ltd/api/v1`. Every answer is JSON (except file and PDF downloads), UTF-8, with dates in ISO 8601 UTC. ## Quick start 1. Create a key at [API keys](/api/keys/) (menu: your name > API keys). Copy it - it is shown once. 2. Keep it secret: store it in an environment variable, e.g. `export PBN_API_KEY=pbn_...`. 3. Call the API: ```bash curl -s https://app.pbn.ltd/api/v1/me -H "Authorization: Bearer $PBN_API_KEY" ``` ```python import os, requests r = requests.get("https://app.pbn.ltd/api/v1/sites", headers={"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}) for site in r.json()["data"]: print(site["domain"], site["state"], site["online"]["status"] if site["online"] else "-") ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites?state=frozen", { headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); const {data} = await res.json(); ``` ## What you get back A single object comes as `{"data": {...}}`; a list as `{"data": [...], "next_cursor": "...", "has_more": true}` (see [Pagination](/api/docs/pagination)). An action that finishes later also returns a `job` (see [Jobs](/api/docs/jobs)). An error is `{"error": {"code": "...", "message": "...", "details": {...}}}` with a matching HTTP status (see [Errors](/api/docs/errors)). ## Same rules as the panel The API does exactly what the panel does, through the same code: the same checks, limits and refusals apply (an expired plan, a paused or suspended site, a busy site, the site-slot limit, the ticket limits, add-ons that must be active). The API never gives more access than your account has. ## Machine-readable - OpenAPI 3.1: [https://app.pbn.ltd/api/v1/openapi.json](https://app.pbn.ltd/api/v1/openapi.json) (import it into Postman, Insomnia or a code generator). - For AI tools: [llms.txt](/api/docs/llms.txt) (index) and [llms-full.txt](/api/docs/llms-full.txt) (everything); every page is also available as Markdown by adding `.md` to its address. --- # Authentication and keys Send your key in the `Authorization` header of every request: ``` Authorization: Bearer pbn_1a2b3c4d5e6f_Xy... ``` `X-API-Key: pbn_...` works too. Keys are never accepted in the URL (they would end up in logs). ## Creating keys Open [API keys](/api/keys/) and click **Create key**. You choose: - **Name** - what the key is for ("Grafana dashboard", "Deploy script"). - **Access** - *Full access* (everything your account can do) or *Scoped*: only the permissions you tick (see [Scopes](/api/docs/scopes)). Give each tool only what it needs. - **Sites** - all sites, or only the sites you pick. A restricted key cannot see or touch any other site and cannot create sites. - **Expiry** - never, or after a number of days / on a date. - **IP allowlist** - optional: one IP address or range (CIDR) per line; calls from anywhere else get `403 ip_not_allowed`. The full key is shown **once**. We store only a hash of it: if you lose it, revoke it and make a new one. You can have many keys; each shows when and from which IP it was last used. **Revoke** stops a key at once; revoked keys move to a hidden list at the bottom of the page. ## Key format `pbn_<12 characters>_`. The first part (`pbn_1a2b3c4d5e6f`) is the key's public prefix, shown on the keys page and in the call log; the secret after it is never shown again. ## OAuth (apps and AI assistants) Apps such as AI assistants connect with OAuth 2.1 instead of a pasted key: you log in to PBN.LTD, see what the app asks for, and approve. Discovery: `https://app.pbn.ltd/.well-known/oauth-authorization-server`. Dynamic client registration, authorization code with PKCE (S256) and refresh-token rotation are supported. Connected apps are listed on the [API keys](/api/keys/) page, where you can disconnect them. See [MCP](/api/docs/mcp). ## Security tips - Never put a key in client-side code of a public web page or in a git repository. - Use a scoped key, restricted to the sites it needs, with an IP allowlist for servers with fixed addresses. - Rotate keys: create the new key, switch your tool, then revoke the old one. --- # Scopes A scoped key can do only what its scopes allow. A `:write` scope includes the `:read` scope of the same area. Full-access keys have every scope. The scope each endpoint needs is shown on its reference entry. | Scope | Area | Allows | |---|---|---| | `account:read` | Account | Profile, plan, site slots, limits and usage, frozen state and why. | | `account:write` | Account | Refresh usage measurements. | | `billing:read` | Billing | Subscription status, payment due, invoices and invoice PDFs, add-ons. | | `sites:read` | Sites | List and read sites: status, nameservers, CDN, indexation, SEO, platform, usage, health. | | `sites:write` | Sites | Create sites, change settings and PHP version, purge cache, reinstall, recheck health, run Site Cleaner. | | `sites:delete` | Sites | Delete sites (kept separate on purpose). | | `sites:login` | Sites | One-click admin login links (WordPress, Joomla, Drupal and the other applications). | | `dns:read` | DNS | Read DNS records and nameserver status. | | `dns:write` | DNS | Add, change and delete DNS records. | | `backups:read` | Backups | List backups and get download links. | | `backups:write` | Backups | Create backups, restore a backup, delete a backup. | | `files:read` | Files | List folders and read files of a site. | | `files:write` | Files | Upload, create folders, rename and delete files of a site. | | `logs:read` | Logs | Error log and access log of a site. | | `tickets:read` | Support | Read your support tickets. | | `tickets:write` | Support | Open tickets and reply to them. | | `content:read` | Content | Read posts, pages and the auto-posting campaigns of the account. | | `content:write` | Content | Publish and change posts and pages on any site type, upload pictures, and start or pause auto-posting campaigns. | | `kb:read` | Knowledge base | Search the knowledge base (needs no scope; listed for completeness). | A call without the scope gets `403 scope_missing`, with `details.required` naming the scope it needs. Jobs (`GET /jobs`) need `account:read`; the knowledge base and `GET /limits` work with any key. --- # Rate limits and sizes Every key has its own limits: | Limit | Default | |---|---| | Requests per minute | 300 | | Requests per hour | 10000 | | Write requests (POST, PUT, PATCH, DELETE) per minute | 90 | | File upload size | 25 MB | | File read size | 5 MB | | Active keys per account | 50 | A bulk upload counts as ONE write request however many files are in it, so the archive upload (POST on `/sites//files/archive`) is the way to put a whole built site up - not one file write per file. Every answer carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix time the minute window ends). Over the limit you get `429 rate_limited` with `Retry-After` (seconds): wait that long and retry. `GET /limits` returns the limits of the key making the call, and says whether they are the standard ones: `overridden` is `true` (with `override_scope` set to `key` or `account`) when support has adjusted them for you. ## Need a higher limit? These limits suit almost every dashboard, script and AI assistant. If your tool genuinely needs more - a large account, a migration, a nightly sync over thousands of sites - [ask support](https://app.pbn.ltd/support/), say what you are building and roughly how many calls a minute you need. We can raise the limit on a single key or on the whole account, and it applies to your next call - nothing to change in your code. Some actions also have the panel's own limits, whatever the key: measuring usage (one per site every few minutes), error logs (20 fetches per 10 minutes per account), access logs (30 per 10 minutes), health re-checks (10 an hour), integrity checks and repairs (12 per 10 minutes per site), admin login links (a few per minute). ## Good practice - Cache what does not change often; use `ETag` / `If-None-Match` on GET requests: an unchanged answer is `304 Not Modified` with no body. - Poll jobs every 5-10 seconds, not faster. - On `503 unavailable` (a server did not answer) retry after a short wait. --- # Pagination List endpoints return one page at a time: ```json {"data": [ ... ], "next_cursor": "eyJvIjo1MCwicyI6Im5ld2VzdCJ9", "has_more": true, "total": 312} ``` Ask for the next page with `?cursor=` (keep the same filters). `has_more` is false and `next_cursor` is null on the last page. `limit` sets the page size (1-200, default 50). `total` (where given) is the number of matching items. ```python url, params, sites = "https://app.pbn.ltd/api/v1/sites", {"limit": 200}, [] while True: page = requests.get(url, headers=headers, params=params).json() sites += page["data"] if not page["has_more"]: break params["cursor"] = page["next_cursor"] ``` --- # Errors Errors have one shape: ```json {"error": {"code": "validation_failed", "message": "Some fields are not valid.", "details": {"fields": {"domain": ["Site with this Domain already exists."]}}}} ``` | HTTP | code | When | |---|---|---| | 400 | `bad_request` | The request is malformed (bad JSON, unknown parameter value). | | 401 | `unauthorized` | No key, a malformed key, an unknown key, a revoked key or an expired key. | | 402 | `payment_required` | The plan has expired, or the feature needs an add-on the account does not have. | | 403 | `forbidden` | The key lacks the scope, is restricted to other sites, the IP is not on its allowlist, or the account / site is suspended. | | 403 | `ip_not_allowed` | The request came from an IP address that is not on the key's allowlist. | | 403 | `scope_missing` | The key does not have the scope this endpoint needs (see details.required). | | 403 | `site_not_allowed` | The key is restricted to other sites. | | 404 | `not_found` | The object does not exist or does not belong to this account. | | 409 | `conflict` | The site is busy (another job is running) or not in a state that allows the action. | | 413 | `too_large` | An upload or a file read is larger than the limit. | | 422 | `validation_failed` | Some fields are invalid; details.fields lists the message(s) for each field. | | 429 | `rate_limited` | Too many requests for this key; see Retry-After. | | 500 | `internal_error` | Something failed on our side. It is logged; the request_id helps support find it. | | 503 | `api_disabled` | The API is switched off for maintenance. | | 503 | `unavailable` | A server this call depends on did not answer; try again in a few minutes. | Every answer has an `X-Request-Id` header. If you contact support about a failed call, quote it. Validation messages are the same messages the panel shows for the same form. --- # Jobs Creating a site, deleting one, reinstalling, restoring or making a backup, changing a DNS record, re-checking health and Site Cleaner runs take seconds to minutes. Those calls answer at once (usually `202 Accepted`) with a `job`: ```json {"data": {"id": 88001, "state": "Pending"}, "job": {"id": "job_4f1c0a9e2b7d6c5a3e10", "kind": "backup.create", "status": "running", "url": "https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10"}} ``` Poll `GET /jobs/{job_id}` until `status` is `succeeded` or `failed`; `message` says what happened. The status is read live from the site or backup itself, so it is always what the panel shows. | kind | finished when | |---|---| | `site.create` | the site is installed (Ok, or waiting for its nameservers) | | `site.update` | the server change is applied (PHP version, domain, HTTPS/www) | | `site.reinstall`, `site.restore` | the site is back up | | `site.delete` | the site is gone | | `backup.create` | the backup is Ok | | `dns.publish` | the records were published at the DNS provider | | `health.recheck` | the home page was probed again | | `cleaner.scan`, `cleaner.clean`, `cleaner.undo` | Site Cleaner finished | A job with no result after 12 hours is marked failed. --- # MCP (AI assistants) Connect Claude, ChatGPT, Cursor, VS Code, Windsurf, Gemini CLI or any other MCP client to your PBN.LTD account: ask in plain words ("which of my sites are offline?", "add a TXT record to example.com", "why is my site paused?") and the assistant uses the same operations as this API, with the same permissions and limits. **Server URL:** `https://app.pbn.ltd/mcp` (live) Authentication: the assistant opens a PBN.LTD login and asks you to approve access (OAuth). Clients that do not support OAuth can send an API key instead: `Authorization: Bearer pbn_...` (use a scoped key). Everything the assistant does is in the call log of that key/app, and you can disconnect it at any time on [API keys](/api/keys/). ## Claude (claude.ai and Claude Desktop) 1. Settings > **Connectors** (or Customize > Connectors) > **+** > **Add custom connector**. 2. Name: `PBN.LTD`; URL: `https://app.pbn.ltd/mcp`. Click **Add**, then **Connect** and approve on PBN.LTD. Vendor guide: [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp) and [Connect to remote MCP servers](https://modelcontextprotocol.io/docs/develop/connect-remote-servers). ## Claude Code ```bash claude mcp add --transport http pbn https://app.pbn.ltd/mcp # then run /mcp in Claude Code, pick "pbn" and log in; or use a key: claude mcp add --transport http pbn https://app.pbn.ltd/mcp --header "Authorization: Bearer $PBN_API_KEY" ``` Vendor guide: [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp). ## ChatGPT 1. Settings > Security and login: switch on **Developer mode**. 2. Open Plugins (Apps) > **+**, give it the name PBN.LTD, choose the public endpoint option and enter `https://app.pbn.ltd/mcp`. 3. Approve access on PBN.LTD when asked. Vendor guides: [MCP in ChatGPT](https://developers.openai.com/api/docs/mcp/) and [Connect a server to ChatGPT](https://developers.openai.com/plugins/deploy/connect-chatgpt). ## Cursor Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json {"mcpServers": {"pbn": {"url": "https://app.pbn.ltd/mcp"}}} ``` To use a key instead of OAuth: `{"mcpServers": {"pbn": {"url": "https://app.pbn.ltd/mcp", "headers": {"Authorization": "Bearer pbn_..."}}}}`. Vendor guide: [Model Context Protocol (MCP) in Cursor](https://cursor.com/docs/context/mcp). ## VS Code (GitHub Copilot) Command Palette > **MCP: Add Server** > HTTP, URL `https://app.pbn.ltd/mcp` - or in `.vscode/mcp.json`: ```json {"servers": {"pbn": {"type": "http", "url": "https://app.pbn.ltd/mcp"}}} ``` Vendor guide: [Add and manage MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers). ## Windsurf In Cascade's MCP settings (`mcp_config.json`): ```json {"mcpServers": {"pbn": {"serverUrl": "https://app.pbn.ltd/mcp"}}} ``` Vendor guide: [Cascade MCP](https://docs.windsurf.com/windsurf/cascade/mcp). ## Gemini CLI ```bash gemini mcp add --transport http pbn https://app.pbn.ltd/mcp # or with a key: gemini mcp add --transport http --header "Authorization: Bearer $PBN_API_KEY" pbn https://app.pbn.ltd/mcp ``` Vendor guide: [MCP servers with Gemini CLI](https://geminicli.com/docs/tools/mcp-server/). ## Other MCP clients Any client that supports remote MCP servers over Streamable HTTP works: give it `https://app.pbn.ltd/mcp` and either let it do the OAuth login or send an `Authorization: Bearer pbn_...` header. About MCP: [modelcontextprotocol.io](https://modelcontextprotocol.io/). ## What the assistant can do The tools mirror the API reference one to one (tool `sites_list` = `GET /sites`, `dns_create` = `POST /sites/{site_id}/dns`, ...). Actions that delete or overwrite (delete a site, restore, reinstall, delete files) are marked destructive, so the assistant asks you before running them. A scoped key or a limited OAuth approval limits the assistant exactly like any other key. ## What the server is | | | |---|---| | Server URL | `https://app.pbn.ltd/mcp` (POST only) | | Transport | Streamable HTTP | | Protocol revisions | `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26` | | Tools | 85 | | Resources | 4 fixed, 5 templates, one per docs page | | Prompts | 12 | | Authentication | OAuth 2.1 (PKCE, dynamic client registration) or an API key as `Authorization: Bearer pbn_...` | | Server name / version | `pbn.ltd` 1.0.0 | The endpoint serves both shapes of the protocol on the same URL. Revision `2026-07-28` made MCP stateless (no `initialize` handshake, no sessions, per-request metadata, `server/discover`); revisions `2025-11-25`, `2025-06-18` and `2025-03-26` use the `initialize` handshake. You do not have to care which one your client speaks - it is detected from the request. Because the server holds no session, `GET` and `DELETE` on `https://app.pbn.ltd/mcp` answer `405`, no `Mcp-Session-Id` is issued, and streams are not resumable. Nothing is lost by that: every answer is complete in its own response. ## Authentication Two ways in. Both end up as the same thing: a key with scopes, an owner, a call log and a rate limit. **1. OAuth 2.1 (what a connector does for you).** Point the client at `https://app.pbn.ltd/mcp` and it will find the login by itself: the first call answers `401` with ``` WWW-Authenticate: Bearer realm="PBN.LTD", resource_metadata="https://app.pbn.ltd/.well-known/oauth-protected-resource" ``` From there the client reads its [protected-resource metadata](https://app.pbn.ltd/.well-known/oauth-protected-resource) (RFC 9728), then the [authorization-server metadata](https://app.pbn.ltd/.well-known/oauth-authorization-server) (RFC 8414), registers itself (RFC 7591 dynamic client registration), and opens `https://app.pbn.ltd/oauth/authorize` in your browser. You log in as yourself, see exactly which permissions the app is asking for, and approve or refuse. PKCE with `S256` is required, the `resource` parameter (RFC 8707) is honoured, authorization codes last 60 seconds, access tokens 1 hour and refresh tokens 90 days with rotation. **2. An API key.** Create one at [API keys](https://app.pbn.ltd/api/keys/), give it only the permissions the assistant needs, and put it in the client's configuration as the `Authorization: Bearer pbn_...` header. Use this for clients that have no OAuth support, and for anything running unattended. Either way you can end the connection at any moment: revoke the key (or the app) on the [API keys](https://app.pbn.ltd/api/keys/) page and the next call fails. A key created through delegated access also dies the moment that access is withdrawn. ## What you can ask for The tools cover the whole job, not only reading: - **"Build me a site."** Create a site of any type, upload a built site as a zip, set its home page, then fetch the pages and prove they really serve. - **"Publish this post."** Write a post or a page and put it on the site - WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki, and Static HTML / PHP hosting too (those get a page built in the look of the site's own pages, with a blog index and `sitemap.xml` kept up to date). Pictures upload with it. - **"Upload these images."** Straight into the WordPress media library, or into the site's own files on the other types, and you get the address back to use in a page. - **"Write me a plugin - and test it."** Write the files into the site, run PHP's own syntax check over them, switch the plugin on, fetch the page, read the PHP error log, and switch it off again if anything broke. Themes work the same way. - **"Keep the blog going."** Look at the AI auto-posting campaigns, pause or resume one, or ask one to write now. - **"Why is this site down?"** Health checks, the error log, the access log, usage, nameservers and DNS. - **"What do I owe?"** Plan, invoices, payment due, add-ons. - **"Publish it to Netlify as well."** The sites you deploy to third-party hosts, and a redeploy or a rollback. Everything is done with the account's own permissions, and anything that replaces or deletes is marked destructive so your assistant asks you first. ## Permissions (scopes) An assistant can only do what the connection was granted. `tools/list` returns **only the tools the connection may actually use**, so the assistant never offers something it cannot do. Ask for the narrowest set that does the job: - `account:read` - Profile, plan, site slots, limits and usage, frozen state and why. - `account:write` - Refresh usage measurements. - `billing:read` - Subscription status, payment due, invoices and invoice PDFs, add-ons. - `sites:read` - List and read sites: status, nameservers, CDN, indexation, SEO, platform, usage, health. - `sites:write` - Create sites, change settings and PHP version, purge cache, reinstall, recheck health, run Site Cleaner. - `sites:delete` - Delete sites (kept separate on purpose). - `sites:login` - One-click admin login links (WordPress, Joomla, Drupal and the other applications). - `dns:read` - Read DNS records and nameserver status. - `dns:write` - Add, change and delete DNS records. - `backups:read` - List backups and get download links. - `backups:write` - Create backups, restore a backup, delete a backup. - `files:read` - List folders and read files of a site. - `files:write` - Upload, create folders, rename and delete files of a site. - `logs:read` - Error log and access log of a site. - `tickets:read` - Read your support tickets. - `tickets:write` - Open tickets and reply to them. - `content:read` - Read posts, pages and the auto-posting campaigns of the account. - `content:write` - Publish and change posts and pages on any site type, upload pictures, and start or pause auto-posting campaigns. - `kb:read` - Search the knowledge base (needs no scope; listed for completeness). A `:write` scope includes the matching `:read`. `sites:delete` and `sites:login` are deliberately separate from `sites:write`, so an assistant can be allowed to change a site without being able to delete it or to log in as its administrator. If a tool needs a permission the connection does not have, an OAuth connection is asked to step up (`403` with `WWW-Authenticate: ... error="insufficient_scope"`), and an API-key connection gets a tool error that names the missing scope. ## Limits MCP calls share the API's limits exactly - they are the same requests. The defaults are **300 requests a minute**, **10000 an hour** and **90 writes a minute** per key or per connected app, with `X-RateLimit-*` headers on the answer; see [Rate limits and sizes](https://app.pbn.ltd/api/docs/rate-limits). They were set for exactly this kind of work: an assistant building a site or writing a plugin makes a burst of small writes. A bulk upload counts as **one** request however many files are inside it, so `files_upload_archive` is the way to put a built site up, not one `files_write` per file. A tool that hits the limit returns a tool error saying how long to wait - a well-behaved assistant waits rather than retrying at once. File uploads are limited to 25 MB and file reads to 5 MB (the exact numbers for your key are in the `meta_limits` tool). Every tool call is written to the call log of the key that made it, with the site it touched, and write calls keep what was asked for as an audit trail. You can read it on your key's activity page. ## Tools 85 tools, one for every operation of the REST API plus `jobs_wait`. The complete reference - every argument, what comes back, and a worked example for each - is on the [MCP tool reference](https://app.pbn.ltd/api/docs/mcp-tools) page. - **Account** - 4 tools - **Billing** - 3 tools - **Sites** - 7 tools - **Site tools** - 14 tools - **DNS records** - 5 tools - **Backups** - 6 tools - **Files** - 7 tools - **Logs** - 2 tools - **Support tickets** - 5 tools - **Knowledge base** - 3 tools - **Jobs** - 3 tools - **Content and publishing** - 13 tools - **Building, plugins and themes** - 13 tools Tools are named after the API endpoints with dots replaced by underscores, so `sites_list` is `GET /sites` and `dns_create` is `POST /sites/{site_id}/dns` (paths relative to `https://app.pbn.ltd/api/v1`). Each one carries a JSON-Schema for its input **and** for its output, so a client can check the answer, and behaviour annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so your assistant knows what needs your say-so first. Answers carry `structuredContent` (the JSON the API would return) as well as a text block, so an assistant can work with the data rather than re-reading prose. ### Things that finish later Creating a site, taking or restoring a backup, deleting a site, reinstalling and publishing DNS all start a job. The tool answers immediately with a `job` block; `jobs_wait` then waits for it (up to 5 minutes at a time) and `jobs_get` / `jobs_list` show it on demand. If the client sends a `progressToken`, those calls stream `notifications/progress` while they wait, so nothing looks frozen. ## Resources Read-only context an assistant can pull in without spending a tool call: | URI | What it holds | |---|---| | `pbn://account` | Who this connection belongs to and what the connected key may do. | | `pbn://account/limits` | Site slots, subscription state, the default per-site limits, paused sites and the add-ons on the account. | | `pbn://billing` | Subscription, payment due, next renewal and the add-ons that are being paid for. | | `pbn://sites` | Every site on the account with its status, type, platform and nameserver state. | | `pbn://site/{site_id}` | One site in full: status, type, CDN, HTTPS, PHP version, nameservers, SEO. | | `pbn://site/{site_id}/health` | Is the site up, what the last checks found and what is wrong. | | `pbn://site/{site_id}/dns` | The DNS records published for this site. | | `pbn://invoice/{number}` | One invoice as a PDF. | | `pbn://docs/{slug}` | A page of the PBN.LTD API / MCP documentation as Markdown. Slugs: see pbn://docs/index. | `pbn://docs/index` lists a `pbn://docs/` resource for every page of this documentation, so an assistant can read the API reference itself before answering a question about it. ## Prompts Ready-made jobs. In Claude they appear under the **+** menu (or as `/` commands) once the connector is added; other clients show them in a command palette. | Prompt | What it does | |---|---| | **Diagnose a site** (`diagnose_site`) | Work out why a site is down, slow, showing an error or paused, and say what to do about it. | | **Check a site is ready to go live** (`going_live_check`) | Check the nameservers, DNS records, CDN and HTTPS of a site that is waiting to go live, and give the customer the exact steps left. | | **Create a new site** (`create_site`) | Create a site of any type (WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki, PHP hosting or static HTML) and follow it through to ready. | | **Clean up a site** (`clean_up_site`) | Use Site Cleaner to find what is filling a site up, and clean it after showing the customer what would go. | | **Review my account** (`account_review`) | A plain-language review of the whole account: plan, what is owed, slots, usage, paused sites, sites that are not live and anything that needs attention. | | **Answer a billing question** (`billing_questions`) | Answer a question about the plan, an invoice, a payment or a renewal from the real billing record, with the invoice links. | | **Read the error log and fix it** (`fix_from_error_log`) | Read a site's error log, explain the errors in plain words and propose the safest fix (never changing a file without asking first). | | **Back up or restore a site** (`backup_and_restore`) | Take a backup now, or restore one - with the confirmation and the warnings a restore needs. | | **Build me a site** (`build_site`) | Create a site of any type, put the pages on it, set the home page and check that it really serves - end to end. | | **Write and publish a post** (`publish_post`) | Write a post or page and publish it to a site of ANY type, with its pictures, and check the published page. | | **Make me a plugin** (`make_plugin`) | Write a WordPress plugin (or theme) into the site, check it, switch it on and prove the site still works - and take it back off if it does not. | | **Find help / open a ticket** (`get_help`) | Search the PBN.LTD knowledge base for the customer's question and, only if it is not answered there, open a support ticket with the right detail in it. | ## Try it without a client Everything below works with a plain API key. Replace `$PBN_API_KEY` with a key from [API keys](https://app.pbn.ltd/api/keys/). List the tools: ```bash curl -s https://app.pbn.ltd/mcp \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' ``` Call one (this one is free of side effects - it is the cheapest way to prove a key works): ```bash curl -s https://app.pbn.ltd/mcp \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"account_me","arguments":{}}}' ``` Search your sites: ```bash curl -s https://app.pbn.ltd/mcp \ -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"sites_list","arguments":{"search":"example.com","limit":5}}}' ``` Read a resource: ```bash curl -s https://app.pbn.ltd/mcp \ -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"pbn://account"}}' ``` The handshake a client does first, if you want to see it (revision `2025-11-25`): ```bash curl -s https://app.pbn.ltd/mcp \ -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25", "capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' ``` …and the same thing on revision `2026-07-28`, which needs no handshake at all: ```bash curl -s https://app.pbn.ltd/mcp \ -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" \ -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{ "io.modelcontextprotocol/protocolVersion":"2026-07-28", "io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"}, "io.modelcontextprotocol/clientCapabilities":{}}}}' ``` On `2026-07-28` the `MCP-Protocol-Version`, `Mcp-Method` and (for `tools/call`, `resources/read` and `prompts/get`) `Mcp-Name` headers must agree with the body, or the answer is `400` with JSON-RPC error `-32020`. An unsupported version is `400` with `-32022` and the list of versions we do support; an unknown method is `404` with `-32601`. ## More clients Anything that speaks remote MCP over Streamable HTTP works. A few more, with their own documentation: - **Zed** - Settings > AI > MCP Servers > Add Server > Add Remote Server (`context_servers` in `settings.json`, a `url` and optional `headers`): [MCP in Zed](https://zed.dev/docs/ai/mcp). - **MCP Inspector** (the reference client, for checking a connection): [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector). - Anything else: point it at `https://app.pbn.ltd/mcp` and let it do the OAuth login, or give it the `Authorization: Bearer pbn_...` header. The protocol itself: [modelcontextprotocol.io](https://modelcontextprotocol.io/). ## Checking the connection yourself The reference client from the MCP project lists the tools and calls one without any assistant in the way: ```bash npx @modelcontextprotocol/inspector --cli https://app.pbn.ltd/mcp --transport http --header "Authorization: Bearer $PBN_API_KEY" --method tools/list npx @modelcontextprotocol/inspector --cli https://app.pbn.ltd/mcp --transport http --header "Authorization: Bearer $PBN_API_KEY" --method tools/call --tool-name account_me --format json ``` Leave out `--cli` for the graphical version in a browser, which also walks through the OAuth login. ## When something is wrong | What you see | What it means | |---|---| | `401` with `WWW-Authenticate` | No key, a revoked or expired key, or a token issued for something else. Reconnect, or check the key on the API keys page. | | `403` `insufficient_scope` | The connection was not granted that permission. An OAuth client will ask you to approve it; a key has to be edited. | | A tool answer with `isError` | The call reached us and was refused or failed - the text says why and what to do (wrong site id, site busy, plan expired, over a limit). | | `429` / a rate-limit tool error | Too many calls for this key in the last minute or hour. Wait the seconds it names. | | `503` `mcp_disabled` | The MCP server is switched off for maintenance. The REST API is unaffected. | ## What it cannot do - It never sees your PBN.LTD password, and it cannot change your password, your e-mail address or your payment method. - It cannot reach another customer's account, and a key restricted to certain sites cannot see the others. - It cannot spend money: nothing here buys a plan, an add-on or a domain. - It can delete sites, files and backups if you grant those permissions - grant them only if you mean to, and keep a backup. --- # MCP tool reference The 85 tools the PBN.LTD MCP server (`https://app.pbn.ltd/mcp`) offers, in the order `tools/list` returns them. Each one is the same operation as the REST endpoint named beside it (paths relative to `https://app.pbn.ltd/api/v1`), with the same permission, the same limits and the same audit trail - see the [API reference](https://app.pbn.ltd/api/docs) for the full description of what each field means. A connection only sees the tools its permissions allow. "Kind" is the behaviour annotation the tool carries: **read** never changes anything, **write** does, and **DESTRUCTIVE** replaces or deletes something that cannot be brought back. ## All tools | Tool | What it does | Permission | Kind | |---|---|---|---| | [account_limits](https://app.pbn.ltd/api/docs/mcp-tools#account-limits) | Site slots (plan + extra-site add-ons), subscription state and paid-until date, the default per-site limits, every paused (frozen) site with the re… | `account:read` | read | | [account_me](https://app.pbn.ltd/api/docs/mcp-tools#account-me) | The account and the key making the call. The cheapest call there is: use it to test a key. | `account:read` | read | | [account_usage_refresh](https://app.pbn.ltd/api/docs/mcp-tools#account-usage-refresh) | Starts a fresh disk / database / file-count measurement for the account's sites - the usage tab's "Check now", for many sites at once. It returns a… | `account:write` | write | | [meta_limits](https://app.pbn.ltd/api/docs/mcp-tools#meta-limits) | The rate limits and size limits that apply to the key making the call. `overridden` is true when support has raised or lowered a limit for this key… | `kb:read` | read | | [billing_invoice_pdf](https://app.pbn.ltd/api/docs/mcp-tools#billing-invoice-pdf) | The PDF of an invoice or credit note of this account (Content-Type application/pdf). | `billing:read` | read | | [billing_invoices](https://app.pbn.ltd/api/docs/mcp-tools#billing-invoices) | Every payment and refund of the account, newest first. `document` is the invoice / credit note when one has been issued; download it with GET /bill… | `billing:read` | read | | [billing_summary](https://app.pbn.ltd/api/docs/mcp-tools#billing-summary) | Subscription (state, plan, paid-until date, days left, payment method), unpaid state with the date sites are removed if it stays unpaid, site slots… | `billing:read` | read | | [sites_create](https://app.pbn.ltd/api/docs/mcp-tools#sites-create) | 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… | `sites:write` | write | | [sites_create_options](https://app.pbn.ltd/api/docs/mcp-tools#sites-create-options) | The fields the Create site form accepts for this account right now, with their choices (site types, CDNs, PHP versions, templates, blueprints, grou… | `sites:write` | read | | [sites_delete](https://app.pbn.ltd/api/docs/mcp-tools#sites-delete) | 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… | `sites:delete` | DESTRUCTIVE | | [sites_edit_options](https://app.pbn.ltd/api/docs/mcp-tools#sites-edit-options) | The fields the Edit site form offers for THIS site right now (they depend on type, CDN and state), with choices and current values. | `sites:write` | read | | [sites_get](https://app.pbn.ltd/api/docs/mcp-tools#sites-get) | Everything the site page shows: state, URL, settings, why it is paused (freeze.reasons), nameserver status (current vs required, pointed, autopilot… | `sites:read` | read | | [sites_list](https://app.pbn.ltd/api/docs/mcp-tools#sites-list) | 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 see… | `sites:read` | read | | [sites_update](https://app.pbn.ltd/api/docs/mcp-tools#sites-update) | Change any setting the Edit site form offers: name, domain, PHP version, HTTPS, www, group, admin e-mail, login URL, auto-updates, mailbox. Send on… | `sites:write` | write | | [cleaner_get](https://app.pbn.ltd/api/docs/mcp-tools#cleaner-get) | Site Cleaner for this site: whether the add-on is active, the last scan (what can be removed and how much it saves), the running job, recent jobs (… | `sites:read` | read | | [cleaner_run](https://app.pbn.ltd/api/docs/mcp-tools#cleaner-run) | Runs Site Cleaner. `scan` lists unused plugins/themes, junk files and database clutter (free). `clean` removes the selected items after taking a ba… | `sites:write` | write | | [cleaner_schedule](https://app.pbn.ltd/api/docs/mcp-tools#cleaner-schedule) | Sets automatic cleaning for the site (needs the add-on to actually clean). | `sites:write` | write | | [health_get](https://app.pbn.ltd/api/docs/mcp-tools#health-get) | The online badge (online / offline / slow / paused... with the reason) and, for application sites, the last integrity check (V1 config lines, admin… | `sites:read` | read | | [health_integrity_check](https://app.pbn.ltd/api/docs/mcp-tools#health-integrity-check) | Checks the application now (WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki): V1's config lines, the site URL, the admin account,… | `sites:write` | write | | [health_recheck](https://app.pbn.ltd/api/docs/mcp-tools#health-recheck) | Probes the home page again now (same probe and recording as the platform's monitor) and asks the server why if it fails. Asynchronous: follow the j… | `sites:write` | write | | [health_repair](https://app.pbn.ltd/api/docs/mcp-tools#health-repair) | "Repair site": checks, repairs everything repairable (backups first, on the server), checks again. You get an e-mail listing what was repaired. | `sites:write` | write | | [sites_admin_login](https://app.pbn.ltd/api/docs/mcp-tools#sites-admin-login) | A single-use login link to the site's admin (WordPress wp-admin, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki), valid for 60 seconds - the… | `sites:login` | write | | [sites_php_version](https://app.pbn.ltd/api/docs/mcp-tools#sites-php-version) | Shortcut for PATCH /sites/{site_id} with php_version. The web server is reconfigured in the background (a job is returned). | `sites:write` | write | | [sites_purge_cache](https://app.pbn.ltd/api/docs/mcp-tools#sites-purge-cache) | Clears the CDN cache of both hostnames (as Purge CDN cache in the panel). The site must be live. | `sites:write` | write | | [sites_reinstall](https://app.pbn.ltd/api/docs/mcp-tools#sites-reinstall) | Installs the site again from scratch (as Reinstall in the panel) - the current files and database are replaced. Refused while the site is busy, swi… | `sites:write` | DESTRUCTIVE | | [sites_temp_unfreeze](https://app.pbn.ltd/api/docs/mcp-tools#sites-temp-unfreeze) | A site paused ONLY for a usage limit (disk / database / files) comes back for 30 minutes so you can clean it up (3 times per site per day). At the… | `sites:write` | write | | [sites_usage](https://app.pbn.ltd/api/docs/mcp-tools#sites-usage) | The Usage & add-ons tab: disk, database and file-count use against the limits (plan + add-ons), what is over, CDN bandwidth for metered CDNs, and t… | `sites:read` | read | | [sites_usage_refresh](https://app.pbn.ltd/api/docs/mcp-tools#sites-usage-refresh) | Starts a fresh measurement of disk, database and files (the usage tab's "Check now"). Returns the current figures with busy=true; poll GET /sites/{… | `sites:write` | write | | [dns_create](https://app.pbn.ltd/api/docs/mcp-tools#dns-create) | Adds a record with the panel's own validation. It is published to the site's DNS provider within a minute or two (follow the job); your records are… | `dns:write` | write | | [dns_delete](https://app.pbn.ltd/api/docs/mcp-tools#dns-delete) | Deletes a record; it is removed at the DNS provider within a minute or two. | `dns:write` | write | | [dns_list](https://app.pbn.ltd/api/docs/mcp-tools#dns-list) | The site's own DNS records (the DNS records tab), and the publishing state: the DNS provider, when the records were last published and any record t… | `dns:read` | read | | [dns_update](https://app.pbn.ltd/api/docs/mcp-tools#dns-update) | Changes a record; send only the fields that change. | `dns:write` | write | | [sites_nameservers](https://app.pbn.ltd/api/docs/mcp-tools#sites-nameservers) | The nameservers the domain must use (required), what it uses now (current, checked daily), whether it is pointed, whether the DNS zone expired at t… | `dns:read` | read | | [backups_create](https://app.pbn.ltd/api/docs/mcp-tools#backups-create) | Takes a full backup (files + database) now, as Create backup in the panel. Follow the job until the backup is Ok. | `backups:write` | write | | [backups_delete](https://app.pbn.ltd/api/docs/mcp-tools#backups-delete) | Deletes a backup. A backup that a blueprint uses cannot be deleted. | `backups:write` | DESTRUCTIVE | | [backups_download](https://app.pbn.ltd/api/docs/mcp-tools#backups-download) | A temporary link to download the backup archive (tar.gz) - valid about 4 hours. | `backups:read` | read | | [backups_get](https://app.pbn.ltd/api/docs/mcp-tools#backups-get) | One backup of the site. | `backups:read` | read | | [backups_list](https://app.pbn.ltd/api/docs/mcp-tools#backups-list) | The site's backups, newest first (automatic daily ones, manual ones and uploaded ones). | `backups:read` | read | | [backups_restore](https://app.pbn.ltd/api/docs/mcp-tools#backups-restore) | Replaces the site's files and database with the backup (as Restore in the panel). Refused while the site is busy, switched off or the plan has expi… | `backups:write` | DESTRUCTIVE | | [files_delete](https://app.pbn.ltd/api/docs/mcp-tools#files-delete) | Deletes a file, or a folder (with recursive=true when it is not empty). There is no undo - take a backup first for anything important. | `files:write` | DESTRUCTIVE | | [files_list](https://app.pbn.ltd/api/docs/mcp-tools#files-list) | Folders first, then files; up to 5,000 entries. Symbolic links are listed (type "link") but never followed. Available under the same rule as the Fi… | `files:read` | read | | [files_mkdir](https://app.pbn.ltd/api/docs/mcp-tools#files-mkdir) | Creates a folder (and any missing parent folders). | `files:write` | write | | [files_move](https://app.pbn.ltd/api/docs/mcp-tools#files-move) | Renames or moves a file or folder inside the site folder. | `files:write` | write | | [files_read](https://app.pbn.ltd/api/docs/mcp-tools#files-read) | Returns a file of up to 5 MB (the limit is in GET /limits). Use encoding=raw to stream the bytes directly. | `files:read` | read | | [files_stat](https://app.pbn.ltd/api/docs/mcp-tools#files-stat) | Type, size, modification time and mode of one path. | `files:read` | read | | [files_write](https://app.pbn.ltd/api/docs/mcp-tools#files-write) | Writes a file of up to 25 MB atomically (a temporary file renamed into place), owned by the site's web server user. Refuses to write through symbol… | `files:write` | write | | [logs_access](https://app.pbn.ltd/api/docs/mcp-tools#logs-access) | Requests that reached the web server for this site's hostnames, newest last. The client IP is the visitor as the CDN reported it. 30 fetches per 10… | `logs:read` | read | | [logs_errors](https://app.pbn.ltd/api/docs/mcp-tools#logs-errors) | Recent PHP and web server errors of THIS site (the Error log tab): last 7 days, grouped, sanitised (paths shown relative to the site folder, secret… | `logs:read` | read | | [tickets_create](https://app.pbn.ltd/api/docs/mcp-tools#tickets-create) | Opens a ticket (support is e-mailed as for a panel ticket). The ticket limits apply: at most 2 open tickets at a time (409 ticket_limit). | `tickets:write` | write | | [tickets_get](https://app.pbn.ltd/api/docs/mcp-tools#tickets-get) | A ticket with every public message (yours and support's). can_reply says if you may add a message now (ticket limits: one message before support an… | `tickets:read` | read | | [tickets_list](https://app.pbn.ltd/api/docs/mcp-tools#tickets-list) | Your tickets, newest first. | `tickets:read` | read | | [tickets_queues](https://app.pbn.ltd/api/docs/mcp-tools#tickets-queues) | The categories (queues) a ticket can be opened in. | `tickets:read` | read | | [tickets_reply](https://app.pbn.ltd/api/docs/mcp-tools#tickets-reply) | Adds your message to the ticket (support is notified). One message until support answers. | `tickets:write` | write | | [kb_article](https://app.pbn.ltd/api/docs/mcp-tools#kb-article) | One article as plain text and as sanitised HTML, with related articles. | `kb:read` | read | | [kb_search](https://app.pbn.ltd/api/docs/mcp-tools#kb-search) | The dashboard's knowledge-base search (keyword + meaning). Every key may use it. | `kb:read` | read | | [sites_kb_suggestions](https://app.pbn.ltd/api/docs/mcp-tools#sites-kb-suggestions) | Knowledge-base articles that fit what is going on with this site right now (why it is paused, nameservers not pointed, offline...), with app and pu… | `sites:read` | read | | [jobs_get](https://app.pbn.ltd/api/docs/mcp-tools#jobs-get) | The live status of a job: running, succeeded or failed, with a message. Poll every few seconds (it counts against the rate limit like any call). | `account:read` | read | | [jobs_list](https://app.pbn.ltd/api/docs/mcp-tools#jobs-list) | Jobs the API started for this account, newest first. | `account:read` | read | | [campaigns_get](https://app.pbn.ltd/api/docs/mcp-tools#campaigns-get) | One campaign with its last ten runs. | `content:read` | read | | [campaigns_list](https://app.pbn.ltd/api/docs/mcp-tools#campaigns-list) | The AI auto-posting campaigns on the account: what they write, when they run next and how many articles they have made. | `content:read` | read | | [campaigns_status](https://app.pbn.ltd/api/docs/mcp-tools#campaigns-status) | Pauses a campaign or starts it again. A campaign paused by our staff or by a problem with an AI key cannot be resumed from here - fix the cause first. | `content:write` | write | | [campaigns_write_now](https://app.pbn.ltd/api/docs/mcp-tools#campaigns-write-now) | Asks a campaign to write and publish now, the same as the "Write now" button. Returns the run; follow it with runs.get. | `content:write` | write | | [content_create](https://app.pbn.ltd/api/docs/mcp-tools#content-create) | Writes the post and puts it on the site. Works for every site type: WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav and MediaWiki get a nativ… | `content:write` | write | | [content_get](https://app.pbn.ltd/api/docs/mcp-tools#content-get) | One post in full, with its text and the history of what happened to it. | `content:read` | read | | [content_list](https://app.pbn.ltd/api/docs/mcp-tools#content-list) | The latest posts the site itself reports, and every post this account has written for it through PBN.LTD (including ones still being written or wai… | `content:read` | read | | [content_media](https://app.pbn.ltd/api/docs/mcp-tools#content-media) | Puts a picture or file on the site and gives back the address to use in a post. On WordPress it goes into the media library; on every other site ty… | `content:write` | write | | [content_options](https://app.pbn.ltd/api/docs/mcp-tools#content-options) | Before writing anything, ask this: it says how a post appears on this site type, which fields it supports (featured picture, categories, tags, auth… | `content:read` | read | | [content_queue](https://app.pbn.ltd/api/docs/mcp-tools#content-queue) | Every post on the account that is queued, being written, waiting for approval, scheduled, publishing, published, failed or cancelled - newest first… | `content:read` | read | | [content_remove](https://app.pbn.ltd/api/docs/mcp-tools#content-remove) | Removes a published post from the site, or cancels one that has not gone out yet. There is no undo. | `content:write` | DESTRUCTIVE | | [content_update](https://app.pbn.ltd/api/docs/mcp-tools#content-update) | Changes a post that has not gone out yet. A post that is already on the site cannot be edited from here - remove it and write a new one. | `content:write` | write | | [runs_get](https://app.pbn.ltd/api/docs/mcp-tools#runs-get) | One run of a campaign and every article in it. | `content:read` | read | | [code_check](https://app.pbn.ltd/api/docs/mcp-tools#code-check) | Runs PHP's own syntax check over a file or every .php file in a folder, in the exact PHP version the site runs. Do this before switching a plugin o… | `sites:read` | read | | [extsites_deploy](https://app.pbn.ltd/api/docs/mcp-tools#extsites-deploy) | Publishes the site to its provider again, or rolls it back to a deploy that is already there. This replaces what is live at that provider. No files… | `sites:write` | DESTRUCTIVE | | [extsites_history](https://app.pbn.ltd/api/docs/mcp-tools#extsites-history) | The recent deploys of one external site, newest first - what to pass to a rollback. | `sites:read` | read | | [extsites_list](https://app.pbn.ltd/api/docs/mcp-tools#extsites-list) | The sites this account publishes to outside PBN.LTD (GitHub Pages, Cloudflare Pages, Netlify, Vercel and the rest), with their address and the stat… | `sites:read` | read | | [files_upload_archive](https://app.pbn.ltd/api/docs/mcp-tools#files-upload-archive) | Unpacks a zip of a built website (or a plugin or theme) into the site. Every file goes through the same path a single upload uses, so the same rule… | `files:write` | write | | [plugins_delete](https://app.pbn.ltd/api/docs/mcp-tools#plugins-delete) | Switches a plugin off and deletes its files. There is no undo; the plugin's own data in the database is removed the way the plugin asks for. | `sites:write` | DESTRUCTIVE | | [plugins_install](https://app.pbn.ltd/api/docs/mcp-tools#plugins-install) | Installs a plugin on a WordPress site, from wordpress.org or from a zip you send. A plugin that is on our blocked list is refused. Activating a plu… | `sites:write` | DESTRUCTIVE | | [plugins_list](https://app.pbn.ltd/api/docs/mcp-tools#plugins-list) | Every plugin installed on a WordPress site, whether it is active, its version and whether an update is waiting. | `sites:read` | read | | [plugins_state](https://app.pbn.ltd/api/docs/mcp-tools#plugins-state) | Activates or deactivates a plugin. Activating can take a site down, so check it with sites.verify afterwards; deactivating is how you put it back. | `sites:write` | DESTRUCTIVE | | [sites_set_home](https://app.pbn.ltd/api/docs/mcp-tools#sites-set-home) | Makes a page the front page of the site. On WordPress this sets the site's own "front page" setting; on the other site types the file you name is c… | `sites:write` | DESTRUCTIVE | | [sites_verify](https://app.pbn.ltd/api/docs/mcp-tools#sites-verify) | Fetches a page of the site and says exactly what came back: the HTTP status, how long it took, its size, its title and any redirect. The page is al… | `sites:read` | read | | [themes_activate](https://app.pbn.ltd/api/docs/mcp-tools#themes-activate) | Switches the site to another theme. This changes how every page looks at once - check the site with sites.verify straight after, and switch back if… | `sites:write` | DESTRUCTIVE | | [themes_list](https://app.pbn.ltd/api/docs/mcp-tools#themes-list) | Every theme on a WordPress site and which one is in use. | `sites:read` | read | | [jobs_wait](https://app.pbn.ltd/api/docs/mcp-tools#jobs-wait) | Waits for a job started by another tool (site create, backup, restore, delete, reinstall, DNS publish) and returns it once it has succeeded or fail… | `account:read` | read | ## Account The account behind the key: profile, plan, site slots, limits and usage. ### `account_limits` **Plan, slots, limits and frozen sites** · read-only · permission `account:read` · REST equivalent `GET /account/limits` Site slots (plan + extra-site add-ons), subscription state and paid-until date, the default per-site limits, every paused (frozen) site with the reason(s) it is paused, and the add-ons on the account. Needs the `account:read` permission. Takes no arguments. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "account_limits", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Account](https://app.pbn.ltd/api/docs/reference/account#account-limits). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `account_me` **Who am I** · read-only · permission `account:read` · REST equivalent `GET /me` The account and the key making the call. The cheapest call there is: use it to test a key. Needs the `account:read` permission. Takes no arguments. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "account_me", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Account](https://app.pbn.ltd/api/docs/reference/account#account-me). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `account_usage_refresh` **Measure usage of the whole account now** · write · permission `account:write` · REST equivalent `POST /account/limits/refresh` Starts a fresh disk / database / file-count measurement for the account's sites - the usage tab's "Check now", for many sites at once. It returns at once: each site is reported as started, busy, throttled (the panel's own per-site cooldown and per-account budget apply unchanged) or skipped with the reason. Poll GET /account/limits or GET /sites/{site_id}/usage for the new figures a few seconds later. Needs the `account:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_ids` | array | no | Only these sites (default: every installed site of the account, newest first). | | `max_sites` | integer | no | How many sites to start a measurement for in this call (1-100). Default: `25`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "account_usage_refresh", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Account](https://app.pbn.ltd/api/docs/reference/account#account-usage_refresh). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `meta_limits` **API limits for this key** · read-only · permission `kb:read` · REST equivalent `GET /limits` The rate limits and size limits that apply to the key making the call. `overridden` is true when support has raised or lowered a limit for this key or for the whole account. Needs the `kb:read` permission. Takes no arguments. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "meta_limits", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Account](https://app.pbn.ltd/api/docs/reference/account#meta-limits). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Billing Subscription, payment due, invoices and invoice PDFs, add-ons. ### `billing_invoice_pdf` **Download an invoice PDF** · read-only · permission `billing:read` · REST equivalent `GET /billing/invoices/{number}.pdf` The PDF of an invoice or credit note of this account (Content-Type application/pdf). Needs the `billing:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `number` | string | yes | Invoice number, e.g. PBN-2026-001234. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "billing_invoice_pdf", "arguments": { "number": "PBN-2026-001234" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Billing](https://app.pbn.ltd/api/docs/reference/billing#billing-invoice_pdf). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `billing_invoices` **Payments and invoices** · read-only · permission `billing:read` · REST equivalent `GET /billing/invoices` Every payment and refund of the account, newest first. `document` is the invoice / credit note when one has been issued; download it with GET /billing/invoices/{number}.pdf. Needs the `billing:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | Items per page (1-200). Default: `50`. | | `cursor` | string | no | The next_cursor value of the previous page. Omit for the first page. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "billing_invoices", "arguments": { "limit": 10 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Billing](https://app.pbn.ltd/api/docs/reference/billing#billing-invoices). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `billing_summary` **Billing overview** · read-only · permission `billing:read` · REST equivalent `GET /billing` Subscription (state, plan, paid-until date, days left, payment method), unpaid state with the date sites are removed if it stays unpaid, site slots, add-ons, VAT treatment and the panel links. Needs the `billing:read` permission. Takes no arguments. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "billing_summary", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Billing](https://app.pbn.ltd/api/docs/reference/billing#billing-summary). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Sites Every site type: list, search, create, change, delete; status, nameservers, CDN, SEO. ### `sites_create` **Create a site (any type)** · write · permission `sites:write` · REST equivalent `POST /sites` 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}. Needs the `sites:write` permission. Starts a job that finishes later: the answer carries `job.id` - wait for it with `jobs_wait` or poll `jobs_get`. | Argument | Type | Required | Description | |---|---|---|---| | `type` | string (one of: Wordpress, Static HTML, PHP hosting, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki) | yes | Site type. | | `name` | string | yes | A unique short name (letters, digits, dashes). | | `domain` | string | yes | The domain (or subdomain of one of your sites). | | `cdn` | string | no | CDN (see GET /sites/options for the choices open to you). | | `php_version` | string | no | PHP version value, e.g. "PHP 8.3" (see GET /sites/options). | | `use_https` | boolean | no | Serve over HTTPS. | | `use_www` | boolean | no | Use www. as the primary host. | | `title` | string | no | Site title (WordPress and the ready-installed applications). REQUIRED when type is Wordpress. | | `subtitle` | string | no | Tagline. REQUIRED when type is Wordpress; ignored for other types. | | `admin_email` | string | no | Administrator e-mail (WordPress and the applications). | | `feedback_email` | 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` | string | no | WordPress login path (default wp-login.php). REQUIRED when type is Wordpress. | | `template_id` | integer | no | WordPress template id (omit for random). | | `blueprint_id` | integer | no | Deploy from one of your blueprints (WordPress). | | `group_id` | integer | no | Put the site in this group. | | `create_mailbox` | boolean | no | Create an e-mail account for the domain. | | `autoupdate_wordpress` | boolean | no | Auto-update WordPress core and plugins. | | `form_fields` | 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. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_create", "arguments": { "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" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-create). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_create_options` **Fields and choices for a new site** · read-only · permission `sites:write` · REST equivalent `GET /sites/options` 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. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `type` | 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. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_create_options", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-create_options). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_delete` **Delete a site** · **destructive** · permission `sites:delete` · REST equivalent `DELETE /sites/{site_id}` 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. Needs the `sites:delete` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_delete", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-delete). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_edit_options` **Fields and choices to edit a site** · read-only · permission `sites:write` · REST equivalent `GET /sites/{site_id}/options` The fields the Edit site form offers for THIS site right now (they depend on type, CDN and state), with choices and current values. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_edit_options", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-edit_options). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_get` **Get one site** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}` 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). Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_get", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_list` **List and search sites** · read-only · permission `sites:read` · REST equivalent `GET /sites` 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. Needs the `sites:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `search` | string | no | Part of the domain or name (www. is ignored). | | `state` | 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` | 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` | string | no | Group id, or "none" for sites in no group. | | `cdn` | string | no | CDN name, e.g. Cloudflare, BunnyCDN, KeyCDN, CDN77.COM, Gcore, CloudFront. | | `php_version` | string | no | PHP version value, e.g. "PHP 8.3". | | `indexed` | string (one of: yes, no, pending) | no | Google indexation state. | | `online` | string (one of: online, offline) | no | The online badge: offline = a confirmed problem. | | `sort` | string (one of: newest, oldest, name, name_desc, domain, domain_desc) | no | Order of the list. Default: `newest`. | | `limit` | integer | no | Items per page (1-200). Default: `50`. | | `cursor` | string | no | The next_cursor value of the previous page. Omit for the first page. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_list", "arguments": { "search": "blog", "state": "ok", "limit": 20 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_update` **Change site settings** · write · permission `sites:write` · REST equivalent `PATCH /sites/{site_id}` 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. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `name` | string | no | A unique short name (letters, digits, dashes). | | `domain` | string | no | The domain (or subdomain of one of your sites). | | `php_version` | string | no | PHP version value, e.g. "PHP 8.3" (see GET /sites/options). | | `use_https` | boolean | no | Serve over HTTPS. | | `use_www` | boolean | no | Use www. as the primary host. | | `admin_email` | string | no | Administrator e-mail (WordPress and the applications). | | `login_url` | string | no | WordPress login path (default wp-login.php). REQUIRED when type is Wordpress. | | `group_id` | integer | no | Put the site in this group. | | `create_mailbox` | boolean | no | Create an e-mail account for the domain. | | `autoupdate_wordpress` | boolean | no | Auto-update WordPress core and plugins. | | `form_fields` | 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` | string (one of: full, flexible) | no | Cloudflare SSL mode (only offered when the site has its own address records). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_update", "arguments": { "site_id": 123, "php_version": "PHP 8.3", "use_www": true } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Sites](https://app.pbn.ltd/api/docs/reference/sites#sites-update). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Site tools Cache purge, one-click admin login, reinstall, health checks, Site Cleaner, usage. ### `cleaner_get` **Site Cleaner status** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}/cleaner` Site Cleaner for this site: whether the add-on is active, the last scan (what can be removed and how much it saves), the running job, recent jobs (with undo_available) and the schedule. Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "cleaner_get", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#cleaner-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `cleaner_run` **Scan, clean or undo** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/cleaner/{action}` Runs Site Cleaner. `scan` lists unused plugins/themes, junk files and database clutter (free). `clean` removes the selected items after taking a backup and checks the site afterwards - it needs the Site Cleaner add-on (402 addon_required otherwise). `undo` puts a clean back from its backup. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `action` | string (one of: scan, clean, undo) | yes | scan (free) / clean (needs the Site Cleaner add-on) / undo a clean. | | `items` | array | no | clean: ids from last_scan.items to remove (omit = the default selection). | | `job_id` | integer | no | undo: the id of the clean job to undo. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "cleaner_run", "arguments": { "site_id": 123, "action": "scan" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#cleaner-run). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `cleaner_schedule` **Automatic cleaning** · write · permission `sites:write` · REST equivalent `PUT /sites/{site_id}/cleaner/schedule` Sets automatic cleaning for the site (needs the add-on to actually clean). Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `schedule` | string (one of: off, daily, weekly, monthly) | yes | How often Site Cleaner runs by itself. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "cleaner_schedule", "arguments": { "site_id": 123, "schedule": "weekly" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#cleaner-schedule). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `health_get` **Health status** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}/health` The online badge (online / offline / slow / paused... with the reason) and, for application sites, the last integrity check (V1 config lines, admin account, login health, core files). Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "health_get", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#health-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `health_integrity_check` **Integrity check now** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/health/integrity-check` Checks the application now (WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki): V1's config lines, the site URL, the admin account, one-click login health and core file checksums. Changes nothing. Takes a few seconds. Needs the `sites:write` permission. This can take up to a minute. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "health_integrity_check", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#health-integrity_check). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `health_recheck` **Re-check if the site is up** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/health/recheck` Probes the home page again now (same probe and recording as the platform's monitor) and asks the server why if it fails. Asynchronous: follow the job; its result has the outcome. At most 10 an hour. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "health_recheck", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#health-recheck). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `health_repair` **Repair the site** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/health/repair` "Repair site": checks, repairs everything repairable (backups first, on the server), checks again. You get an e-mail listing what was repaired. Needs the `sites:write` permission. This can take up to a minute. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "health_repair", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#health-repair). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_admin_login` **One-click admin login link** · write · permission `sites:login` · REST equivalent `POST /sites/{site_id}/admin-login` A single-use login link to the site's admin (WordPress wp-admin, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki), valid for 60 seconds - the panel's one-click login. Open it in a browser; do not store it. Not for Static HTML / PHP hosting sites. Needs the `sites:login` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_admin_login", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-admin_login). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_php_version` **Change the PHP version** · write · permission `sites:write` · REST equivalent `PUT /sites/{site_id}/php-version` Shortcut for PATCH /sites/{site_id} with php_version. The web server is reconfigured in the background (a job is returned). Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `php_version` | string | yes | e.g. "PHP 8.3" (see GET /sites/{site_id}/options) | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_php_version", "arguments": { "site_id": 123, "php_version": "PHP 8.3" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-php_version). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_purge_cache` **Purge the CDN cache** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/purge-cache` Clears the CDN cache of both hostnames (as Purge CDN cache in the panel). The site must be live. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_purge_cache", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-purge_cache). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_reinstall` **Reinstall from scratch** · **destructive** · permission `sites:write` · REST equivalent `POST /sites/{site_id}/reinstall` Installs the site again from scratch (as Reinstall in the panel) - the current files and database are replaced. Refused while the site is busy, switched off or being deleted. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `confirm` | boolean | yes | Must be true: this action overwrites the site. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_reinstall", "arguments": { "site_id": 123, "confirm": true } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-reinstall). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_temp_unfreeze` **Switch a paused site on for cleanup** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/temporary-unfreeze` A site paused ONLY for a usage limit (disk / database / files) comes back for 30 minutes so you can clean it up (3 times per site per day). At the end it is paused again only if still over the limit. freeze.temporary_unfreeze in GET /sites/{site_id} says if it is available and why not. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_temp_unfreeze", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-temp_unfreeze). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_usage` **Disk, database and file usage** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}/usage` The Usage & add-ons tab: disk, database and file-count use against the limits (plan + add-ons), what is over, CDN bandwidth for metered CDNs, and the add-ons on the site. Numbers are measured every few hours; POST /sites/{site_id}/usage/refresh measures now. Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_usage", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-usage). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_usage_refresh` **Measure usage now** · write · permission `sites:write` · REST equivalent `POST /sites/{site_id}/usage/refresh` Starts a fresh measurement of disk, database and files (the usage tab's "Check now"). Returns the current figures with busy=true; poll GET /sites/{site_id}/usage until busy is false (seconds). Same throttles as the panel: one measurement per site every few minutes. Needs the `sites:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_usage_refresh", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Site tools](https://app.pbn.ltd/api/docs/reference/site-tools#sites-usage_refresh). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## DNS records The site's own DNS records (A, AAAA, CNAME, TXT, MX, SRV, CAA, NS). ### `dns_create` **Add a DNS record** · write · permission `dns:write` · REST equivalent `POST /sites/{site_id}/dns` Adds a record with the panel's own validation. It is published to the site's DNS provider within a minute or two (follow the job); your records are never overwritten by the platform. Needs the `dns:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `type` | string (one of: A, AAAA, CNAME, TXT, MX, SRV, CAA, NS) | yes | Record type. | | `name` | string | yes | "@" for the domain itself, or a name like blog, shop.eu, _dmarc, _sip._tcp. | | `value` | string | yes | IPv4 (A), IPv6 (AAAA), host name (CNAME, MX, SRV target, NS), text (TXT) or CA domain (CAA). | | `priority` | integer | no | MX and SRV. | | `weight` | integer | no | SRV. | | `port` | integer | no | SRV. | | `caa_flags` | integer (one of: 0, 128) | no | CAA: 0 or 128 (critical). | | `caa_tag` | string (one of: issue, issuewild, iodef) | no | CAA tag. | | `proxied` | boolean | no | Cloudflare only, A/AAAA/CNAME: proxy through the CDN (off = DNS only). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "dns_create", "arguments": { "site_id": 123, "type": "TXT", "name": "@", "value": "google-site-verification=abc123" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [DNS records](https://app.pbn.ltd/api/docs/reference/dns#dns-create). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `dns_delete` **Delete a DNS record** · write · permission `dns:write` · REST equivalent `DELETE /sites/{site_id}/dns/{record_id}` Deletes a record; it is removed at the DNS provider within a minute or two. Needs the `dns:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `record_id` | integer | yes | The DNS record id. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "dns_delete", "arguments": { "site_id": 123, "record_id": 5501 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [DNS records](https://app.pbn.ltd/api/docs/reference/dns#dns-delete). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `dns_list` **List DNS records** · read-only · permission `dns:read` · REST equivalent `GET /sites/{site_id}/dns` The site's own DNS records (the DNS records tab), and the publishing state: the DNS provider, when the records were last published and any record the provider refused (with its error). Records the platform manages for the CDN are not listed and are never changed through this API. Needs the `dns:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "dns_list", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [DNS records](https://app.pbn.ltd/api/docs/reference/dns#dns-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `dns_update` **Change a DNS record** · write · permission `dns:write` · REST equivalent `PATCH /sites/{site_id}/dns/{record_id}` Changes a record; send only the fields that change. Needs the `dns:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `record_id` | integer | yes | The DNS record id. | | `type` | string (one of: A, AAAA, CNAME, TXT, MX, SRV, CAA, NS) | no | Record type. | | `name` | string | no | "@" for the domain itself, or a name like blog, shop.eu, _dmarc, _sip._tcp. | | `value` | string | no | IPv4 (A), IPv6 (AAAA), host name (CNAME, MX, SRV target, NS), text (TXT) or CA domain (CAA). | | `priority` | integer | no | MX and SRV. | | `weight` | integer | no | SRV. | | `port` | integer | no | SRV. | | `caa_flags` | integer (one of: 0, 128) | no | CAA: 0 or 128 (critical). | | `caa_tag` | string (one of: issue, issuewild, iodef) | no | CAA tag. | | `proxied` | boolean | no | Cloudflare only, A/AAAA/CNAME: proxy through the CDN (off = DNS only). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "dns_update", "arguments": { "site_id": 123, "record_id": 5501, "value": "google-site-verification=xyz789" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [DNS records](https://app.pbn.ltd/api/docs/reference/dns#dns-update). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_nameservers` **Nameserver status** · read-only · permission `dns:read` · REST equivalent `GET /sites/{site_id}/nameservers` The nameservers the domain must use (required), what it uses now (current, checked daily), whether it is pointed, whether the DNS zone expired at the provider (zone_expired: re-create it from the site page), and the registrar autopilot status when a registrar connection sets them for you. Needs the `dns:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_nameservers", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [DNS records](https://app.pbn.ltd/api/docs/reference/dns#sites-nameservers). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Backups List, create, download and restore backups. ### `backups_create` **Create a backup now** · write · permission `backups:write` · REST equivalent `POST /sites/{site_id}/backups` Takes a full backup (files + database) now, as Create backup in the panel. Follow the job until the backup is Ok. Needs the `backups:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backups_create", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Backups](https://app.pbn.ltd/api/docs/reference/backups#backups-create). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `backups_delete` **Delete a backup** · **destructive** · permission `backups:write` · REST equivalent `DELETE /sites/{site_id}/backups/{backup_id}` Deletes a backup. A backup that a blueprint uses cannot be deleted. Needs the `backups:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `backup_id` | integer | yes | The backup id. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backups_delete", "arguments": { "site_id": 123, "backup_id": 88001 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Backups](https://app.pbn.ltd/api/docs/reference/backups#backups-delete). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `backups_download` **Download link** · read-only · permission `backups:read` · REST equivalent `POST /sites/{site_id}/backups/{backup_id}/download-link` A temporary link to download the backup archive (tar.gz) - valid about 4 hours. Needs the `backups:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `backup_id` | integer | yes | The backup id. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backups_download", "arguments": { "site_id": 123, "backup_id": 88001 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Backups](https://app.pbn.ltd/api/docs/reference/backups#backups-download). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `backups_get` **Get one backup** · read-only · permission `backups:read` · REST equivalent `GET /sites/{site_id}/backups/{backup_id}` One backup of the site. Needs the `backups:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `backup_id` | integer | yes | The backup id. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backups_get", "arguments": { "site_id": 123, "backup_id": 88001 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Backups](https://app.pbn.ltd/api/docs/reference/backups#backups-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `backups_list` **List backups** · read-only · permission `backups:read` · REST equivalent `GET /sites/{site_id}/backups` The site's backups, newest first (automatic daily ones, manual ones and uploaded ones). Needs the `backups:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `state` | string (one of: Pending, Creating, Ok, Error, Storing) | no | Only backups in this state. | | `limit` | integer | no | Items per page (1-200). Default: `50`. | | `cursor` | string | no | The next_cursor value of the previous page. Omit for the first page. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backups_list", "arguments": { "site_id": 123, "limit": 10 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Backups](https://app.pbn.ltd/api/docs/reference/backups#backups-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `backups_restore` **Restore a backup** · **destructive** · permission `backups:write` · REST equivalent `POST /sites/{site_id}/backups/{backup_id}/restore` Replaces the site's files and database with the backup (as Restore in the panel). Refused while the site is busy, switched off or the plan has expired. Follow the job. Needs the `backups:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `backup_id` | integer | yes | The backup id. | | `confirm` | boolean | yes | Must be true: this action overwrites the site. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backups_restore", "arguments": { "site_id": 123, "backup_id": 88001, "confirm": true } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Backups](https://app.pbn.ltd/api/docs/reference/backups#backups-restore). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Files Browse, read, upload, rename and delete the files of a site. ### `files_delete` **Delete a file or folder** · **destructive** · permission `files:write` · REST equivalent `DELETE /sites/{site_id}/files` Deletes a file, or a folder (with recursive=true when it is not empty). There is no undo - take a backup first for anything important. Needs the `files:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). | | `recursive` | boolean | no | Required to delete a folder that is not empty. Default: `False`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_delete", "arguments": { "site_id": 123, "path": "hello.txt" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-delete). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_list` **List a folder** · read-only · permission `files:read` · REST equivalent `GET /sites/{site_id}/files` Folders first, then files; up to 5,000 entries. Symbolic links are listed (type "link") but never followed. Available under the same rule as the File Manager: the site is installed, the plan is active and the site is not suspended. Needs the `files:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | no | Folder path relative to the site folder (default: the site folder itself). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_list", "arguments": { "site_id": 123, "path": "wp-content" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_mkdir` **Create a folder** · write · permission `files:write` · REST equivalent `POST /sites/{site_id}/files/folders` Creates a folder (and any missing parent folders). Needs the `files:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | The new folder path. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_mkdir", "arguments": { "site_id": 123, "path": "wp-content/uploads/reports" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-mkdir). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_move` **Rename or move** · write · permission `files:write` · REST equivalent `POST /sites/{site_id}/files/move` Renames or moves a file or folder inside the site folder. Needs the `files:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | What to move. | | `to` | string | yes | The new path. | | `overwrite` | boolean | no | Replace an existing file at the destination. Default: `False`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_move", "arguments": { "site_id": 123, "path": "hello.txt", "to": "old/hello.txt" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-move). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_read` **Read (download) a file** · read-only · permission `files:read` · REST equivalent `GET /sites/{site_id}/files/content` Returns a file of up to 5 MB (the limit is in GET /limits). Use encoding=raw to stream the bytes directly. Needs the `files:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). | | `encoding` | string (one of: base64, text, raw) | no | base64 (JSON, any file), text (JSON, UTF-8 files) or raw (the bytes themselves, with a Content-Type). Default: `base64`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_read", "arguments": { "site_id": 123, "path": "robots.txt", "encoding": "text" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-read). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_stat` **File or folder details** · read-only · permission `files:read` · REST equivalent `GET /sites/{site_id}/files/stat` Type, size, modification time and mode of one path. Needs the `files:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_stat", "arguments": { "site_id": 123, "path": "index.php" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-stat). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_write` **Upload (write) a file** · write · permission `files:write` · REST equivalent `PUT /sites/{site_id}/files/content` Writes a file of up to 25 MB atomically (a temporary file renamed into place), owned by the site's web server user. Refuses to write through symbolic links and into folders the platform manages. Needs the `files:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | Where to write it, relative to the site folder. | | `content` | string | no | Text content (UTF-8). Or use content_base64. | | `content_base64` | string | no | The file, base64-encoded. Or send the raw bytes as the request body (Content-Type application/octet-stream) or a multipart form with a "file" field. | | `overwrite` | boolean | no | Replace an existing file. Default: `False`. | | `mkdirs` | boolean | no | Create missing folders. Default: `True`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_write", "arguments": { "site_id": 123, "path": "hello.txt", "overwrite": "true", "content": "Hello world\n" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Files](https://app.pbn.ltd/api/docs/reference/files#files-write). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Logs Error log and access log of a site. ### `logs_access` **Access log** · read-only · permission `logs:read` · REST equivalent `GET /sites/{site_id}/logs/access` Requests that reached the web server for this site's hostnames, newest last. The client IP is the visitor as the CDN reported it. 30 fetches per 10 minutes per account. Needs the `logs:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `hours` | integer | no | How far back to look. Default: `1`. | | `lines` | integer | no | At most this many (the newest) lines. Default: `200`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "logs_access", "arguments": { "site_id": 123, "hours": 1, "lines": 100 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Logs](https://app.pbn.ltd/api/docs/reference/logs#logs-access). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `logs_errors` **Error log** · read-only · permission `logs:read` · REST equivalent `GET /sites/{site_id}/logs/errors` Recent PHP and web server errors of THIS site (the Error log tab): last 7 days, grouped, sanitised (paths shown relative to the site folder, secrets hidden). php_hint=true when the errors look like code written for an older PHP version. Cached 60 s; 20 fetches per 10 minutes per account. Needs the `logs:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "logs_errors", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Logs](https://app.pbn.ltd/api/docs/reference/logs#logs-errors). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Support tickets Open support tickets and reply to them. ### `tickets_create` **Open a ticket** · write · permission `tickets:write` · REST equivalent `POST /tickets` Opens a ticket (support is e-mailed as for a panel ticket). The ticket limits apply: at most 2 open tickets at a time (409 ticket_limit). Needs the `tickets:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `title` | string | yes | Short summary. | | `message` | string | yes | What happened, what you expected. | | `queue` | string | no | Category slug (GET /tickets/queues). Default: `general-support-request`. | | `priority` | string (one of: low, normal, high) | no | Priority. Default: `normal`. | | `site_id` | integer | no | The site it is about (its domain is added to the ticket). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tickets_create", "arguments": { "title": "Question about my site", "message": "How do I add a subdomain?", "queue": "general-support-request", "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Support tickets](https://app.pbn.ltd/api/docs/reference/tickets#tickets-create). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `tickets_get` **Read a ticket** · read-only · permission `tickets:read` · REST equivalent `GET /tickets/{ticket_id}` A ticket with every public message (yours and support's). can_reply says if you may add a message now (ticket limits: one message before support answers). Needs the `tickets:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `ticket_id` | integer | yes | The ticket id. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tickets_get", "arguments": { "ticket_id": 4412 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Support tickets](https://app.pbn.ltd/api/docs/reference/tickets#tickets-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `tickets_list` **List tickets** · read-only · permission `tickets:read` · REST equivalent `GET /tickets` Your tickets, newest first. Needs the `tickets:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `status` | string (one of: open, closed) | no | Only open or only closed tickets. | | `limit` | integer | no | Items per page (1-200). Default: `50`. | | `cursor` | string | no | The next_cursor value of the previous page. Omit for the first page. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tickets_list", "arguments": { "status": "open" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Support tickets](https://app.pbn.ltd/api/docs/reference/tickets#tickets-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `tickets_queues` **Ticket categories** · read-only · permission `tickets:read` · REST equivalent `GET /tickets/queues` The categories (queues) a ticket can be opened in. Needs the `tickets:read` permission. Takes no arguments. ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tickets_queues", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Support tickets](https://app.pbn.ltd/api/docs/reference/tickets#tickets-queues). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `tickets_reply` **Reply to a ticket** · write · permission `tickets:write` · REST equivalent `POST /tickets/{ticket_id}/messages` Adds your message to the ticket (support is notified). One message until support answers. Needs the `tickets:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `ticket_id` | integer | yes | The ticket id. | | `message` | string | yes | Your message. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tickets_reply", "arguments": { "ticket_id": 4412, "message": "Thanks, that worked." } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Support tickets](https://app.pbn.ltd/api/docs/reference/tickets#tickets-reply). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Knowledge base Search the knowledge base. ### `kb_article` **Read an article** · read-only · permission `kb:read` · REST equivalent `GET /kb/articles/{slug}` One article as plain text and as sanitised HTML, with related articles. Needs the `kb:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `slug` | string | yes | Article slug from a search result. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "kb_article", "arguments": { "slug": "change-nameservers" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Knowledge base](https://app.pbn.ltd/api/docs/reference/kb#kb-article). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `kb_search` **Search the knowledge base** · read-only · permission `kb:read` · REST equivalent `GET /kb/search` The dashboard's knowledge-base search (keyword + meaning). Every key may use it. Needs the `kb:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `q` | string | yes | What you are looking for, in plain words. | | `limit` | integer | no | At most this many. Default: `8`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "kb_search", "arguments": { "q": "change nameservers" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Knowledge base](https://app.pbn.ltd/api/docs/reference/kb#kb-search). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_kb_suggestions` **Help articles for this site** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}/help` Knowledge-base articles that fit what is going on with this site right now (why it is paused, nameservers not pointed, offline...), with app and public links. Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_kb_suggestions", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Knowledge base](https://app.pbn.ltd/api/docs/reference/kb#sites-kb_suggestions). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Jobs Progress of the long-running actions the API started. ### `jobs_get` **Job status** · read-only · permission `account:read` · REST equivalent `GET /jobs/{job_id}` The live status of a job: running, succeeded or failed, with a message. Poll every few seconds (it counts against the rate limit like any call). Needs the `account:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `job_id` | string | yes | The id returned as job.id. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "jobs_get", "arguments": { "job_id": "job_4f1c0a9e2b7d6c5a3e10" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Jobs](https://app.pbn.ltd/api/docs/reference/jobs#jobs-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `jobs_list` **List jobs** · read-only · permission `account:read` · REST equivalent `GET /jobs` Jobs the API started for this account, newest first. Needs the `account:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `status` | string (one of: running, succeeded, failed) | no | Only jobs in this state. | | `site_id` | integer | no | Only jobs of this site. | | `limit` | integer | no | Items per page (1-200). Default: `50`. | | `cursor` | string | no | The next_cursor value of the previous page. Omit for the first page. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "jobs_list", "arguments": { "status": "running" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Jobs](https://app.pbn.ltd/api/docs/reference/jobs#jobs-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `jobs_wait` **Wait for a job to finish** · read-only · permission `account:read` · REST equivalent `GET /jobs/{job_id}, polled until it finishes` Waits for a job started by another tool (site create, backup, restore, delete, reinstall, DNS publish) and returns it once it has succeeded or failed, or when the wait runs out. Use this instead of calling jobs_get in a loop. Needs the `account:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `job_id` | string | yes | The job id returned by the tool that started it (job_...). | | `timeout_seconds` | integer | no | How long to wait before giving up and returning the job as it is. Default: `120`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "jobs_wait", "arguments": { "job_id": "job_4f1c0a9e2b7d6c5a3e10", "timeout_seconds": 120 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Jobs](https://app.pbn.ltd/api/docs/jobs). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Content and publishing Posts, pages, pictures and the AI auto-posting campaigns, on every site type. ### `campaigns_get` **Read one campaign** · read-only · permission `content:read` · REST equivalent `GET /campaigns/{campaign_id}` One campaign with its last ten runs. Needs the `content:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `campaign_id` | integer | yes | The campaign. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "campaigns_get", "arguments": { "campaign_id": 44 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#campaigns-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `campaigns_list` **List auto-posting campaigns** · read-only · permission `content:read` · REST equivalent `GET /campaigns` The AI auto-posting campaigns on the account: what they write, when they run next and how many articles they have made. Needs the `content:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | How many to return. Default: `20`. | | `cursor` | string | no | The next_cursor value of the previous page. | | `status` | string (one of: active, paused, problem, finished) | no | | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "campaigns_list", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#campaigns-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `campaigns_status` **Pause or resume** · write · permission `content:write` · REST equivalent `PUT /campaigns/{campaign_id}/status` Pauses a campaign or starts it again. A campaign paused by our staff or by a problem with an AI key cannot be resumed from here - fix the cause first. Needs the `content:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `campaign_id` | integer | yes | | | `status` | string (one of: active, paused) | yes | "paused" stops it writing; "active" starts it again. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "campaigns_status", "arguments": { "campaign_id": 44, "status": "paused" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#campaigns-status). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `campaigns_write_now` **Write one now** · write · permission `content:write` · REST equivalent `POST /campaigns/{campaign_id}/write-now` Asks a campaign to write and publish now, the same as the "Write now" button. Returns the run; follow it with runs.get. Needs the `content:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `campaign_id` | integer | yes | | | `site_ids` | array | no | Only these sites of the campaign (default: all of them). | | `count` | integer | no | Articles per site. Default: `1`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "campaigns_write_now", "arguments": { "campaign_id": 44, "count": 1 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#campaigns-write_now). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_create` **Publish a post or page** · write · permission `content:write` · REST equivalent `POST /sites/{site_id}/content` Writes the post and puts it on the site. Works for every site type: WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav and MediaWiki get a native post/article/page, and Static HTML and PHP hosting sites get a page built in the look of their own pages, with a blog index and sitemap.xml kept up to date. The answer carries a job - wait for it and then read the post to get its address. Needs the `content:write` permission. Starts a job that finishes later: the answer carries `job.id` - wait for it with `jobs_wait` or poll `jobs_get`. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `title` | string | yes | The title of the post. | | `body_html` | string | no | The text as HTML. Or send body_markdown. | | `body_markdown` | string | no | The text as Markdown (headings, lists, links, bold, code, quotes). | | `status` | string (one of: publish, draft) | no | "draft" only on site types that have drafts (see content.options). Default: `publish`. | | `category` | string | no | Category id or name, on site types that have them. | | `author` | string | no | Author id, on site types that have authors. | | `tags` | array | no | Tags for the post. | | `slug` | string | no | The address of the post; one is made from the title when you leave it out. | | `publish_at` | string | no | ISO date and time to publish it (default: now). | | `featured_image_base64` | string | no | The main picture, base64. It leads the post and becomes the featured image on site types that have one. | | `images_base64` | array | no | More pictures, base64; they are placed in the text. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_create", "arguments": { "site_id": 123, "title": "Five ways to speed up your shop", "body_markdown": "## Why speed matters\n\nA faster shop sells more.", "status": "publish", "tags": [ "speed", "shop" ] } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-create). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_get` **Read one post** · read-only · permission `content:read` · REST equivalent `GET /sites/{site_id}/content/{post_id}` One post in full, with its text and the history of what happened to it. Needs the `content:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `post_id` | integer | yes | The post id this API gave you when it was created. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_get", "arguments": { "site_id": 123, "post_id": 90210 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_list` **Posts on this site** · read-only · permission `content:read` · REST equivalent `GET /sites/{site_id}/content` The latest posts the site itself reports, and every post this account has written for it through PBN.LTD (including ones still being written or waiting to go out). Needs the `content:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `limit` | integer | no | How many to return. Default: `20`. | | `state` | string (one of: queued, writing, images, review, ready, publishing, done, failed, cancelled) | no | Only posts in this state. | | `on_site` | boolean | no | Also ask the site itself for its latest posts (slower). Default: `True`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_list", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_media` **Upload a picture or file** · write · permission `content:write` · REST equivalent `POST /sites/{site_id}/content/media` Puts a picture or file on the site and gives back the address to use in a post. On WordPress it goes into the media library; on every other site type it goes into the site's own files. Needs the `content:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `name` | string | yes | The file name, e.g. "hero.jpg". | | `content_base64` | string | yes | The file itself, base64. | | `alt` | string | no | Alt text (WordPress media library). | | `folder` | string | no | Where to put it on site types with no media library. Default: `assets`. | | `overwrite` | boolean | no | Replace a file of the same name. Default: `False`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_media", "arguments": { "site_id": 123, "name": "hero.jpg", "content_base64": "iVBORw0KGgo=", "alt": "The shop front" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-media). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_options` **What a post on this site can have** · read-only · permission `content:read` · REST equivalent `GET /sites/{site_id}/content/options` Before writing anything, ask this: it says how a post appears on this site type, which fields it supports (featured picture, categories, tags, author, drafts) and the real categories and authors the site has. Site types with no drafts publish straight away - the answer says so. Needs the `content:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_options", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-options). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_queue` **Everything being written or published** · read-only · permission `content:read` · REST equivalent `GET /content/queue` Every post on the account that is queued, being written, waiting for approval, scheduled, publishing, published, failed or cancelled - newest first, across all sites. Needs the `content:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | How many to return. Default: `20`. | | `cursor` | string | no | The next_cursor value of the previous page. | | `state` | string (one of: queued, writing, images, review, ready, publishing, done, failed, cancelled) | no | Only posts in this state. | | `site_id` | integer | no | Only this site. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_queue", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-queue). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_remove` **Take a post off the site** · **destructive** · permission `content:write` · REST equivalent `DELETE /sites/{site_id}/content/{post_id}` Removes a published post from the site, or cancels one that has not gone out yet. There is no undo. Needs the `content:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `post_id` | integer | yes | The post id this API gave you when it was created. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_remove", "arguments": { "site_id": 123, "post_id": 90210 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-remove). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `content_update` **Change a post** · write · permission `content:write` · REST equivalent `PATCH /sites/{site_id}/content/{post_id}` Changes a post that has not gone out yet. A post that is already on the site cannot be edited from here - remove it and write a new one. Needs the `content:write` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `post_id` | integer | yes | The post id this API gave you when it was created. | | `title` | string | no | A new title. | | `body_html` | string | no | New text as HTML. | | `body_markdown` | string | no | New text as Markdown. | | `tags` | array | no | Replace the tags. | | `status` | string (one of: publish, draft) | no | | | `publish_at` | string | no | Move when it goes out. | | `approve` | boolean | no | Approve a post that is waiting for approval. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "content_update", "arguments": { "site_id": 123, "post_id": 90210, "title": "Five ways to speed up your shop (updated)" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#content-update). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `runs_get` **How a run is going** · read-only · permission `content:read` · REST equivalent `GET /runs/{run_id}` One run of a campaign and every article in it. Needs the `content:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `run_id` | integer | yes | The run. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "runs_get", "arguments": { "run_id": 7782 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Content and publishing](https://app.pbn.ltd/api/docs/reference/content#runs-get). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ## Building, plugins and themes Upload a built site, set the home page, install and test plugins and themes, and the sites you publish to other hosts. ### `code_check` **Check PHP code for errors** · read-only · permission `sites:read` · REST equivalent `POST /sites/{site_id}/code-check` Runs PHP's own syntax check over a file or every .php file in a folder, in the exact PHP version the site runs. Do this before switching a plugin or theme on - a syntax error there takes the whole site down. Needs the `sites:read` permission. This can take up to a minute. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | yes | A file or folder in the site, e.g. "wp-content/plugins/my-plugin". | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "code_check", "arguments": { "site_id": 123, "path": "wp-content/plugins/my-plugin" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#code-check). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `extsites_deploy` **Deploy again or roll back** · **destructive** · permission `sites:write` · REST equivalent `POST /external-sites/{ext_site_id}/deploy` Publishes the site to its provider again, or rolls it back to a deploy that is already there. This replaces what is live at that provider. No files travel through this call - to publish new files, put them on the PBN.LTD site first (files.write or files.upload_archive) and then redeploy. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `ext_site_id` | integer | yes | | | `kind` | string (one of: redeploy, rollback) | no | Build and publish again, or go back to an earlier deploy. Default: `redeploy`. | | `rollback_to` | string | no | The deploy to go back to (see extsites.history). Needed for a rollback. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "extsites_deploy", "arguments": { "ext_site_id": 9, "kind": "redeploy" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#extsites-deploy). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `extsites_history` **Deploys of an external site** · read-only · permission `sites:read` · REST equivalent `GET /external-sites/{ext_site_id}/deployments` The recent deploys of one external site, newest first - what to pass to a rollback. Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `ext_site_id` | integer | yes | | | `limit` | integer | no | Default: `25`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "extsites_history", "arguments": { "ext_site_id": 9 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#extsites-history). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `extsites_list` **Sites on third-party hosts** · read-only · permission `sites:read` · REST equivalent `GET /external-sites` The sites this account publishes to outside PBN.LTD (GitHub Pages, Cloudflare Pages, Netlify, Vercel and the rest), with their address and the state of the last deploy. Connecting a provider and adding a site stay in the panel, because they need your own provider credentials. Needs the `sites:read` permission. Paginated: pass `cursor` from `next_cursor` to read the next page. | Argument | Type | Required | Description | |---|---|---|---| | `limit` | integer | no | How many to return. Default: `20`. | | `cursor` | string | no | The next_cursor value of the previous page. | | `provider` | string | no | Only this provider. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "extsites_list", "arguments": {} } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#extsites-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `files_upload_archive` **Upload a built site (zip)** · write · permission `files:write` · REST equivalent `POST /sites/{site_id}/files/archive` Unpacks a zip of a built website (or a plugin or theme) into the site. Every file goes through the same path a single upload uses, so the same rules hold for each one: inside the site folder only, symbolic links are never followed and the platform's own folders are refused. Up to 60 files and 25 MB in one call - send a bigger site in parts. Needs the `files:write` permission. This can take up to a minute. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `zip_base64` | string | yes | The .zip file, base64. | | `path` | string | no | Folder inside the site to unpack into (default: the site root). | | `strip_top_folder` | boolean | no | Drop the single top folder the zip may have ("mysite/index.html" -> "index.html"). Default: `False`. | | `overwrite` | boolean | no | Replace files that already exist. Default: `False`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "files_upload_archive", "arguments": { "site_id": 123, "zip_base64": "UEsDBAoAAAAAA...", "path": "", "strip_top_folder": true, "overwrite": true } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#files-upload_archive). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `plugins_delete` **Remove a plugin** · **destructive** · permission `sites:write` · REST equivalent `DELETE /sites/{site_id}/plugins/{slug}` Switches a plugin off and deletes its files. There is no undo; the plugin's own data in the database is removed the way the plugin asks for. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `slug` | string | yes | The plugin folder name. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "plugins_delete", "arguments": { "site_id": 123, "slug": "my-plugin" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#plugins-delete). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `plugins_install` **Install a plugin** · **destructive** · permission `sites:write` · REST equivalent `POST /sites/{site_id}/plugins` Installs a plugin on a WordPress site, from wordpress.org or from a zip you send. A plugin that is on our blocked list is refused. Activating a plugin can break a site: install first, then activate, then check the site with sites.verify and read the error log. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. This can take up to a minute. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `slug` | string | no | A wordpress.org plugin slug, e.g. "classic-editor". | | `zip_base64` | string | no | Or your own plugin as a .zip file, base64. | | `activate` | boolean | no | Switch it on straight away. Test the site afterwards with sites.verify. Default: `False`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "plugins_install", "arguments": { "site_id": 123, "slug": "classic-editor", "activate": false } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#plugins-install). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `plugins_list` **Plugins on this site** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}/plugins` Every plugin installed on a WordPress site, whether it is active, its version and whether an update is waiting. Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "plugins_list", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#plugins-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `plugins_state` **Switch a plugin on or off** · **destructive** · permission `sites:write` · REST equivalent `POST /sites/{site_id}/plugins/{slug}/{action}` Activates or deactivates a plugin. Activating can take a site down, so check it with sites.verify afterwards; deactivating is how you put it back. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `slug` | string | yes | The plugin folder name. | | `action` | string (one of: activate, deactivate) | yes | What to do. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "plugins_state", "arguments": { "site_id": 123, "slug": "my-plugin", "action": "activate" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#plugins-state). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_set_home` **Set the home page** · **destructive** · permission `sites:write` · REST equivalent `POST /sites/{site_id}/home-page` Makes a page the front page of the site. On WordPress this sets the site's own "front page" setting; on the other site types the file you name is copied over index.html (or index.php), which replaces what is there now. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `page` | string | yes | WordPress: the page id or its exact title. Other site types: the file to use, e.g. "home.html". | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_set_home", "arguments": { "site_id": 123, "page": "home.html" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#sites-set_home). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `sites_verify` **Fetch a page and check it** · read-only · permission `sites:read` · REST equivalent `POST /sites/{site_id}/verify` Fetches a page of the site and says exactly what came back: the HTTP status, how long it took, its size, its title and any redirect. The page is always fetched on the site's own server (so it works even before the domain points at us) and, when the address allows it, from the internet as well. The newest PHP errors are read at the same time. This is the honest test after changing anything - a plugin, a theme, a file or a setting. Needs the `sites:read` permission. This can take up to a minute. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `path` | string | no | The page to fetch, e.g. "/about". Default: `/`. | | `public` | boolean | no | Also try the page from the internet. Default: `True`. | | `errors` | boolean | no | Also read the PHP error log afterwards. Default: `True`. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "sites_verify", "arguments": { "site_id": 123, "path": "/" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#sites-verify). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `themes_activate` **Use this theme** · **destructive** · permission `sites:write` · REST equivalent `POST /sites/{site_id}/themes/{slug}/activate` Switches the site to another theme. This changes how every page looks at once - check the site with sites.verify straight after, and switch back if it is wrong. Needs the `sites:write` permission. DESTRUCTIVE: this cannot be undone. Confirm with the user before calling it. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | | `slug` | string | yes | The theme folder name. | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "themes_activate", "arguments": { "site_id": 123, "slug": "twentytwentyfive" } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#themes-activate). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. ### `themes_list` **Themes on this site** · read-only · permission `sites:read` · REST equivalent `GET /sites/{site_id}/themes` Every theme on a WordPress site and which one is in use. Needs the `sites:read` permission. | Argument | Type | Required | Description | |---|---|---|---| | `site_id` | integer | yes | The site id (see GET /sites). | ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "themes_list", "arguments": { "site_id": 123 } } } ``` The answer carries the same data as the REST call, as `structuredContent`, with an example of every field in [Building, plugins and themes](https://app.pbn.ltd/api/docs/reference/devtools#themes-list). The exact JSON Schema the answer is validated against is the tool's own `outputSchema` in `tools/list`. --- # Account The account behind the key: profile, plan, site slots, limits and usage. ## Who am I `GET /me` · scope `account:read` The account and the key making the call. The cheapest call there is: use it to test a key. ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/me" \ -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/me", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/me", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 1234, "username": "example", "email": "you@example.com", "first_name": "Ann", "last_name": "Example", "company": null, "country": "GB", "staff": false, "created_at": "2025-01-10T09:00:00Z", "last_login_at": "2026-09-19T21:03:11Z", "key": { "name": "Dashboard", "prefix": "pbn_1a2b3c4d5e6f", "kind": "key", "full_access": false, "scopes": [ "account:read", "sites:read" ], "site_ids": null, "expires_at": null, "created_at": "2026-09-20T08:00:00Z" } } } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Plan, slots, limits and frozen sites `GET /account/limits` · scope `account:read` Site slots (plan + extra-site add-ons), subscription state and paid-until date, the default per-site limits, every paused (frozen) site with the reason(s) it is paused, and the add-ons on the account. ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/account/limits" \ -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/account/limits", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/account/limits", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "slots": { "used": 12, "limit": 20, "from_plan": 20, "extra_site_addons": 0, "free": 8, "can_create": true }, "subscription": { "state": "active", "paid_until": "2026-10-19T00:00:00Z", "expired": false, "days_left": 29 }, "default_site_limits": { "disk_mib": 1024, "database_mib": 500, "files": 50000 }, "frozen_sites": { "count": 1, "sites": [ { "site_id": 123, "domain": "example.com", "reasons": [ { "code": "site_oversize", "text": "The site is over its disk space or file-count limit." } ], "temporary_unfreeze": { "available": true, "why_not": null, "left_today": 3 } } ] }, "addons": { "extra_sites": [], "usage": [], "site_cleaner": { "active": false, "ends_at": null }, "wayback_credits": 0, "staff_free": false } } } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Measure usage of the whole account now `POST /account/limits/refresh` · scope `account:write` Starts a fresh disk / database / file-count measurement for the account's sites - the usage tab's "Check now", for many sites at once. It returns at once: each site is reported as started, busy, throttled (the panel's own per-site cooldown and per-account budget apply unchanged) or skipped with the reason. Poll GET /account/limits or GET /sites/{site_id}/usage for the new figures a few seconds later. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_ids` | body | array | no | Only these sites (default: every installed site of the account, newest first). | | `max_sites` | body | integer | no | How many sites to start a measurement for in this call (1-100). Default: `25`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/account/limits/refresh" \ -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/account/limits/refresh", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/account/limits/refresh", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "requested": 12, "started": 9, "busy": 1, "throttled": 2, "skipped": 0, "sites": [ { "site_id": 123, "domain": "example.com", "result": "started", "message": null } ], "limits": { "slots": { "used": 12, "limit": 20 } } } } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized`, `unavailable` ## API limits for this key `GET /limits` · scope `kb:read` The rate limits and size limits that apply to the key making the call. `overridden` is true when support has raised or lowered a limit for this key or for the whole account. ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/limits" \ -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/limits", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/limits", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "per_minute": 120, "per_hour": 5000, "writes_per_minute": 30, "upload_max_mb": 25, "read_max_mb": 5, "max_keys_per_customer": 50, "overridden": false, "override_scope": null } } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` --- # Billing Subscription, payment due, invoices and invoice PDFs, add-ons. ## Billing overview `GET /billing` · scope `billing:read` Subscription (state, plan, paid-until date, days left, payment method), unpaid state with the date sites are removed if it stays unpaid, site slots, add-ons, VAT treatment and the panel links. ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/billing" \ -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/billing", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/billing", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "subscription": { "state": "active", "payment_method": "paypal", "paid_until": "2026-10-19T00:00:00Z", "expired": false, "days_left": 29, "trial": false, "next_plan": null, "pending_change": false, "cancelled": false, "staff_account": false, "plans": [ { "name": "Standard", "slug": "standard", "sites_per_unit": 20, "units": 1, "sites": 20, "interval": "month", "trial": false, "price_usd": "49.00", "price_eur": "45.00", "price_gbp": "39.00" } ] }, "slots": { "used": 12, "limit": 20 }, "addons": {}, "vat": { "treatment": "reverse_charge", "rate_percent": "0", "country": "DE" }, "invoices_url": "https://app.pbn.ltd/invoices/" } } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Payments and invoices `GET /billing/invoices` · scope `billing:read` Every payment and refund of the account, newest first. `document` is the invoice / credit note when one has been issued; download it with GET /billing/invoices/{number}.pdf. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `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/billing/invoices?limit=10" \ -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/billing/invoices?limit=10", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/billing/invoices?limit=10", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "key": "pp:I-ABC123", "date": "2026-09-01T10:00:00Z", "description": "Hosting plan (PayPal subscription)", "amount": "49.00", "currency": "USD", "kind": "payment", "seller": "pbn", "document": { "number": "PBN-2026-001234", "type": "invoice", "total": "49.00", "vat": "0.00", "net": "49.00", "issued_at": "2026-09-01T10:05:00Z", "pdf_api_url": "https://app.pbn.ltd/api/v1/billing/invoices/PBN-2026-001234.pdf", "pdf_panel_url": "https://app.pbn.ltd/invoices/PBN-2026-001234.pdf" } } ], "next_cursor": null, "has_more": false } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Download an invoice PDF `GET /billing/invoices/{number}.pdf` · scope `billing:read` The PDF of an invoice or credit note of this account (Content-Type application/pdf). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `number` | path | string | yes | Invoice number, e.g. PBN-2026-001234. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/billing/invoices/PBN-2026-001234.pdf" \ -H "Authorization: Bearer $PBN_API_KEY" \ -o invoice.pdf ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.get("https://app.pbn.ltd/api/v1/billing/invoices/PBN-2026-001234.pdf", headers=headers, timeout=120) r.raise_for_status() open("invoice.pdf", "wb").write(r.content) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/billing/invoices/PBN-2026-001234.pdf", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); const pdf = Buffer.from(await res.arrayBuffer()); // Node 18+ ``` ### Response `200` with the file (`application/pdf`). Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # 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` --- # Site tools Cache purge, one-click admin login, reinstall, health checks, Site Cleaner, usage. ## Change the PHP version `PUT /sites/{site_id}/php-version` · scope `sites:write` Shortcut for PATCH /sites/{site_id} with php_version. The web server is reconfigured in the background (a job is returned). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `php_version` | body | string | yes | e.g. "PHP 8.3" (see GET /sites/{site_id}/options) | ### Example ```bash curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/php-version" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"php_version": "PHP 8.3"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.put("https://app.pbn.ltd/api/v1/sites/123/php-version", headers=headers, json={"php_version": "PHP 8.3"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/php-version", { method: "PUT", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"php_version": "PHP 8.3"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "php_version": "PHP 8.3", "state": "Creating webserver config" } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Disk, database and file usage `GET /sites/{site_id}/usage` · scope `sites:read` The Usage & add-ons tab: disk, database and file-count use against the limits (plan + add-ons), what is over, CDN bandwidth for metered CDNs, and the add-ons on the site. Numbers are measured every few hours; POST /sites/{site_id}/usage/refresh measures now. ### 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/usage" \ -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/usage", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/usage", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "state": "Ok", "frozen": false, "over": [], "disk": { "used_mib": 310, "limit_mib": 1024, "included_mib": 1024, "addon_mib": 0, "percent": 30, "over": false, "measured_at": "2026-09-19T18:00:00+00:00" }, "db": { "used_mib": 40, "limit_mib": 500 }, "inodes": { "used": 8200, "limit": 50000 }, "bandwidth": null, "addons": [], "busy": false } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Measure usage now `POST /sites/{site_id}/usage/refresh` · scope `sites:write` Starts a fresh measurement of disk, database and files (the usage tab's "Check now"). Returns the current figures with busy=true; poll GET /sites/{site_id}/usage until busy is false (seconds). Same throttles as the panel: one measurement per site every few minutes. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/usage/refresh" \ -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/sites/123/usage/refresh", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/usage/refresh", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "busy": true, "disk": { "used_mib": 310 } } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable` ## Purge the CDN cache `POST /sites/{site_id}/purge-cache` · scope `sites:write` Clears the CDN cache of both hostnames (as Purge CDN cache in the panel). The site must be live. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/purge-cache" \ -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/sites/123/purge-cache", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/purge-cache", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "purged": true } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Reinstall from scratch `POST /sites/{site_id}/reinstall` · scope `sites:write` · **destructive** Installs the site again from scratch (as Reinstall in the panel) - the current files and database are replaced. Refused while the site is busy, switched off or being deleted. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `confirm` | body | boolean | yes | Must be true: this action overwrites the site. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/reinstall" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"confirm": true}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/reinstall", headers=headers, json={"confirm": True}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/reinstall", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"confirm": true}) }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "reinstalling": true } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## One-click admin login link `POST /sites/{site_id}/admin-login` · scope `sites:login` A single-use login link to the site's admin (WordPress wp-admin, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki), valid for 60 seconds - the panel's one-click login. Open it in a browser; do not store it. Not for Static HTML / PHP hosting sites. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/admin-login" \ -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/sites/123/admin-login", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/admin-login", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "url": "https://example.com/?ltoken=0f3c...", "expires_in_seconds": 60, "single_use": true } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Health status `GET /sites/{site_id}/health` · scope `sites:read` The online badge (online / offline / slow / paused... with the reason) and, for application sites, the last integrity check (V1 config lines, admin account, login health, core files). ### 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/health" \ -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/health", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/health", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "online": { "status": "online", "label": "Online" }, "integrity": { "checked": "19 Sep 2026 20:00 UTC", "status": "ok", "findings": [], "problems": 0 } } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Re-check if the site is up `POST /sites/{site_id}/health/recheck` · scope `sites:write` Probes the home page again now (same probe and recording as the platform's monitor) and asks the server why if it fails. Asynchronous: follow the job; its result has the outcome. At most 10 an hour. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/health/recheck" \ -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/sites/123/health/recheck", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/health/recheck", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "started": true } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Integrity check now `POST /sites/{site_id}/health/integrity-check` · scope `sites:write` Checks the application now (WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki): V1's config lines, the site URL, the admin account, one-click login health and core file checksums. Changes nothing. Takes a few seconds. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/health/integrity-check" \ -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/sites/123/health/integrity-check", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/health/integrity-check", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "checked": "20 Sep 2026 08:00 UTC", "status": "ok", "findings": [], "problems": 0, "message": "No problems found." } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Repair the site `POST /sites/{site_id}/health/repair` · scope `sites:write` "Repair site": checks, repairs everything repairable (backups first, on the server), checks again. You get an e-mail listing what was repaired. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/health/repair" \ -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/sites/123/health/repair", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/health/repair", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "status": "ok", "fixed": [ "WordPress core files restored" ], "message": "Repaired." } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Switch a paused site on for cleanup `POST /sites/{site_id}/temporary-unfreeze` · scope `sites:write` A site paused ONLY for a usage limit (disk / database / files) comes back for 30 minutes so you can clean it up (3 times per site per day). At the end it is paused again only if still over the limit. freeze.temporary_unfreeze in GET /sites/{site_id} says if it is available and why not. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/temporary-unfreeze" \ -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/sites/123/temporary-unfreeze", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/temporary-unfreeze", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "active": true, "ends_at": "2026-09-20T09:30:00+00:00", "message": "example.com is coming back online for 30 minutes..." } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Site Cleaner status `GET /sites/{site_id}/cleaner` · scope `sites:read` Site Cleaner for this site: whether the add-on is active, the last scan (what can be removed and how much it saves), the running job, recent jobs (with undo_available) and the schedule. ### 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/cleaner" \ -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/cleaner", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cleaner", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "cms": "wordpress", "usable": true, "why_not": null, "addon_active": true, "schedule": "off", "last_scan": { "items": [ { "id": "plugin:hello-dolly", "cat": "plugins", "name": "Hello Dolly", "mb": 0.1, "default": true } ], "at": "2026-09-19T10:00:00Z" }, "running": null, "recent_jobs": [], "buy_url": "https://app.pbn.ltd/site-cleaner/" } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Scan, clean or undo `POST /sites/{site_id}/cleaner/{action}` · scope `sites:write` Runs Site Cleaner. `scan` lists unused plugins/themes, junk files and database clutter (free). `clean` removes the selected items after taking a backup and checks the site afterwards - it needs the Site Cleaner add-on (402 addon_required otherwise). `undo` puts a clean back from its backup. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `action` | path | string (one of: scan, clean, undo) | yes | scan (free) / clean (needs the Site Cleaner add-on) / undo a clean. | | `items` | body | array | no | clean: ids from last_scan.items to remove (omit = the default selection). | | `job_id` | body | integer | no | undo: the id of the clean job to undo. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/cleaner/scan" \ -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/sites/123/cleaner/scan", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cleaner/scan", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "cleaner_job": { "id": 991, "kind": "scan", "state": "queued" } } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Automatic cleaning `PUT /sites/{site_id}/cleaner/schedule` · scope `sites:write` Sets automatic cleaning for the site (needs the add-on to actually clean). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `schedule` | body | string (one of: off, daily, weekly, monthly) | yes | How often Site Cleaner runs by itself. | ### Example ```bash curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/cleaner/schedule" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"schedule": "weekly"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.put("https://app.pbn.ltd/api/v1/sites/123/cleaner/schedule", headers=headers, json={"schedule": "weekly"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/cleaner/schedule", { method: "PUT", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"schedule": "weekly"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "schedule": "weekly", "text": "weekly" } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # DNS records The site's own DNS records (A, AAAA, CNAME, TXT, MX, SRV, CAA, NS). ## Nameserver status `GET /sites/{site_id}/nameservers` · scope `dns:read` The nameservers the domain must use (required), what it uses now (current, checked daily), whether it is pointed, whether the DNS zone expired at the provider (zone_expired: re-create it from the site page), and the registrar autopilot status when a registrar connection sets them for you. ### 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/nameservers" \ -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/nameservers", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/nameservers", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "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 } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## List DNS records `GET /sites/{site_id}/dns` · scope `dns:read` The site's own DNS records (the DNS records tab), and the publishing state: the DNS provider, when the records were last published and any record the provider refused (with its error). Records the platform manages for the CDN are not listed and are never changed through this API. ### 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/dns" \ -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/dns", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/dns", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "records": [ { "id": 5501, "type": "MX", "name": "@", "value": "10 mail.example.com", "proxied": false, "updated_at": "2026-09-19T10:00:00Z", "parts": { "priority": 10, "target": "mail.example.com" } } ], "publishing": { "provider": "Cloudflare", "errors": [], "last_synced_at": "2026-09-19T10:01:00Z" } } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Add a DNS record `POST /sites/{site_id}/dns` · scope `dns:write` Adds a record with the panel's own validation. It is published to the site's DNS provider within a minute or two (follow the job); your records are never overwritten by the platform. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `type` | body | string (one of: A, AAAA, CNAME, TXT, MX, SRV, CAA, NS) | yes | Record type. | | `name` | body | string | yes | "@" for the domain itself, or a name like blog, shop.eu, _dmarc, _sip._tcp. | | `value` | body | string | yes | IPv4 (A), IPv6 (AAAA), host name (CNAME, MX, SRV target, NS), text (TXT) or CA domain (CAA). | | `priority` | body | integer | no | MX and SRV. | | `weight` | body | integer | no | SRV. | | `port` | body | integer | no | SRV. | | `caa_flags` | body | integer (one of: 0, 128) | no | CAA: 0 or 128 (critical). | | `caa_tag` | body | string (one of: issue, issuewild, iodef) | no | CAA tag. | | `proxied` | body | boolean | no | Cloudflare only, A/AAAA/CNAME: proxy through the CDN (off = DNS only). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/dns" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"type": "TXT", "name": "@", "value": "google-site-verification=abc123"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/dns", headers=headers, json={"type": "TXT", "name": "@", "value": "google-site-verification=abc123"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/dns", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"type": "TXT", "name": "@", "value": "google-site-verification=abc123"}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "id": 5501, "type": "MX", "name": "@", "value": "10 mail.example.com", "proxied": false, "updated_at": "2026-09-19T10:00:00Z", "parts": { "priority": 10, "target": "mail.example.com" } } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Change a DNS record `PATCH /sites/{site_id}/dns/{record_id}` · scope `dns:write` Changes a record; send only the fields that change. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `record_id` | path | integer | yes | The DNS record id. | | `type` | body | string (one of: A, AAAA, CNAME, TXT, MX, SRV, CAA, NS) | no | Record type. | | `name` | body | string | no | "@" for the domain itself, or a name like blog, shop.eu, _dmarc, _sip._tcp. | | `value` | body | string | no | IPv4 (A), IPv6 (AAAA), host name (CNAME, MX, SRV target, NS), text (TXT) or CA domain (CAA). | | `priority` | body | integer | no | MX and SRV. | | `weight` | body | integer | no | SRV. | | `port` | body | integer | no | SRV. | | `caa_flags` | body | integer (one of: 0, 128) | no | CAA: 0 or 128 (critical). | | `caa_tag` | body | string (one of: issue, issuewild, iodef) | no | CAA tag. | | `proxied` | body | boolean | no | Cloudflare only, A/AAAA/CNAME: proxy through the CDN (off = DNS only). | ### Example ```bash curl -s -X PATCH "https://app.pbn.ltd/api/v1/sites/123/dns/5501" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"value": "google-site-verification=xyz789"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.patch("https://app.pbn.ltd/api/v1/sites/123/dns/5501", headers=headers, json={"value": "google-site-verification=xyz789"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/dns/5501", { method: "PATCH", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"value": "google-site-verification=xyz789"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 5501, "type": "MX", "name": "@", "value": "10 mail.example.com", "proxied": false, "updated_at": "2026-09-19T10:00:00Z", "parts": { "priority": 10, "target": "mail.example.com" } } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Delete a DNS record `DELETE /sites/{site_id}/dns/{record_id}` · scope `dns:write` Deletes a record; it is removed at the DNS provider within a minute or two. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `record_id` | path | integer | yes | The DNS record id. | ### Example ```bash curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/dns/5501" \ -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/dns/5501", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/dns/5501", { method: "DELETE", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "deleted": true, "id": 5501 } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Backups List, create, download and restore backups. ## List backups `GET /sites/{site_id}/backups` · scope `backups:read` The site's backups, newest first (automatic daily ones, manual ones and uploaded ones). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `state` | query | string (one of: Pending, Creating, Ok, Error, Storing) | no | Only backups in this state. | | `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/123/backups?limit=10" \ -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/backups?limit=10", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/backups?limit=10", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "id": 88001, "created_at": "2026-09-19T09:12:00Z", "type": "Automatic", "state": "Ok", "size_bytes": 52428800, "is_blueprint": false, "locked": false } ], "next_cursor": null, "has_more": false } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Create a backup now `POST /sites/{site_id}/backups` · scope `backups:write` Takes a full backup (files + database) now, as Create backup in the panel. Follow the job until the backup is Ok. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/backups" \ -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/sites/123/backups", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/backups", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "id": 88001, "created_at": "2026-09-19T09:12:00Z", "type": "Manual", "state": "Pending", "size_bytes": 52428800, "is_blueprint": false, "locked": false } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Get one backup `GET /sites/{site_id}/backups/{backup_id}` · scope `backups:read` One backup of the site. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `backup_id` | path | integer | yes | The backup id. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/backups/88001" \ -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/backups/88001", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/backups/88001", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 88001, "created_at": "2026-09-19T09:12:00Z", "type": "Automatic", "state": "Ok", "size_bytes": 52428800, "is_blueprint": false, "locked": false } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Download link `POST /sites/{site_id}/backups/{backup_id}/download-link` · scope `backups:read` A temporary link to download the backup archive (tar.gz) - valid about 4 hours. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `backup_id` | path | integer | yes | The backup id. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/backups/88001/download-link" \ -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/sites/123/backups/88001/download-link", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/backups/88001/download-link", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "url": "https://dl.dropboxusercontent.com/...", "expires_in_seconds": 14400, "size_bytes": 52428800 } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable` ## Restore a backup `POST /sites/{site_id}/backups/{backup_id}/restore` · scope `backups:write` · **destructive** Replaces the site's files and database with the backup (as Restore in the panel). Refused while the site is busy, switched off or the plan has expired. Follow the job. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `backup_id` | path | integer | yes | The backup id. | | `confirm` | body | boolean | yes | Must be true: this action overwrites the site. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/backups/88001/restore" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"confirm": true}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/backups/88001/restore", headers=headers, json={"confirm": True}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/backups/88001/restore", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"confirm": true}) }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "restoring": true } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Delete a backup `DELETE /sites/{site_id}/backups/{backup_id}` · scope `backups:write` · **destructive** Deletes a backup. A backup that a blueprint uses cannot be deleted. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `backup_id` | path | integer | yes | The backup id. | ### Example ```bash curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/backups/88001" \ -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/backups/88001", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/backups/88001", { method: "DELETE", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "deleted": true, "id": 88001 } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Files Browse, read, upload, rename and delete the files of a site. ## List a folder `GET /sites/{site_id}/files` · scope `files:read` Folders first, then files; up to 5,000 entries. Symbolic links are listed (type "link") but never followed. Available under the same rule as the File Manager: the site is installed, the plan is active and the site is not suspended. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | query | string | no | Folder path relative to the site folder (default: the site folder itself). | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/files?path=wp-content" \ -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/files?path=wp-content", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files?path=wp-content", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "path": "wp-content", "items": [ { "name": "themes", "type": "folder", "size": null, "modified": "2026-09-01T10:00:00Z", "mode": "644", "path": "wp-content/themes" } ], "truncated": false } } ``` Errors: `conflict`, `forbidden`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## File or folder details `GET /sites/{site_id}/files/stat` · scope `files:read` Type, size, modification time and mode of one path. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | query | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/files/stat?path=index.php" \ -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/files/stat?path=index.php", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/stat?path=index.php", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "name": "index.php", "type": "file", "size": 405, "modified": "2026-09-01T10:00:00Z", "mode": "644", "path": "index.php" } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Read (download) a file `GET /sites/{site_id}/files/content` · scope `files:read` Returns a file of up to 5 MB (the limit is in GET /limits). Use encoding=raw to stream the bytes directly. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | query | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). | | `encoding` | query | string (one of: base64, text, raw) | no | base64 (JSON, any file), text (JSON, UTF-8 files) or raw (the bytes themselves, with a Content-Type). Default: `base64`. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/files/content?path=robots.txt&encoding=text" \ -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/files/content?path=robots.txt&encoding=text", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/content?path=robots.txt&encoding=text", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "path": "robots.txt", "size": 67, "modified": "2026-09-01T10:00:00Z", "content_type": "text/plain", "content": "User-agent: *\nDisallow:\n", "encoding": "text" } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized` ## Upload (write) a file `PUT /sites/{site_id}/files/content` · scope `files:write` Writes a file of up to 25 MB atomically (a temporary file renamed into place), owned by the site's web server user. Refuses to write through symbolic links and into folders the platform manages. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | query | string | yes | Where to write it, relative to the site folder. | | `content` | body | string | no | Text content (UTF-8). Or use content_base64. | | `content_base64` | body | string | no | The file, base64-encoded. Or send the raw bytes as the request body (Content-Type application/octet-stream) or a multipart form with a "file" field. | | `overwrite` | query | boolean | no | Replace an existing file. Default: `False`. | | `mkdirs` | query | boolean | no | Create missing folders. Default: `True`. | ### Example ```bash curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/files/content?path=hello.txt&overwrite=true" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"content": "Hello world\n"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.put("https://app.pbn.ltd/api/v1/sites/123/files/content?path=hello.txt&overwrite=true", headers=headers, json={"content": "Hello world\n"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/content?path=hello.txt&overwrite=true", { method: "PUT", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"content": "Hello world\n"}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "name": "hello.txt", "type": "file", "size": 12, "modified": "2026-09-01T10:00:00Z", "mode": "644", "path": "hello.txt", "written": 12 } } ``` Errors: `conflict`, `forbidden`, `not_found`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized` ## Create a folder `POST /sites/{site_id}/files/folders` · scope `files:write` Creates a folder (and any missing parent folders). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | body | string | yes | The new folder path. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/files/folders" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "wp-content/uploads/reports"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/files/folders", headers=headers, json={"path": "wp-content/uploads/reports"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/folders", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"path": "wp-content/uploads/reports"}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "name": "new", "type": "folder", "size": null, "modified": "2026-09-01T10:00:00Z", "mode": "755", "path": "wp-content/new" } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Rename or move `POST /sites/{site_id}/files/move` · scope `files:write` Renames or moves a file or folder inside the site folder. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | body | string | yes | What to move. | | `to` | body | string | yes | The new path. | | `overwrite` | body | boolean | no | Replace an existing file at the destination. Default: `False`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/files/move" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "hello.txt", "to": "old/hello.txt"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/files/move", headers=headers, json={"path": "hello.txt", "to": "old/hello.txt"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/move", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"path": "hello.txt", "to": "old/hello.txt"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "name": "index.php", "type": "file", "size": 405, "modified": "2026-09-01T10:00:00Z", "mode": "644", "path": "index.php", "moved_from": "old.php" } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Delete a file or folder `DELETE /sites/{site_id}/files` · scope `files:write` · **destructive** Deletes a file, or a folder (with recursive=true when it is not empty). There is no undo - take a backup first for anything important. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | query | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). | | `recursive` | query | boolean | no | Required to delete a folder that is not empty. Default: `False`. | ### Example ```bash curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/files?path=hello.txt" \ -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/files?path=hello.txt", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files?path=hello.txt", { method: "DELETE", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "path": "old.zip", "deleted": true, "entries_removed": 1 } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Logs Error log and access log of a site. ## Error log `GET /sites/{site_id}/logs/errors` · scope `logs:read` Recent PHP and web server errors of THIS site (the Error log tab): last 7 days, grouped, sanitised (paths shown relative to the site folder, secrets hidden). php_hint=true when the errors look like code written for an older PHP version. Cached 60 s; 20 fetches per 10 minutes per account. ### 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/logs/errors" \ -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/logs/errors", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/logs/errors", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "entries": [ { "level": "Fatal error", "message": "Uncaught Error: Call to undefined function mysql_connect() in /wp-content/plugins/old/db.php:12", "count": 3, "first": "2026-09-19T08:00:00", "last": "2026-09-19T09:30:00" } ], "total": 3, "partial": false, "lookback_days": 7, "php_version": "PHP 8.3", "php_hint": true, "fetched_at": "2026-09-20 09:00:00 UTC", "seconds": 1.8, "cached": false } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Access log `GET /sites/{site_id}/logs/access` · scope `logs:read` Requests that reached the web server for this site's hostnames, newest last. The client IP is the visitor as the CDN reported it. 30 fetches per 10 minutes per account. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `hours` | query | integer | no | How far back to look. Default: `1`. | | `lines` | query | integer | no | At most this many (the newest) lines. Default: `200`. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/logs/access?hours=1&lines=100" \ -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/logs/access?hours=1&lines=100", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/logs/access?hours=1&lines=100", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "entries": [ { "time": "2026-09-20T08:59:58+00:00", "client_ip": "203.0.113.9", "host": "example.com", "method": "GET", "path": "/", "protocol": "HTTP/1.1", "status": 200, "bytes": 5123, "referer": null, "user_agent": "Mozilla/5.0 ...", "scheme": "https" } ], "count": 1, "status_counts": { "2xx": 1 }, "hours": 1, "partial": false, "note": "Requests the CDN answered from its cache never reach the server and are not in this log." } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Support tickets Open support tickets and reply to them. ## Ticket categories `GET /tickets/queues` · scope `tickets:read` The categories (queues) a ticket can be opened in. ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/tickets/queues" \ -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/tickets/queues", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/tickets/queues", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "slug": "technical-problems", "title": "Technical Problems" } ] } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## List tickets `GET /tickets` · scope `tickets:read` Your tickets, newest first. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `status` | query | string (one of: open, closed) | no | Only open or only closed tickets. | | `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/tickets?status=open" \ -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/tickets?status=open", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/tickets?status=open", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "id": 4412, "reference": "general-support-request-4412", "title": "Site shows error 500", "queue": "General Support Request", "status": "open", "priority": "normal", "created_at": "2026-09-20T08:00:00Z", "updated_at": "2026-09-20T08:00:00Z", "panel_url": "https://app.pbn.ltd/account/tickets" } ], "next_cursor": null, "has_more": false } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Read a ticket `GET /tickets/{ticket_id}` · scope `tickets:read` A ticket with every public message (yours and support's). can_reply says if you may add a message now (ticket limits: one message before support answers). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `ticket_id` | path | integer | yes | The ticket id. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/tickets/4412" \ -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/tickets/4412", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/tickets/4412", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 4412, "reference": "general-support-request-4412", "title": "Site shows error 500", "queue": "General Support Request", "status": "open", "priority": "normal", "created_at": "2026-09-20T08:00:00Z", "updated_at": "2026-09-20T08:00:00Z", "panel_url": "https://app.pbn.ltd/account/tickets", "resolution": null, "can_reply": true, "can_reply_reason": null, "messages": [ { "at": "2026-09-20T08:00:00Z", "from": "you", "text": "My site shows error 500 since this morning." }, { "at": "2026-09-20T08:40:00Z", "from": "support", "text": "We are looking at it now.", "attachments": [] } ] } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Open a ticket `POST /tickets` · scope `tickets:write` Opens a ticket (support is e-mailed as for a panel ticket). The ticket limits apply: at most 2 open tickets at a time (409 ticket_limit). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `title` | body | string | yes | Short summary. | | `message` | body | string | yes | What happened, what you expected. | | `queue` | body | string | no | Category slug (GET /tickets/queues). Default: `general-support-request`. | | `priority` | body | string (one of: low, normal, high) | no | Priority. Default: `normal`. | | `site_id` | body | integer | no | The site it is about (its domain is added to the ticket). | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/tickets" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title": "Question about my site", "message": "How do I add a subdomain?", "queue": "general-support-request", "site_id": 123}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/tickets", headers=headers, json={"title": "Question about my site", "message": "How do I add a subdomain?", "queue": "general-support-request", "site_id": 123}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/tickets", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"title": "Question about my site", "message": "How do I add a subdomain?", "queue": "general-support-request", "site_id": 123}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "id": 4412, "reference": "general-support-request-4412", "title": "Site shows error 500", "queue": "General Support Request", "status": "open", "priority": "normal", "created_at": "2026-09-20T08:00:00Z", "updated_at": "2026-09-20T08:00:00Z", "panel_url": "https://app.pbn.ltd/account/tickets" } } ``` Errors: `conflict`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Reply to a ticket `POST /tickets/{ticket_id}/messages` · scope `tickets:write` Adds your message to the ticket (support is notified). One message until support answers. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `ticket_id` | path | integer | yes | The ticket id. | | `message` | body | string | yes | Your message. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/tickets/4412/messages" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"message": "Thanks, that worked."}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/tickets/4412/messages", headers=headers, json={"message": "Thanks, that worked."}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/tickets/4412/messages", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"message": "Thanks, that worked."}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "id": 4412, "reference": "general-support-request-4412", "title": "Site shows error 500", "queue": "General Support Request", "status": "open", "priority": "normal", "created_at": "2026-09-20T08:00:00Z", "updated_at": "2026-09-20T08:00:00Z", "panel_url": "https://app.pbn.ltd/account/tickets", "messages": [] } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Knowledge base Search the knowledge base. ## Help articles for this site `GET /sites/{site_id}/help` · scope `sites:read` Knowledge-base articles that fit what is going on with this site right now (why it is paused, nameservers not pointed, offline...), with app and public links. ### 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/help" \ -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/help", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/help", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "topics": [ "change nameservers at registrar" ], "articles": [ { "title": "How to change nameservers", "slug": "change-nameservers", "app_url": "https://app.pbn.ltd/kb/a/change-nameservers/", "public_url": "https://pbn.ltd/knowledgebase/change-nameservers/", "because": "change nameservers at registrar" } ] } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Search the knowledge base `GET /kb/search` · scope `kb:read` The dashboard's knowledge-base search (keyword + meaning). Every key may use it. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `q` | query | string | yes | What you are looking for, in plain words. | | `limit` | query | integer | no | At most this many. Default: `8`. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/kb/search?q=change+nameservers" \ -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/kb/search?q=change+nameservers", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/kb/search?q=change+nameservers", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "query": "change nameservers", "mode": "semantic", "confident": true, "note": null, "results": [ { "title": "How to change nameservers", "category": "DNS", "snippet": "...", "slug": "change-nameservers", "app_url": "https://app.pbn.ltd/kb/a/change-nameservers/", "public_url": "https://pbn.ltd/knowledgebase/change-nameservers/" } ] } } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Read an article `GET /kb/articles/{slug}` · scope `kb:read` One article as plain text and as sanitised HTML, with related articles. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `slug` | path | string | yes | Article slug from a search result. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/kb/articles/change-nameservers" \ -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/kb/articles/change-nameservers", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/kb/articles/change-nameservers", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "slug": "change-nameservers", "title": "How to change nameservers", "category": "DNS", "excerpt": "...", "text": "plain text...", "html": "

...

", "updated_at": null, "app_url": "https://app.pbn.ltd/kb/a/change-nameservers/", "public_url": "https://pbn.ltd/knowledgebase/change-nameservers/", "related": [] } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Jobs Progress of the long-running actions the API started. ## List jobs `GET /jobs` · scope `account:read` Jobs the API started for this account, newest first. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `status` | query | string (one of: running, succeeded, failed) | no | Only jobs in this state. | | `site_id` | query | integer | no | Only jobs of this site. | | `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/jobs?status=running" \ -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/jobs?status=running", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/jobs?status=running", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "id": "job_4f1c0a9e2b7d6c5a3e10", "kind": "backup.create", "status": "running", "message": "In progress (state: Creating).", "site_id": 123, "object_type": "backup", "object_id": 88001, "created_at": "2026-09-20T09:00:00Z", "finished_at": null, "url": "https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10" } ], "next_cursor": null, "has_more": false } ``` Errors: `rate_limited`, `scope_missing`, `unauthorized` ## Job status `GET /jobs/{job_id}` · scope `account:read` The live status of a job: running, succeeded or failed, with a message. Poll every few seconds (it counts against the rate limit like any call). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `job_id` | path | string | yes | The id returned as job.id. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10" \ -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/jobs/job_4f1c0a9e2b7d6c5a3e10", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": "job_4f1c0a9e2b7d6c5a3e10", "kind": "backup.create", "status": "running", "message": "In progress (state: Creating).", "site_id": 123, "object_type": "backup", "object_id": 88001, "created_at": "2026-09-20T09:00:00Z", "finished_at": null, "url": "https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10" } } ``` Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized` --- # Content and publishing Posts, pages, pictures and the AI auto-posting campaigns, on every site type. ## What a post on this site can have `GET /sites/{site_id}/content/options` · scope `content:read` Before writing anything, ask this: it says how a post appears on this site type, which fields it supports (featured picture, categories, tags, author, drafts) and the real categories and authors the site has. Site types with no drafts publish straight away - the answer says so. ### 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/content/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/content/options", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content/options", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "domain": "example.com", "type": "Wordpress", "how": "Published as a WordPress post with its featured image, categories, tags and author.", "supports": { "featured": true, "categories": true, "tags": true, "authors": true, "drafts": true }, "statuses": [ "publish", "draft" ], "categories": [ { "id": "1", "name": "Uncategorised" } ], "authors": [ { "id": "1", "name": "Ann Example" } ], "media_uploads": true, "options_error": null } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Posts on this site `GET /sites/{site_id}/content` · scope `content:read` The latest posts the site itself reports, and every post this account has written for it through PBN.LTD (including ones still being written or waiting to go out). ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `limit` | query | integer | no | How many to return. Default: `20`. | | `state` | query | string (one of: queued, writing, images, review, ready, publishing, done, failed, cancelled) | no | Only posts in this state. | | `on_site` | query | boolean | no | Also ask the site itself for its latest posts (slower). Default: `True`. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/content" \ -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/content", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "on_site": [ { "title": "Welcome", "url": "https://example.com/welcome/" } ], "on_site_error": null, "ours": [ { "id": 90210, "site_id": 123, "title": "Five ways to speed up your shop", "slug": "five-ways", "state": "done", "state_text": "Published", "step": "Published", "source": "manual", "status": "publish", "tags": [ "speed", "shop" ], "excerpt": "A short summary of the post.", "url": "https://example.com/blog/five-ways/", "remote_id": "412", "publish_at": "2026-09-20T10:00:00Z", "published_at": "2026-09-20T10:00:14Z", "created_at": "2026-09-20T09:59:02Z", "error": null, "error_code": null, "campaign_id": null, "images": 1 } ] } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Everything being written or published `GET /content/queue` · scope `content:read` Every post on the account that is queued, being written, waiting for approval, scheduled, publishing, published, failed or cancelled - newest first, across all sites. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | query | integer | no | How many to return. Default: `20`. | | `cursor` | query | string | no | The next_cursor value of the previous page. | | `state` | query | string (one of: queued, writing, images, review, ready, publishing, done, failed, cancelled) | no | Only posts in this state. | | `site_id` | query | integer | no | Only this site. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/content/queue" \ -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/content/queue", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/content/queue", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "id": 90210, "site_id": 123, "title": "Five ways to speed up your shop", "slug": "five-ways", "state": "done", "state_text": "Published", "step": "Published", "source": "manual", "status": "publish", "tags": [ "speed", "shop" ], "excerpt": "A short summary of the post.", "url": "https://example.com/blog/five-ways/", "remote_id": "412", "publish_at": "2026-09-20T10:00:00Z", "published_at": "2026-09-20T10:00:14Z", "created_at": "2026-09-20T09:59:02Z", "error": null, "error_code": null, "campaign_id": null, "images": 1 } ], "next_cursor": null, "has_more": false } ``` Errors: `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Read one post `GET /sites/{site_id}/content/{post_id}` · scope `content:read` One post in full, with its text and the history of what happened to it. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `post_id` | path | integer | yes | The post id this API gave you when it was created. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/sites/123/content/90210" \ -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/content/90210", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content/90210", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 90210, "site_id": 123, "title": "Five ways to speed up your shop", "slug": "five-ways", "state": "done", "state_text": "Published", "step": "Published", "source": "manual", "status": "publish", "tags": [ "speed", "shop" ], "excerpt": "A short summary of the post.", "url": "https://example.com/blog/five-ways/", "remote_id": "412", "publish_at": "2026-09-20T10:00:00Z", "published_at": "2026-09-20T10:00:14Z", "created_at": "2026-09-20T09:59:02Z", "error": null, "error_code": null, "campaign_id": null, "images": 1, "body_html": "

The text of the post.

", "events": [ { "at": "2026-09-20T10:00:14Z", "kind": "step", "message": "Published: https://example.com/blog/five-ways/" } ] } } ``` Errors: `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Publish a post or page `POST /sites/{site_id}/content` · scope `content:write` · returns a job Writes the post and puts it on the site. Works for every site type: WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav and MediaWiki get a native post/article/page, and Static HTML and PHP hosting sites get a page built in the look of their own pages, with a blog index and sitemap.xml kept up to date. The answer carries a job - wait for it and then read the post to get its address. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `title` | body | string | yes | The title of the post. | | `body_html` | body | string | no | The text as HTML. Or send body_markdown. | | `body_markdown` | body | string | no | The text as Markdown (headings, lists, links, bold, code, quotes). | | `status` | body | string (one of: publish, draft) | no | "draft" only on site types that have drafts (see content.options). Default: `publish`. | | `category` | body | string | no | Category id or name, on site types that have them. | | `author` | body | string | no | Author id, on site types that have authors. | | `tags` | body | array | no | Tags for the post. | | `slug` | body | string | no | The address of the post; one is made from the title when you leave it out. | | `publish_at` | body | string | no | ISO date and time to publish it (default: now). | | `featured_image_base64` | body | string | no | The main picture, base64. It leads the post and becomes the featured image on site types that have one. | | `images_base64` | body | array | no | More pictures, base64; they are placed in the text. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/content" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title": "Five ways to speed up your shop", "body_markdown": "## Why speed matters\n\nA faster shop sells more.", "status": "publish", "tags": ["speed", "shop"]}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/content", headers=headers, json={"title": "Five ways to speed up your shop", "body_markdown": "## Why speed matters\n\nA faster shop sells more.", "status": "publish", "tags": ["speed", "shop"]}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"title": "Five ways to speed up your shop", "body_markdown": "## Why speed matters\n\nA faster shop sells more.", "status": "publish", "tags": ["speed", "shop"]}) }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "id": 90210, "site_id": 123, "title": "Five ways to speed up your shop", "slug": "five-ways", "state": "done", "state_text": "Published", "step": "Published", "source": "manual", "status": "publish", "tags": [ "speed", "shop" ], "excerpt": "A short summary of the post.", "url": "https://example.com/blog/five-ways/", "remote_id": "412", "publish_at": "2026-09-20T10:00:00Z", "published_at": "2026-09-20T10:00:14Z", "created_at": "2026-09-20T09:59:02Z", "error": null, "error_code": null, "campaign_id": null, "images": 1 }, "job": { "id": "job_4f1c0a9e2b7d6c5a3e10", "kind": "...", "status": "running", "url": "https://app.pbn.ltd/api/v1/jobs/job_4f1c0a9e2b7d6c5a3e10" } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized`, `validation_failed` ## Change a post `PATCH /sites/{site_id}/content/{post_id}` · scope `content:write` Changes a post that has not gone out yet. A post that is already on the site cannot be edited from here - remove it and write a new one. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `post_id` | path | integer | yes | The post id this API gave you when it was created. | | `title` | body | string | no | A new title. | | `body_html` | body | string | no | New text as HTML. | | `body_markdown` | body | string | no | New text as Markdown. | | `tags` | body | array | no | Replace the tags. | | `status` | body | string (one of: publish, draft) | no | | | `publish_at` | body | string | no | Move when it goes out. | | `approve` | body | boolean | no | Approve a post that is waiting for approval. | ### Example ```bash curl -s -X PATCH "https://app.pbn.ltd/api/v1/sites/123/content/90210" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"title": "Five ways to speed up your shop (updated)"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.patch("https://app.pbn.ltd/api/v1/sites/123/content/90210", headers=headers, json={"title": "Five ways to speed up your shop (updated)"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content/90210", { method: "PATCH", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"title": "Five ways to speed up your shop (updated)"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 90210, "site_id": 123, "title": "Five ways to speed up your shop", "slug": "five-ways", "state": "ready", "state_text": "Scheduled", "step": "Published", "source": "manual", "status": "publish", "tags": [ "speed", "shop" ], "excerpt": "A short summary of the post.", "url": "https://example.com/blog/five-ways/", "remote_id": "412", "publish_at": "2026-09-20T10:00:00Z", "published_at": "2026-09-20T10:00:14Z", "created_at": "2026-09-20T09:59:02Z", "error": null, "error_code": null, "campaign_id": null, "images": 1, "body_html": "

The new text.

" } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Take a post off the site `DELETE /sites/{site_id}/content/{post_id}` · scope `content:write` · **destructive** Removes a published post from the site, or cancels one that has not gone out yet. There is no undo. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `post_id` | path | integer | yes | The post id this API gave you when it was created. | ### Example ```bash curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/content/90210" \ -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/content/90210", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content/90210", { method: "DELETE", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 90210, "removed": true, "was": "published" } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Upload a picture or file `POST /sites/{site_id}/content/media` · scope `content:write` Puts a picture or file on the site and gives back the address to use in a post. On WordPress it goes into the media library; on every other site type it goes into the site's own files. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `name` | body | string | yes | The file name, e.g. "hero.jpg". | | `content_base64` | body | string | yes | The file itself, base64. | | `alt` | body | string | no | Alt text (WordPress media library). | | `folder` | body | string | no | Where to put it on site types with no media library. Default: `assets`. | | `overwrite` | body | boolean | no | Replace a file of the same name. Default: `False`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/content/media" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "hero.jpg", "content_base64": "iVBORw0KGgo=", "alt": "The shop front"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/content/media", headers=headers, json={"name": "hero.jpg", "content_base64": "iVBORw0KGgo=", "alt": "The shop front"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/content/media", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"name": "hero.jpg", "content_base64": "iVBORw0KGgo=", "alt": "The shop front"}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "name": "hero.jpg", "url": "https://example.com/wp-content/uploads/2026/09/hero.jpg", "media_id": "881", "where": "the WordPress media library", "bytes": 145322 } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized`, `validation_failed` ## List auto-posting campaigns `GET /campaigns` · scope `content:read` The AI auto-posting campaigns on the account: what they write, when they run next and how many articles they have made. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | query | integer | no | How many to return. Default: `20`. | | `cursor` | query | string | no | The next_cursor value of the previous page. | | `status` | query | string (one of: active, paused, problem, finished) | no | | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/campaigns" \ -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/campaigns", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/campaigns", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "id": 44, "name": "Weekly shop tips", "status": "active", "status_reason": null, "schedule": "weekly", "times": "09:00", "weekdays": "0,3", "timezone": "Europe/London", "per_site": 1, "review": "auto", "total_limit": 0, "articles_created": 12, "next_run_at": "2026-09-23T08:00:00Z", "last_run_at": "2026-09-19T08:00:00Z", "site_ids": [ 123, 124 ], "created_at": "2026-07-01T10:00:00Z" } ], "next_cursor": null, "has_more": false } ``` Errors: `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Read one campaign `GET /campaigns/{campaign_id}` · scope `content:read` One campaign with its last ten runs. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `campaign_id` | path | integer | yes | The campaign. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/campaigns/44" \ -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/campaigns/44", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/campaigns/44", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 44, "name": "Weekly shop tips", "status": "active", "status_reason": null, "schedule": "weekly", "times": "09:00", "weekdays": "0,3", "timezone": "Europe/London", "per_site": 1, "review": "auto", "total_limit": 0, "articles_created": 12, "next_run_at": "2026-09-23T08:00:00Z", "last_run_at": "2026-09-19T08:00:00Z", "site_ids": [ 123, 124 ], "created_at": "2026-07-01T10:00:00Z", "runs": [ { "id": 7781, "trigger": "schedule", "slot_at": "2026-09-19T08:00:00Z", "total": 2, "done": 2, "failed": 0, "cancelled": 0, "finished_at": "2026-09-19T08:04:11Z", "state": "finished" } ] } } ``` Errors: `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Pause or resume `PUT /campaigns/{campaign_id}/status` · scope `content:write` Pauses a campaign or starts it again. A campaign paused by our staff or by a problem with an AI key cannot be resumed from here - fix the cause first. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `campaign_id` | path | integer | yes | | | `status` | body | string (one of: active, paused) | yes | "paused" stops it writing; "active" starts it again. | ### Example ```bash curl -s -X PUT "https://app.pbn.ltd/api/v1/campaigns/44/status" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": "paused"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.put("https://app.pbn.ltd/api/v1/campaigns/44/status", headers=headers, json={"status": "paused"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/campaigns/44/status", { method: "PUT", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"status": "paused"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 44, "name": "Weekly shop tips", "status": "active", "status_reason": null, "schedule": "weekly", "times": "09:00", "weekdays": "0,3", "timezone": "Europe/London", "per_site": 1, "review": "auto", "total_limit": 0, "articles_created": 12, "next_run_at": "2026-09-23T08:00:00Z", "last_run_at": "2026-09-19T08:00:00Z", "site_ids": [ 123, 124 ], "created_at": "2026-07-01T10:00:00Z" } } ``` Errors: `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Write one now `POST /campaigns/{campaign_id}/write-now` · scope `content:write` Asks a campaign to write and publish now, the same as the "Write now" button. Returns the run; follow it with runs.get. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `campaign_id` | path | integer | yes | | | `site_ids` | body | array | no | Only these sites of the campaign (default: all of them). | | `count` | body | integer | no | Articles per site. Default: `1`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/campaigns/44/write-now" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"count": 1}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/campaigns/44/write-now", headers=headers, json={"count": 1}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/campaigns/44/write-now", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"count": 1}) }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "run_id": 7782, "articles": 2, "problems": [] } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## How a run is going `GET /runs/{run_id}` · scope `content:read` One run of a campaign and every article in it. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `run_id` | path | integer | yes | The run. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/runs/7782" \ -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/runs/7782", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/runs/7782", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "id": 7782, "campaign_id": 44, "trigger": "manual", "slot_at": "2026-09-20T10:00:00Z", "total": 2, "done": 1, "failed": 0, "cancelled": 0, "finished_at": null, "state": "running", "articles": [ { "id": 90210, "site_id": 123, "title": "Five ways to speed up your shop", "slug": "five-ways", "state": "done", "state_text": "Published", "step": "Published", "source": "manual", "status": "publish", "tags": [ "speed", "shop" ], "excerpt": "A short summary of the post.", "url": "https://example.com/blog/five-ways/", "remote_id": "412", "publish_at": "2026-09-20T10:00:00Z", "published_at": "2026-09-20T10:00:14Z", "created_at": "2026-09-20T09:59:02Z", "error": null, "error_code": null, "campaign_id": null, "images": 1 } ] } } ``` Errors: `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` --- # Building, plugins and themes Upload a built site, set the home page, install and test plugins and themes, and the sites you publish to other hosts. ## Upload a built site (zip) `POST /sites/{site_id}/files/archive` · scope `files:write` Unpacks a zip of a built website (or a plugin or theme) into the site. Every file goes through the same path a single upload uses, so the same rules hold for each one: inside the site folder only, symbolic links are never followed and the platform's own folders are refused. Up to 60 files and 25 MB in one call - send a bigger site in parts. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `zip_base64` | body | string | yes | The .zip file, base64. | | `path` | body | string | no | Folder inside the site to unpack into (default: the site root). | | `strip_top_folder` | body | boolean | no | Drop the single top folder the zip may have ("mysite/index.html" -> "index.html"). Default: `False`. | | `overwrite` | body | boolean | no | Replace files that already exist. Default: `False`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/files/archive" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"zip_base64": "UEsDBAoAAAAAA...", "path": "", "strip_top_folder": true, "overwrite": true}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/files/archive", headers=headers, json={"zip_base64": "UEsDBAoAAAAAA...", "path": "", "strip_top_folder": True, "overwrite": True}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/archive", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"zip_base64": "UEsDBAoAAAAAA...", "path": "", "strip_top_folder": true, "overwrite": true}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "site_id": 123, "folder": "/", "written": 12, "failed": 0, "bytes": 284113, "files": [ { "path": "index.html", "bytes": 4201 } ], "problems": [] } } ``` Errors: `conflict`, `forbidden`, `not_found`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized`, `validation_failed` ## Set the home page `POST /sites/{site_id}/home-page` · scope `sites:write` · **destructive** Makes a page the front page of the site. On WordPress this sets the site's own "front page" setting; on the other site types the file you name is copied over index.html (or index.php), which replaces what is there now. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `page` | body | string | yes | WordPress: the page id or its exact title. Other site types: the file to use, e.g. "home.html". | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/home-page" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"page": "home.html"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/home-page", headers=headers, json={"page": "home.html"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/home-page", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"page": "home.html"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "home": "page 42", "url": "https://example.com/" } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Fetch a page and check it `POST /sites/{site_id}/verify` · scope `sites:read` Fetches a page of the site and says exactly what came back: the HTTP status, how long it took, its size, its title and any redirect. The page is always fetched on the site's own server (so it works even before the domain points at us) and, when the address allows it, from the internet as well. The newest PHP errors are read at the same time. This is the honest test after changing anything - a plugin, a theme, a file or a setting. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | body | string | no | The page to fetch, e.g. "/about". Default: `/`. | | `public` | body | boolean | no | Also try the page from the internet. Default: `True`. | | `errors` | body | boolean | no | Also read the PHP error log afterwards. Default: `True`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/verify" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/verify", headers=headers, json={"path": "/"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/verify", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"path": "/"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "url": "https://example.com/", "path": "/", "from_server": { "status": 200, "seconds": 0.31, "bytes": 51233, "redirect_to": null, "title": "Example - Home", "error": null }, "from_internet": { "status": 200, "content_type": "text/html", "bytes": 51233, "title": "Example - Home", "error": null }, "looks_ok": true, "php_errors": [], "php_errors_total": 0 } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Plugins on this site `GET /sites/{site_id}/plugins` · scope `sites:read` Every plugin installed on a WordPress site, whether it is active, its version and whether an update is waiting. ### 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/plugins" \ -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/plugins", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/plugins", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "plugins": [ { "slug": "my-plugin", "title": "My plugin", "status": "active", "version": "1.0.0", "update": "none", "auto_update": "off" } ], "count": 1 } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Install a plugin `POST /sites/{site_id}/plugins` · scope `sites:write` · **destructive** Installs a plugin on a WordPress site, from wordpress.org or from a zip you send. A plugin that is on our blocked list is refused. Activating a plugin can break a site: install first, then activate, then check the site with sites.verify and read the error log. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `slug` | body | string | no | A wordpress.org plugin slug, e.g. "classic-editor". | | `zip_base64` | body | string | no | Or your own plugin as a .zip file, base64. | | `activate` | body | boolean | no | Switch it on straight away. Test the site afterwards with sites.verify. Default: `False`. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/plugins" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"slug": "classic-editor", "activate": false}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/plugins", headers=headers, json={"slug": "classic-editor", "activate": False}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/plugins", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"slug": "classic-editor", "activate": false}) }); console.log(res.status, await res.json()); ``` ### Response `201` ```json { "data": { "site_id": 123, "installed": true, "activated": false, "output": "Plugin installed successfully.", "plugins": [ { "slug": "my-plugin", "title": "My plugin", "status": "active", "version": "1.0.0", "update": "none", "auto_update": "off" } ] } } ``` Errors: `conflict`, `forbidden`, `not_found`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized`, `validation_failed` ## Switch a plugin on or off `POST /sites/{site_id}/plugins/{slug}/{action}` · scope `sites:write` · **destructive** Activates or deactivates a plugin. Activating can take a site down, so check it with sites.verify afterwards; deactivating is how you put it back. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `slug` | path | string | yes | The plugin folder name. | | `action` | path | string (one of: activate, deactivate) | yes | What to do. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/plugins/my-plugin/activate" \ -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/sites/123/plugins/my-plugin/activate", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/plugins/my-plugin/activate", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "slug": "my-plugin", "action": "activate", "output": "Plugin 'my-plugin' activated." } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Remove a plugin `DELETE /sites/{site_id}/plugins/{slug}` · scope `sites:write` · **destructive** Switches a plugin off and deletes its files. There is no undo; the plugin's own data in the database is removed the way the plugin asks for. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `slug` | path | string | yes | The plugin folder name. | ### Example ```bash curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/plugins/my-plugin" \ -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/plugins/my-plugin", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/plugins/my-plugin", { method: "DELETE", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "slug": "my-plugin", "deleted": true } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Themes on this site `GET /sites/{site_id}/themes` · scope `sites:read` Every theme on a WordPress site and which one is in use. ### 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/themes" \ -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/themes", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/themes", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "themes": [ { "slug": "twentytwentyfive", "title": "Twenty Twenty-Five", "status": "active", "version": "1.0.0", "update": "none", "auto_update": "off" } ], "count": 1 } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Use this theme `POST /sites/{site_id}/themes/{slug}/activate` · scope `sites:write` · **destructive** Switches the site to another theme. This changes how every page looks at once - check the site with sites.verify straight after, and switch back if it is wrong. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `slug` | path | string | yes | The theme folder name. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/themes/twentytwentyfive/activate" \ -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/sites/123/themes/twentytwentyfive/activate", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/themes/twentytwentyfive/activate", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "slug": "twentytwentyfive", "active": true, "output": "Success: Switched to Twenty Twenty-Five theme." } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized` ## Check PHP code for errors `POST /sites/{site_id}/code-check` · scope `sites:read` Runs PHP's own syntax check over a file or every .php file in a folder, in the exact PHP version the site runs. Do this before switching a plugin or theme on - a syntax error there takes the whole site down. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `site_id` | path | integer | yes | The site id (see GET /sites). | | `path` | body | string | yes | A file or folder in the site, e.g. "wp-content/plugins/my-plugin". | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/code-check" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "wp-content/plugins/my-plugin"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/sites/123/code-check", headers=headers, json={"path": "wp-content/plugins/my-plugin"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/code-check", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"path": "wp-content/plugins/my-plugin"}) }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 123, "path": "wp-content/plugins/my-plugin", "php_version": "8.3.14", "ok": true, "problems": [] } } ``` Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed` ## Sites on third-party hosts `GET /external-sites` · scope `sites:read` The sites this account publishes to outside PBN.LTD (GitHub Pages, Cloudflare Pages, Netlify, Vercel and the rest), with their address and the state of the last deploy. Connecting a provider and adding a site stay in the panel, because they need your own provider credentials. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `limit` | query | integer | no | How many to return. Default: `20`. | | `cursor` | query | string | no | The next_cursor value of the previous page. | | `provider` | query | string | no | Only this provider. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/external-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/external-sites", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/external-sites", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": [ { "id": 9, "name": "Landing page", "provider": "netlify", "provider_name": "Netlify", "project": "my-landing", "state": "live", "state_message": null, "url": "https://my-landing.netlify.app", "domain": "landing.example.com", "source": "upload", "source_site_id": 123, "last_deploy_at": "2026-09-19T14:22:00Z", "created_at": "2026-08-02T09:00:00Z" } ], "next_cursor": null, "has_more": false } ``` Errors: `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Deploys of an external site `GET /external-sites/{ext_site_id}/deployments` · scope `sites:read` The recent deploys of one external site, newest first - what to pass to a rollback. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `ext_site_id` | path | integer | yes | | | `limit` | query | integer | no | Default: `25`. | ### Example ```bash curl -s "https://app.pbn.ltd/api/v1/external-sites/9/deployments" \ -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/external-sites/9/deployments", headers=headers, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/external-sites/9/deployments", { method: "GET", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`} }); console.log(res.status, await res.json()); ``` ### Response `200` ```json { "data": { "site_id": 9, "deployments": [ { "id": 551, "kind": "deploy", "state": "done", "created_at": "2026-09-19T14:20:00Z", "finished_at": "2026-09-19T14:22:00Z", "files": 12, "bytes": 284113, "message": null, "reference": "a1b2c3d4" } ] } } ``` Errors: `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized` ## Deploy again or roll back `POST /external-sites/{ext_site_id}/deploy` · scope `sites:write` · **destructive** Publishes the site to its provider again, or rolls it back to a deploy that is already there. This replaces what is live at that provider. No files travel through this call - to publish new files, put them on the PBN.LTD site first (files.write or files.upload_archive) and then redeploy. ### Parameters | Name | In | Type | Required | Description | |---|---|---|---|---| | `ext_site_id` | path | integer | yes | | | `kind` | body | string (one of: redeploy, rollback) | no | Build and publish again, or go back to an earlier deploy. Default: `redeploy`. | | `rollback_to` | body | string | no | The deploy to go back to (see extsites.history). Needed for a rollback. | ### Example ```bash curl -s -X POST "https://app.pbn.ltd/api/v1/external-sites/9/deploy" \ -H "Authorization: Bearer $PBN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"kind": "redeploy"}' ``` ```python import os import requests headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]} r = requests.post("https://app.pbn.ltd/api/v1/external-sites/9/deploy", headers=headers, json={"kind": "redeploy"}, timeout=120) print(r.status_code, r.json()) ``` ```javascript const res = await fetch("https://app.pbn.ltd/api/v1/external-sites/9/deploy", { method: "POST", headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify({"kind": "redeploy"}) }); console.log(res.status, await res.json()); ``` ### Response `202` ```json { "data": { "site_id": 9, "deployment_id": 552, "kind": "redeploy", "state": "queued", "url": "https://my-landing.netlify.app" } } ``` Errors: `conflict`, `not_found`, `payment_required`, `rate_limited`, `scope_missing`, `unauthorized`, `validation_failed`