PBN.LTD API docs
View as Markdown

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/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: 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