Read the public blog
List and filter published blog posts, open one by its address, and fetch authors, disciplines and images, all without signing in or holding a key.
Updated
JourneyAPI: content and public APIsstep 2 of 3
Next: Embed the live-chat widgetThe blog is written by the people who do the work, filed under a discipline such as Security or Networking. This article describes the read API behind the public blog: you can list and filter posts, open one by its address, look up authors and disciplines, and fetch the images a post uses. You need no account and no key.
Every route here is under /api/v1/blog/public. Each answers only with published posts and with what their authors have chosen to show. A draft, a post in review or a withdrawn post is not found. Responses are marked as cacheable for a minute, so a change an editor makes can take that long to appear.
List and filter posts
/api/v1/blog/public/posts- Auth
- none
Returns one page of post cards, newest first, in an object with the cards, the total and the paging. Leave every filter out to list the whole blog. Filters combine: a post must match all of the ones you send.
Parameters (query string)
| Name | Type | Required | Description |
|---|---|---|---|
kind | string | No | article or video. Anything else is 400. |
discipline | string | No | A discipline key such as security. An unknown key is 404 with no such discipline. |
author | string | No | An author's handle. A handle with no public page gives an empty list. |
tag | string | No | One tag, for example mfa. Tags are lower-case and hyphenated. |
q | string | No | Words in the title, summary, tags or body. At most 100 characters; longer is 400. |
kb | string | No | The id or slug of a public help article: only posts that point to it. |
featured | string | No | 1 or true keeps featured posts. |
exclude | string | No | A post slug to leave out, handy for "more like this". |
limit | integer | No | Cards per page. The default is 12 and the most is 48. |
page | integer | No | The page, starting at 1. |
curl -s "https://solnyxus.com/api/v1/blog/public/posts?discipline=security&limit=5"
const params = new URLSearchParams({ discipline: "security", limit: "5" });
const res = await fetch(`https://solnyxus.com/api/v1/blog/public/posts?${params}`);
const list = await res.json();
console.log(list.total, "posts on", list.pages, "pages");
for (const p of list.posts) console.log(p.publishedAt, p.title);
import requests
res = requests.get(
"https://solnyxus.com/api/v1/blog/public/posts",
params={"discipline": "security", "limit": 5},
timeout=30,
)
data = res.json()
print(data["total"], "posts on", data["pages"], "pages")
for p in data["posts"]:
print(p["publishedAt"], p["title"])
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
q := url.Values{"discipline": {"security"}, "limit": {"5"}}
res, err := http.Get("https://solnyxus.com/api/v1/blog/public/posts?" + q.Encode())
if err != nil {
panic(err)
}
defer res.Body.Close()
var list struct {
Total, Pages int
Posts []struct{ PublishedAt, Title string }
}
json.NewDecoder(res.Body).Decode(&list)
fmt.Println(list.Total, "posts on", list.Pages, "pages")
for _, p := range list.Posts {
fmt.Println(p.PublishedAt, p.Title)
}
}
A card holds what a list needs. cover appears when the post has a cover image, and video when it is a video post. The example is trimmed to one card.
{
"posts": [
{
"slug": "recovery-codes-matter",
"title": "Your recovery codes matter as much as your password",
"summary": "Two-factor sign-in stops most account takeovers. The backup codes are how the rest get in.",
"kind": "article",
"discipline": { "key": "security", "label": "Security" },
"tags": ["mfa", "passwords"],
"cover": {
"id": "3f9c0026-0000-4000-8000-000000000026",
"alt": "A printed sheet of recovery codes",
"width": 1600,
"height": 900
},
"author": { "name": "Alex Example", "handle": "alex-example", "jobTitle": "Security analyst" },
"publishedAt": "2026-10-05T21:26:54Z",
"updatedAt": "2026-10-05T21:26:54Z",
"readingMinutes": 3,
"featured": true
}
],
"total": 1,
"page": 1,
"pages": 1
}
A video post's card carries video with provider (youtube or vimeo), id, an embedUrl for a player that avoids tracking, and a watchUrl.
| Status | When |
|---|---|
400 | kind is article or video, or that search is too long. |
404 | no such discipline. |
Read one post
/api/v1/blog/public/posts/{slug}- Auth
- none
Returns the card fields plus body, which is markdown. Images in the body are written as media:<id>; fetch one from the media route below. The answer also holds authorAbout when the author has a public page, related, the help articles the post points to (each with slug, title, category and a short excerpt), and more, up to three other posts in the same discipline.
curl -s "https://solnyxus.com/api/v1/blog/public/posts/recovery-codes-matter"
const res = await fetch("https://solnyxus.com/api/v1/blog/public/posts/recovery-codes-matter");
if (res.status === 404) throw new Error("post not found");
const post = await res.json();
console.log(post.title, "by", post.author.name, "-", post.readingMinutes, "min");
import requests
res = requests.get(
"https://solnyxus.com/api/v1/blog/public/posts/recovery-codes-matter",
timeout=30,
)
res.raise_for_status()
post = res.json()
print(post["title"], "by", post["author"]["name"], "-", post["readingMinutes"], "min")
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, err := http.Get("https://solnyxus.com/api/v1/blog/public/posts/recovery-codes-matter")
if err != nil {
panic(err)
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
panic("post not found")
}
var post struct {
Title string
ReadingMinutes int
Author struct{ Name string }
}
json.NewDecoder(res.Body).Decode(&post)
fmt.Println(post.Title, "by", post.Author.Name, "-", post.ReadingMinutes, "min")
}
The response is trimmed to the useful fields.
{
"slug": "recovery-codes-matter",
"title": "Your recovery codes matter as much as your password",
"kind": "article",
"discipline": { "key": "security", "label": "Security" },
"tags": ["mfa", "passwords"],
"author": { "name": "Alex Example", "handle": "alex-example", "jobTitle": "Security analyst" },
"publishedAt": "2026-10-05T21:26:54Z",
"readingMinutes": 3,
"body": "Multi-factor sign-in is the best single switch a small business can turn on.",
"authorAbout": {
"bio": "I look after security for our clients.",
"links": [{ "kind": "website", "url": "https://example.com/alex" }],
"disciplines": [{ "key": "security", "label": "Security" }]
},
"related": [
{
"slug": "reset-your-own-password",
"title": "Reset your own password",
"category": "Accounts",
"excerpt": "You can reset your password yourself, any time."
}
],
"more": []
}
An address that does not exist, or whose post is not published, is a 404 with post not found.
Authors
/api/v1/blog/public/authors- Auth
- none
Lists the authors who have switched their public page on and have at least one published post, most recently published first. Everyone else appears on their posts by name only, with no handle, photo or biography. Each author has name, handle, jobTitle, bio, location, links (each with a kind such as website or github, and a url), the disciplines they write in, a photo when they have one, and posts, the number of published posts.
GET /api/v1/blog/public/authors/{handle} answers {author, posts}: the same author object and up to 100 of their posts as cards, newest first. A handle that does not belong to a public author is 404 with author not found.
curl -s "https://solnyxus.com/api/v1/blog/public/authors/alex-example"
const res = await fetch("https://solnyxus.com/api/v1/blog/public/authors/alex-example");
const { author, posts } = await res.json();
console.log(author.name, "has", posts.length, "posts");
import requests
res = requests.get(
"https://solnyxus.com/api/v1/blog/public/authors/alex-example",
timeout=30,
)
data = res.json()
print(data["author"]["name"], "has", len(data["posts"]), "posts")
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, err := http.Get("https://solnyxus.com/api/v1/blog/public/authors/alex-example")
if err != nil {
panic(err)
}
defer res.Body.Close()
var out struct {
Author struct{ Name string }
Posts []struct{ Title string }
}
json.NewDecoder(res.Body).Decode(&out)
fmt.Println(out.Author.Name, "has", len(out.Posts), "posts")
}
Disciplines
/api/v1/blog/public/disciplines- Auth
- none
Returns the catalogue as an array of {key, label, blurb, posts}, where posts is the number of published posts filed there. The keys are security, networking, cloud, linux, windows, databases, service-desk and company. Use a key as the discipline filter above.
curl -s "https://solnyxus.com/api/v1/blog/public/disciplines"
const res = await fetch("https://solnyxus.com/api/v1/blog/public/disciplines");
for (const d of await res.json()) console.log(d.key, d.posts);
import requests
res = requests.get("https://solnyxus.com/api/v1/blog/public/disciplines", timeout=30)
for d in res.json():
print(d["key"], d["posts"])
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
res, err := http.Get("https://solnyxus.com/api/v1/blog/public/disciplines")
if err != nil {
panic(err)
}
defer res.Body.Close()
var ds []struct {
Key string
Posts int
}
json.NewDecoder(res.Body).Decode(&ds)
for _, d := range ds {
fmt.Println(d.Key, d.Posts)
}
}
[
{
"key": "security",
"label": "Security",
"blurb": "Threats, hardening, and the everyday habits that keep a business out of the headlines.",
"posts": 4
}
]
Images
/api/v1/blog/public/media/{id}- Auth
- none
Serves an image by its id: a post's cover, an image in its body, or the photo of an author who has a public page. An image is released only while a published post uses it, or while it is a public author's photo. An image from a draft, or one that was never used, is a 404 with image not found, the same answer as an id that does not exist. The body is the image itself (PNG, JPEG, GIF or WebP) and can be cached for ten minutes.
curl -s "https://solnyxus.com/api/v1/blog/public/media/$IMAGE_ID" -o cover.img
import { writeFile } from "node:fs/promises";
const id = "3f9c0026-0000-4000-8000-000000000026";
const res = await fetch(`https://solnyxus.com/api/v1/blog/public/media/${id}`);
await writeFile("cover.img", Buffer.from(await res.arrayBuffer()));
import os
import requests
res = requests.get(
f"https://solnyxus.com/api/v1/blog/public/media/{os.environ['IMAGE_ID']}",
timeout=30,
)
open("cover.img", "wb").write(res.content)
package main
import (
"io"
"net/http"
"os"
)
func main() {
res, err := http.Get("https://solnyxus.com/api/v1/blog/public/media/" + os.Getenv("IMAGE_ID"))
if err != nil {
panic(err)
}
defer res.Body.Close()
f, _ := os.Create("cover.img")
defer f.Close()
io.Copy(f, res.Body)
}
Where next
- Search the knowledge base and the blog together with Read the public knowledge base.
- Open the live-chat window on a page with Embed the live-chat widget.
- If a call returns
400or404, read theerrormessage: it names the parameter or the missing post.