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
| Scope | Allows |
|---|---|
service.blogs.read | List + fetch published posts. |
service.blogs.view | List posts of any status, read by id, list subscribers, categories and analytics. |
service.blogs.manage | Create / edit / delete posts; add, update and delete subscribers. |
service.blogs.publish | Publish, 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.
| Endpoint | Returns |
|---|---|
GET /api/services/blogs/widget/posts | Published 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
| Event | When |
|---|---|
blogs.post_published | A post is published (manually or by the scheduler). |
blogs.subscriber_added | A new email subscribes via the widget or API. |
See Webhooks for payload + signature format.