Weekly Briefs API¶
The signup lifecycle for BriefBlip Weekly: subscribe → confirm → (later) unsubscribe. Plus the public archive.
POST /api/briefblip/subscribe.php¶
Auth: anonymous — 10/hour per network, 3/day per email address.
{ "email": "you@example.com" }
{ "ok": true, "status": "pending" }
The response never reveals subscription state
This is deliberate. A brand-new signup, a re-submitted still-pending
signup, and an already-active subscriber all get the exact same 200
response — so this endpoint can't be used to check whether a given
address is subscribed. Don't build logic that branches on the response
body for this reason.
Errors: 400 invalid email · 429 too many signups from this network
(the per-email daily cap does not produce a 429 — it just silently skips
sending another confirmation email) · 503 signup temporarily unavailable.
Side effect: sends a double opt-in confirmation email, unless the address is already active. Resubmitting a still-pending signup reuses the same confirmation link rather than invalidating it — so an earlier confirmation email always keeps working.
GET /api/briefblip/confirm.php — public path /confirm¶
Request: ?token=<40-character hex token> (from the confirmation
email).
Response: HTML, always 200 unless something breaks server-side
(503). States: confirmed, already active, or invalid/expired token — the
page text differs per state, there's no JSON variant.
GET|POST /api/briefblip/unsubscribe.php — public path /unsubscribe¶
Request: ?token=<40-character hex token> (from any issue email).
Idempotent — clicking an already-used unsubscribe link isn't an error.
POST is the RFC 8058 one-click path: every issue carries List-Unsubscribe
and List-Unsubscribe-Post: List-Unsubscribe=One-Click, and Gmail/Outlook POST
rather than GET when the reader uses their built-in unsubscribe button. A POST
answers 200 text/plain, not the HTML page — the caller is a machine that never
renders it.
GET|POST /api/briefblip/manage.php — public path /manage¶
The subscriber dashboard. Two token tiers, and which one you hold decides what you may do:
| Token | Shape | Lifetime | Grants |
|---|---|---|---|
unsub_token |
40 hex | never expires | status, pause/resume, unsubscribe, resubscribe, Stripe portal |
| manage token | 64 hex | 60 minutes | the above plus changing the delivery address |
GET /manage→ request-a-link form.GET /manage?token=…→ the dashboard, at whichever tier the token shape implies.GET /manage?token=…&billing=1→ 302 to Stripe's billing portal (Pro only).POST /manage→ oneaction, then a redirect (Post/Redirect/Get). Actions:request_link,pause,resume,unsubscribe,resubscribe,billing,request_email_change. The token travels in the body, not the query.
Every state change is a POST on purpose — link scanners GET every URL in an
email. There is no CSRF token: the token in the body is itself unguessable, magic
links expire in an hour, and both entry points are rate limited
(10/10min per network and 3/10min per address for request_link).
request_link always redirects to ?sent=1, whether the address is known,
unknown, or rate limited — the response must not reveal who is subscribed.
Address-change confirmations redeem through /confirm, not a separate path, and
report one state whether the change moved a row or merged it into an existing
one, for the same reason.
GET /api/briefblip/weekly.php — public paths /weekly, /weekly/<issue>¶
Fully public, no rate limit, no auth.
- No
issueparam → index of published issues. ?issue=2026-W34→ that single issue.
Only published issues are ever served — a draft and a nonexistent issue
key both come back 404. There's no way to preview an unpublished issue
through this endpoint.