PBN.LTD API docs
View as Markdown

Content and publishing

Posts, pages, pictures and the AI auto-posting campaigns, on every site type.

AI auto posting: overview

GET/api/v1/autopost

Scope content:read

Whether auto posting is available on the account (and why not), how many campaigns and AI keys it has and how many articles are in each state.

Example

curl -s "https://app.pbn.ltd/api/v1/autopost" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/autopost", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "allowed": true,
    "reason": "",
    "campaigns": 2,
    "articles": {
      "done": 40,
      "queued": 3
    },
    "keys": 1,
    "page_url": "https://app.pbn.ltd/autopost/"
  }
}

Errors: rate_limited, scope_missing, unauthorized

Your AI keys

GET/api/v1/autopost/keys

Scope content:read

The keys to your own AI accounts that campaigns write with: provider, whether the last test worked and how many campaigns use each. The key itself is never returned, and a key is only ever added on the AI keys page (https://app.pbn.ltd/autopost/keys/).

Example

curl -s "https://app.pbn.ltd/api/v1/autopost/keys" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/autopost/keys", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost/keys", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": [
    {
      "id": 12,
      "label": "My OpenAI key",
      "provider": "openai",
      "provider_name": "OpenAI",
      "status": "ok",
      "status_message": "",
      "last_checked_at": "2026-09-26T09:00:00Z",
      "campaigns_using": 2
    }
  ]
}

Errors: rate_limited, scope_missing, unauthorized

Test an AI key

POST/api/v1/autopost/keys/{key_id}/test

Scope content:write

Asks the provider whether the key works (the page's Test button). A key that works again resumes the campaigns that were paused because of it.

Parameters

NameInTypeRequiredDescription
key_idpathintegeryesThe AI key (see GET /autopost/keys).

Example

curl -s -X POST "https://app.pbn.ltd/api/v1/autopost/keys/key_id/test" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/autopost/keys/key_id/test", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost/keys/key_id/test", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "id": 12,
    "label": "My OpenAI key",
    "provider": "openai",
    "provider_name": "OpenAI",
    "status": "ok",
    "status_message": "",
    "last_checked_at": "2026-09-26T09:00:00Z",
    "campaigns_using": 2,
    "ok": true,
    "message": "My OpenAI key works.",
    "campaigns_resumed": 1
  }
}

Errors: not_found, rate_limited, scope_missing, unauthorized

Remove an AI key

DELETE/api/v1/autopost/keys/{key_id}

Scope content:write · destructive

Removes the key from your account (nothing changes at the provider). Campaigns using it are paused until you choose another key.

Parameters

NameInTypeRequiredDescription
key_idpathintegeryesThe AI key (see GET /autopost/keys).
confirmbodybooleanyesMust be true.

Example

curl -s -X DELETE "https://app.pbn.ltd/api/v1/autopost/keys/key_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/autopost/keys/key_id", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost/keys/key_id", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "deleted": true,
    "campaigns_paused": 2
  }
}

Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed

AI models a campaign can use

GET/api/v1/autopost/models

Scope content:read

The text and picture models on offer. A campaign's model must belong to the provider of the key it writes with.

Example

curl -s "https://app.pbn.ltd/api/v1/autopost/models" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/autopost/models", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost/models", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": [
    {
      "id": 3,
      "provider": "openai",
      "kind": "text",
      "label": "GPT-4o mini",
      "hint": "cheap",
      "recommended": true
    }
  ]
}

Errors: rate_limited, scope_missing, unauthorized

The fields of a campaign

GET/api/v1/autopost/campaign-fields

Scope content:read

Every field POST /campaigns and PATCH /campaigns/{campaign_id} take, from the page's own form: label, whether it is required, the allowed values and the help text. site_ids are your site ids, text_connection / image_connection are AI key ids and text_model / image_model are model ids (GET /autopost/models).

Example

curl -s "https://app.pbn.ltd/api/v1/autopost/campaign-fields" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/autopost/campaign-fields", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost/campaign-fields", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "fields": {
      "schedule": {
        "label": "Schedule",
        "required": true,
        "choices": [
          "once",
          "daily",
          "weekly",
          "manual"
        ]
      }
    },
    "example": {
      "name": "My weekly posts",
      "site_ids": [
        123,
        124
      ],
      "text_connection": 12,
      "text_model": 3,
      "image_connection": null,
      "image_model": null,
      "schedule": "weekly",
      "times": "09:00",
      "weekdays": "0,2,4",
      "once_at": null,
      "timezone": "Europe/London",
      "spread_minutes": 60,
      "per_site": 1,
      "total_limit": null,
      "end_date": null,
      "review": false,
      "topics": "How to pick a widget\nWidget care",
      "niche": "Widgets for home use",
      "length": "medium",
      "language": "English"
    }
  }
}

