Blogs API

A headless blog. Write, schedule and publish posts from your backend, manage subscribers, pull analytics, read published content, or embed the whole blog with one script tag. Server endpoints live under /api/v1/services/blogs and are tenant-scoped (sk_test_* routes to your paired sandbox). Responses are wrapped in { "data": … }; lists add a meta block.

Scopes

ScopeAllows
service.blogs.readList + fetch published posts.
service.blogs.viewList posts of any status, read by id, list subscribers, categories and analytics.
service.blogs.manageCreate / edit / delete posts; add, update and delete subscribers.
service.blogs.publishPublish, schedule, unpublish and archive posts.

List published posts

GET /api/v1/services/blogs/posts/published · scope service.blogs.read

Query params: limit, offset, category, tag. Returns published (and due-scheduled) posts, excluding any flagged noindex.

curl https://app.softsolz.uk/api/v1/services/blogs/posts/published?limit=20 \
  -H "Authorization: Bearer sk_live_…"

Response 200:

{ "data": [ { "slug": "hello-world", "title": "Hello world", "excerpt": "…",
    "category": "Product", "tags": ["news"], "author": "Team",
    "reading_time_minutes": 4, "published_at": "2026-05-29T10:00:00Z" } ],
  "meta": { "limit": 20, "offset": 0, "total": 1, "has_more": false } }

Get a published post

GET /api/v1/services/blogs/posts/published/{slug} · scope service.blogs.read

Returns the full post including sanitized body_html and the optional call-to-action. Each fetch counts one view against the blog's monthly view quota.

{ "data": { "slug": "hello-world", "title": "Hello world", "subtitle": null,
    "body_html": "<p>…</p>", "cover_image_url": "…",
    "cta_label": "Start free", "cta_url": "https://…",
    "tags": ["news"], "published_at": "2026-05-29T10:00:00Z" } }

Create / edit / publish a post

Create and edit need service.blogs.manage; changing status needs service.blogs.publish.

POST   /api/v1/services/blogs/posts            (create a draft)
PATCH  /api/v1/services/blogs/posts/{id}       (edit fields)
POST   /api/v1/services/blogs/posts/{id}/status (publish / schedule / archive)
DELETE /api/v1/services/blogs/posts/{id}
curl -X POST https://app.softsolz.uk/api/v1/services/blogs/posts \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  -d '{ "title": "Hello world", "body_html": "<p>First post</p>",
        "category": "Product", "tags": ["news"], "excerpt": "A short teaser." }'

# then publish (or schedule with scheduled_for)
curl -X POST https://app.softsolz.uk/api/v1/services/blogs/posts/{id}/status \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  -d '{ "status": "published" }'

PATCH supports optimistic concurrency via expected_updated_at (stale write returns 409). Publishing fires the blogs.post_published webhook. DELETE returns 204.

List all posts

GET /api/v1/services/blogs/posts?status=&search=&category=&limit=&offset= · scope service.blogs.view

Unlike the published read endpoint, this returns drafts, scheduled, published and archived posts. Read a single one (any status) by id: GET /api/v1/services/blogs/posts/{id}.

Subscribers

List needs service.blogs.view; add / update / delete need service.blogs.manage.

GET    /api/v1/services/blogs/subscribers?status=&search=
POST   /api/v1/services/blogs/subscribers            { "email": "reader@example.com", "name": "Reader" }
PATCH  /api/v1/services/blogs/subscribers/{id}       { "status": "unsubscribed" }
DELETE /api/v1/services/blogs/subscribers/{id}

Adding a subscriber is idempotent per email and fires blogs.subscriber_added on first insert.

Analytics & categories

Both need service.blogs.view.

GET /api/v1/services/blogs/analytics    (post counts, views over time, top posts, subscribers)
GET /api/v1/services/blogs/categories   (distinct categories in use)

Embed the blog

Drop a single script tag. It mounts a chrome-less iframe that lists your published posts and opens each one in place.

<div id="softsolz-blog"></div>
<script src="https://app.softsolz.uk/softsolz.js"
  data-public-key="pk_live_…"
  data-service-id="blogs"
  data-mount="#softsolz-blog"></script>

See Embeds for the loader contract, origin allowlist and auto-resize messages.

Widget endpoints

These power the embedded blog. Auth is the public key (X-Softsolz-Public-Key: pk_… or ?pk=); origin is checked against the key allowlist.

EndpointReturns
GET /api/services/blogs/widget/postsPublished posts (list).
GET /api/services/blogs/widget/posts/{slug}One full post (records a view).

Subscribe form

POST /api/services/blogs/widget/subscribe · auth: X-Softsolz-Public-Key: pk_…

curl -X POST https://app.softsolz.uk/api/services/blogs/widget/subscribe \
  -H "X-Softsolz-Public-Key: pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "email": "reader@example.com" }'

Idempotent per email; rate-limited per key. Returns 201 { "subscribed": true }.

RSS feed

GET /api/services/blogs/widget/rss?pk=pk_live_…&title=My%20Blog returns an RSS 2.0 feed of the latest published posts.

Webhook events

EventWhen
blogs.post_publishedA post is published (manually or by the scheduler).
blogs.subscriber_addedA new email subscribes via the widget or API.

See Webhooks for payload + signature format.