Skip to content

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 → one action, 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 issue param → 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.