Polyblog for developers

Everything you need to drive Polyblog from your own code: a REST API with scoped keys, endpoints for articles, blogs and topics, and signed webhooks for every change.

API keys

Get an API key

The Polyblog REST API is what you use to publish from a CMS, a static site generator, a CI pipeline or any backend of your own. Every request is authenticated with an API key you create yourself.

  1. Open your Polyblog dashboard and go to settings.
  2. Create an API key, choose the scopes it needs, and optionally pin it to a single blog.
  3. Copy the secret. It is shown once, at creation, and never returned again.

Send the secret as HTTP Basic credentials. All endpoints are relative to https://api.polyblog.io.

Authorization: Basic base64(<your key secret>)

A key pinned to a blog can only ever touch that blog, and a key can only ever touch the organization it was created in — the tenant is read from the key, never from the request.

Quick start

List and publish articles

List the articles of a blog. Anonymous callers only ever see published articles; with a key of the owning organization you also get drafts.

curl "https://api.polyblog.io/api/articles?blogId=YOUR_BLOG_ID&locale=en" \
  -H "Authorization: Basic $(printf %s YOUR_API_KEY_SECRET | base64)"

Publish an article

Posting an article publishes it on your blog and fires the article.created webhook.

curl -X POST "https://api.polyblog.io/api/articles" \
  -H "Authorization: Basic $(printf %s YOUR_API_KEY_SECRET | base64)" \
  -H "Content-Type: application/json" \
  -d '{
    "blogId": "YOUR_BLOG_ID",
    "locale": "en",
    "title": "Hello from the API",
    "description": "A short summary used in listings and meta tags.",
    "content": "<p>Written straight from my own backend.</p>",
    "published": true
  }'

Browse the full API reference — every endpoint with its parameters, request body, responses and required scope.

Scopes

Least privilege by default

Each key carries a set of per-resource scopes, so an integration that only needs to read articles cannot delete them. New keys start read-only; widen them explicitly.

  • articles:readRead articles, including drafts of your own blogs.
  • articles:writeCreate, generate, update and delete articles.
  • blogs:readRead the blogs of your account.
  • blogs:writeCreate a blog and update its settings.
  • topics:readRead the topics of a blog.
  • topics:writeCreate and update topics.
  • contacts:readRead the contacts collected by your blog.
  • contacts:writeCreate and update contacts.
  • images:writeUpload images used as article covers or in content.

A request made with a key that lacks the required scope is refused with 403. Keys are also rate limited, and a request over the limit is refused with 429.

Webhooks

Get told when something changes

Subscribe an endpoint of yours and Polyblog POSTs each matching event to it as it happens. A subscription can cover the whole organization or a single blog, and can filter the events it wants.

  • article.createdA new article was created, by you or by the API.
  • article.updatedAn existing article changed, including translations.
  • article.deletedAn article was deleted.
  • blog.createdA new blog was created.
  • contact.createdA visitor subscribed through one of your blogs.
POST https://your-server.com/polyblog-webhook
X-Polyblog-Event: article.created
X-Polyblog-Signature: t=1719000000,v1=<hmac-sha256 hex>
Content-Type: application/json

{
  "event": "article.created",
  "timestamp": 1719000000,
  "data": { "...": "..." }
}

Verify the signature

Every delivery carries an X-Polyblog-Signature header of the form t=timestamp,v1=signature. The signature is an HMAC-SHA256 of timestamp.body keyed by your subscription secret; recompute it over the raw body and compare before trusting the payload.

import crypto from 'node:crypto'

// body must be the RAW request body, byte for byte
function isSignatureValid({ header, body, secret }) {
  const [t, v1] = header.split(',')
  const timestamp = t.slice(2)
  const signature = v1.slice(3)

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  )
}

Delivery is one best-effort attempt with a 5 second timeout and no retries. Failures are counted, and a subscription that fails 20 times in a row is deactivated until you re-enable it.

Subscriptions are managed from your dashboard or through the webhook subscriptions endpoints in the API reference.

Start building