Errors: rate_limited, scope_missing, unauthorized

Create a campaign

POST/api/v1/campaigns

Scope content:write

Creates an auto-posting campaign exactly as the New campaign page does (same fields, same checks). It writes with one of your AI keys, so add a key on the AI keys page first.

Parameters

NameInTypeRequiredDescription
campaignbodyobjectyesThe campaign fields (see GET /autopost/campaign-fields), e.g. {"name": "...", "site_ids": [123], "text_connection": 12, "text_model": 3, "schedule": "daily", "times": "09:00"}.

Example

curl -s -X POST "https://app.pbn.ltd/api/v1/campaigns" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/campaigns", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/campaigns", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

201

{
  "data": {
    "id": 44,
    "name": "My weekly posts",
    "status": "active",
    "next_run_at": "2026-09-28T09:00:00Z"
  }
}

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

Change a campaign

PATCH/api/v1/campaigns/{campaign_id}

Scope content:write

Changes only the fields you send (the rest stay as they are), with the Edit campaign page's checks. Sending site_ids replaces the campaign's sites.

Parameters

NameInTypeRequiredDescription
campaign_idpathintegeryesThe campaign.
campaignbodyobjectyesThe campaign fields (see GET /autopost/campaign-fields), e.g. {"name": "...", "site_ids": [123], "text_connection": 12, "text_model": 3, "schedule": "daily", "times": "09:00"}.

Example

curl -s -X PATCH "https://app.pbn.ltd/api/v1/campaigns/44" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.patch("https://app.pbn.ltd/api/v1/campaigns/44", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/campaigns/44", {
  method: "PATCH",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "id": 44,
    "name": "My weekly posts",
    "status": "active",
    "next_run_at": "2026-09-28T09:00:00Z"
  }
}

Errors: not_found, payment_required, rate_limited, scope_missing, unauthorized, validation_failed

Delete a campaign

DELETE/api/v1/campaigns/{campaign_id}

Scope content:write · destructive

Deletes the campaign. Articles not written yet are cancelled; articles already published stay on your sites.

Parameters

NameInTypeRequiredDescription
campaign_idpathintegeryesThe campaign.
confirmbodybooleanyesMust be true.

Example

curl -s -X DELETE "https://app.pbn.ltd/api/v1/campaigns/44" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/campaigns/44", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/campaigns/44", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "deleted": true
  }
}

Errors: not_found, rate_limited, scope_missing, unauthorized, validation_failed

An article's buttons

POST/api/v1/autopost/articles/{article_id}/{action}

Scope content:write

The same buttons as an article's page.

Parameters

NameInTypeRequiredDescription
article_idpathintegeryesThe article (a post id from the content endpoints).
actionpathstring (one of: approve, publish_now, cancel, retry, unpublish, delete)yesapprove (a campaign that asks for review), publish_now, cancel, retry (a failed one), unpublish (take it off the site), delete (forget a finished one here - it stays on the site).
confirmbodybooleannoNeeded for unpublish: must be true.

Example

curl -s -X POST "https://app.pbn.ltd/api/v1/autopost/articles/article_id/scan" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/autopost/articles/article_id/scan", headers=headers, timeout=120)
print(r.status_code, r.json())
const res = await fetch("https://app.pbn.ltd/api/v1/autopost/articles/article_id/scan", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());

Response

200

{
  "data": {
    "id": 991,
    "ok": true,
    "message": "It is back in the queue.",
    "state": "queued"
  }
}

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

What a post on this site can have

GET/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).

Example

curl -s "https://app.pbn.ltd/api/v1/sites/123/content/options" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/content/options", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
limitqueryintegernoHow many to return. Default: 20.
statequerystring (one of: queued, writing, images, review, ready, publishing, done, failed, cancelled)noOnly posts in this state.
on_sitequerybooleannoAlso ask the site itself for its latest posts (slower). Default: True.

Example

curl -s "https://app.pbn.ltd/api/v1/sites/123/content" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/content", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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/api/v1/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

