Content and publishing
Posts, pages, pictures and the AI auto-posting campaigns, on every site type.
AI auto posting: overview
GET/api/v1/autopostScope 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/keysScope 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}/testScope 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
key_id | path | integer | yes | The 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
key_id | path | integer | yes | The AI key (see GET /autopost/keys). |
confirm | body | boolean | yes | Must 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/modelsScope 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-fieldsScope 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/campaignsScope 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign | body | object | yes | The 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | path | integer | yes | The campaign. |
campaign | body | object | yes | The 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | path | integer | yes | The campaign. |
confirm | body | boolean | yes | Must 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
article_id | path | integer | yes | The article (a post id from the content endpoints). |
action | path | string (one of: approve, publish_now, cancel, retry, unpublish, delete) | yes | approve (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). |
confirm | body | boolean | no | Needed 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/optionsScope 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
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}/contentScope 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
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/queueScope 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
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
| 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
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}/contentScope 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
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
| 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
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
| 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
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/mediaScope 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
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/campaignsScope 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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | path | integer | yes | The 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}/statusScope 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
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-nowScope 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
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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
run_id | path | integer | yes | The 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