Companion thread API

Open a post's companion discussion thread with one authenticated call, from whatever tooling you already publish with. This is the automation behind "a comment section for your blog, without running a comment section": your blog stays your blog, and each post gets a discussion thread on PopsicleBoat that you can link to and pull replies from.

You don't need this API to use PopsicleBoat as your comments — you can create threads by hand in the web UI. This just removes the copy-paste for people who publish often.

1. Get set up (once)

  1. Have an account and a space. Claim your space at popsicleboat.com/for-bloggers (or ask me, through the letters page, to set it up). A thread is a post in a space.

  2. Mint a personal access token. Sign in, open your Settings page, and follow the API tokens link. Mint the token yourself — it is shown once, to you alone; only a hash is stored, so nobody (including me) can see it again. Store it where your publishing tool runs, e.g.

    export POPSICLEBOAT_TOKEN=pbt_xxxxxxxx
    

    Revoke a token from the same page any time; tokens don't expire on their own.

2. Create a thread

POST https://www.popsicleboat.com/api/companion-threads
Authorization: Bearer <POPSICLEBOAT_TOKEN>
Content-Type: application/json

{
  "space": "blogging",
  "title": "Discuss: The page that rebuilds itself",
  "body": "Companion thread for [my post](https://jva.lol/weblog/the-page-that-rebuilds-itself/).",
  "source": "https://jva.lol/weblog/the-page-that-rebuilds-itself/"
}
Field Required Notes
space yes The space's name, matched case-insensitively (the /c/<space> slug).
title yes The thread title.
body yes Markdown. Include a real link back to your post — that's the loop.
source no The blog post this thread discusses. Becomes the thread's source link: it drives the "Discussing a post on …" meta line, and in build-hook spaces the composer tells readers their replies show up on the post. Must be a public http(s) URL — a bad one fails the request.

The thread is created as your user, public, in the named space.

Success — 201 Created

{
  "url": "https://www.popsicleboat.com/c/blogging/posts/260",
  "id": 260,
  "space": "blogging"
}

Put url wherever your post records its discussion thread (for the Hugo reference client, that's the post's popsicleboat: front-matter field).

Errors

Status error When
401 unauthorized Missing or unknown Bearer token.
404 space_not_found No approved space by that name — claim it first.
422 missing_field space, title, or body was blank/absent.
422 invalid The post was rejected (e.g. posting velocity, or a source that isn't a public http(s) URL).

Every error body is { "error": "<code>", "message": "<human sentence>" }.

Example

curl -sS https://www.popsicleboat.com/api/companion-threads \
  -H "Authorization: Bearer $POPSICLEBOAT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"space":"blogging","title":"Discuss: my post","body":"Companion to [my post](https://example.com/my-post/)."}'

Security model

  • Per-user, hashed. A token identifies one user. Only its SHA-256 hash is stored; the plaintext exists only in your hands.
  • Scope. A token can do exactly what this endpoint does: create a public thread in an approved space, as you — the same thing you could do by hand in the web UI. It is not a session and grants no other access.
  • Revoke. Revoke any token yourself from the same Settings page that minted it. Mint a fresh one anytime; they don't expire on their own.
  • Treat it like a password. Keep it out of your repo and your build logs; pass it through an environment variable.

Reading replies back

Each thread's public replies live at the thread's address plus /replies.json (or by post URL: /discuss/replies.json?url=...). A reply deleted while a conversation still stands underneath syndicates as a stub: "deleted": true with the public removal line as its body, nothing else. Render it like any reply, or filter on the flag if your site prefers absence.

Reference client

The Hugo blog at jva.lol drives this with scripts/companion-thread — it takes a post, calls this endpoint, and writes the returned url into the post's popsicleboat: field, so publishing a post with its thread is one command.