NameInTypeRequiredDescription
limitqueryintegernoHow many to return. Default: 20.
cursorquerystringnoThe next_cursor value of the previous page.
statequerystring (one of: queued, writing, images, review, ready, publishing, done, failed, cancelled)noOnly posts in this state.
site_idqueryintegernoOnly this site.

Example

curl -s "https://app.pbn.ltd/api/v1/content/queue" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/content/queue", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
post_idpathintegeryesThe post id this API gave you when it was created.

Example

curl -s "https://app.pbn.ltd/api/v1/sites/123/content/90210" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/content/90210", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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": "<p>The text of the post.</p>",
    "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/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
titlebodystringyesThe title of the post.
body_htmlbodystringnoThe text as HTML. Or send body_markdown.
body_markdownbodystringnoThe text as Markdown (headings, lists, links, bold, code, quotes).
statusbodystring (one of: publish, draft)no"draft" only on site types that have drafts (see content.options). Default: publish.
categorybodystringnoCategory id or name, on site types that have them.
authorbodystringnoAuthor id, on site types that have authors.
tagsbodyarraynoTags for the post.
slugbodystringnoThe address of the post; one is made from the title when you leave it out.
publish_atbodystringnoISO date and time to publish it (default: now).
featured_image_base64bodystringnoThe main picture, base64. It leads the post and becomes the featured image on site types that have one.
images_base64bodyarraynoMore pictures, base64; they are placed in the text.

Example

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"]}'
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())
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

{
  "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/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
post_idpathintegeryesThe post id this API gave you when it was created.
titlebodystringnoA new title.
body_htmlbodystringnoNew text as HTML.
body_markdownbodystringnoNew text as Markdown.
tagsbodyarraynoReplace the tags.
statusbodystring (one of: publish, draft)no
publish_atbodystringnoMove when it goes out.
approvebodybooleannoApprove a post that is waiting for approval.

Example

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)"}'
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())
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

{
  "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": "<p>The new text.</p>"
  }
}

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

Take a post off the site

DELETE/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
post_idpathintegeryesThe post id this API gave you when it was created.

Example

curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/content/90210" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/sites/123/content/90210", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "data": {
    "id": 90210,
    "removed": true,
    "was": "published"
  }
}

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

Upload a picture or file

POST/api/v1/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

NameInTypeRequiredDescription
site_idpathintegeryesThe site id (see GET /sites).
namebodystringyesThe file name, e.g. "hero.jpg".
content_base64bodystringyesThe file itself, base64.
altbodystringnoAlt text (WordPress media library).
folderbodystringnoWhere to put it on site types with no media library. Default: assets.
overwritebodybooleannoReplace a file of the same name. Default: False.

Example

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"}'
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())
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

{
  "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/api/v1/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

NameInTypeRequiredDescription
limitqueryintegernoHow many to return. Default: 20.
cursorquerystringnoThe next_cursor value of the previous page.
statusquerystring (one of: active, paused, problem, finished)no

Example

curl -s "https://app.pbn.ltd/api/v1/campaigns" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/campaigns", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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/api/v1/campaigns/{campaign_id}

Scope content:read

One campaign with its last ten runs.

Parameters

NameInTypeRequiredDescription
campaign_idpathintegeryesThe campaign.

Example

curl -s "https://app.pbn.ltd/api/v1/campaigns/44" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/campaigns/44", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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/api/v1/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

NameInTypeRequiredDescription
campaign_idpathintegeryes
statusbodystring (one of: active, paused)yes"paused" stops it writing; "active" starts it again.

Example

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"}'
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())
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

{
  "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: conflict, not_found, payment_required, rate_limited, scope_missing, unauthorized, validation_failed

Write one now

POST/api/v1/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

NameInTypeRequiredDescription
campaign_idpathintegeryes
site_idsbodyarraynoOnly these sites of the campaign (default: all of them).
countbodyintegernoArticles per site. Default: 1.

Example

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}'
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())
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

{
  "data": {
    "run_id": 7782,
    "articles": 2,
    "problems": []
  }
}

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

How a run is going

GET/api/v1/runs/{run_id}

Scope content:read

One run of a campaign and every article in it.

Parameters

NameInTypeRequiredDescription
run_idpathintegeryesThe run.

Example

curl -s "https://app.pbn.ltd/api/v1/runs/7782" \
  -H "Authorization: Bearer $PBN_API_KEY"
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/runs/7782", headers=headers, timeout=120)
print(r.status_code, r.json())
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

{
  "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