docs/api-brief.mdx
WorldMonitor composes a per-user intelligence brief on Railway, stores each
edition in Redis at brief:{userId}:{issueSlot}, writes a latest pointer at
brief:latest:{userId}, and exposes these routes for dashboard readback,
public sharing, and Telegram/Slack carousel rendering. Default cadence is
daily, but each alert rule's digestMode can schedule daily, twice-daily, or
weekly editions.
For source selection, filtering, deduplication, LLM grounding, and bias controls, see News Digest and Briefing Methodology.
<Info> All read routes require a valid Clerk session and a PRO tier, except the public share route (`/api/brief/public/{hash}`). </Info>GET /api/latest-briefReturns a short summary of the caller's most recent composed brief, or
{ status: "composing" } if the requested/current slot has not produced a
brief yet.
| Status | Response |
|---|---|
| 200 OK | { status: "ready", issueDate, issueSlot, dateLong, greeting, threadCount, magazineUrl } |
| 200 OK | { status: "composing", issueDate, issueSlot? } — no brief for the current/requested slot yet |
| 401 | Missing / invalid Clerk JWT |
| 403 | pro_required (entitlement confirmed non-Pro) / subscription_lapsed (lapse confirmed with the provider, X-Billing-Verification set) |
| 503 | BRIEF_URL_SIGNING_SECRET not configured — or entitlement verification in doubt: entitlement_verification_unavailable / renewal_verification_pending / renewal_verification_failed, with Retry-After and X-Billing-Verification. Distinguish on the code field. See Error handling |
issueDate remains the display/date field (YYYY-MM-DD). issueSlot is the
frozen edition key (YYYY-MM-DD-HHMM) used for Redis lookup and HMAC binding;
it is present on ready responses and on misses for an explicitly requested
slot. The magazineUrl is freshly signed against {userId, issueSlot} so it
only works for the authenticated owner.
GET /api/brief/{userId}/{issueSlot}Full magazine reader for issueSlot (YYYY-MM-DD-HHMM). HMAC-signed URL
required. The slot format lets two same-day digest sends produce distinct
frozen editions.
POST /api/brief/share-url?slot=YYYY-MM-DD-HHMMMaterialises a public share pointer for the caller's brief on slot. If the
slot is omitted, the route resolves brief:latest:{userId}. Idempotent — hash
is a pure function of {userId, issueSlot, BRIEF_SHARE_SECRET}.
| Status | Response |
|---|---|
| 200 | { shareUrl, hash, issueSlot } |
| 400 | invalid_slot_shape / invalid_payload |
| 401 | UNAUTHENTICATED |
| 403 | pro_required (entitlement confirmed non-Pro) / subscription_lapsed (lapse confirmed with the provider, X-Billing-Verification set) |
| 404 | brief_not_found — reader can't share what doesn't exist |
| 503 | service_unavailable — or entitlement verification in doubt: entitlement_verification_unavailable / renewal_verification_pending / renewal_verification_failed, with Retry-After and X-Billing-Verification. Distinguish on the code field. See Error handling |
GET /api/brief/public/{hash}Unauthenticated public read of a previously-shared brief. The hash resolves to a brief:public:{hash} → {userId, issueSlot} Redis pointer; if absent, the brief was never shared. Share pointers are written lazily (on share, not on compose).
GET /api/brief/carousel/{userId}/{issueSlot}/{page}.pngServer-rendered PNG page (page = 1..N) of the brief, intended for Telegram sendMediaGroup, Slack chat.postMessage, LinkedIn, etc.
@resvg/resvg-js with the bundled Linux native binding.Content-Type: image/png, 1080×1350 (4:5 portrait).GET /api/story?date=YYYY-MM-DDPublic read-only "story view" (web reader) for a shared brief. SEO-friendly HTML response.
GET /api/og-story?date=YYYY-MM-DDOpen Graph preview image for /api/story. Returns image/png, cached aggressively.
POST /api/chat-analystStreaming chat endpoint for the "Ask the analyst" in-dashboard assistant. Takes a user prompt + recent-signal context; returns SSE tokens.
text/event-streamintelligence/v1/chat-analyst-* handlers compose context + promptDenials carry the same three-way split as the brief endpoints above, so a client can tell a plan verdict from a session problem from an outage:
| Status | Response |
|---|---|
| 401 | UNAUTHENTICATED — no credential, or one that did not validate. Sign in; retrying is futile |
| 403 | Pro subscription required (entitlement confirmed non-Pro) / Subscription lapsed (lapse confirmed with the provider, X-Billing-Verification set) |
| 503 | Entitlement verification in doubt: entitlement_verification_unavailable / renewal_verification_pending / renewal_verification_failed, with Retry-After and X-Billing-Verification. See Error handling |
POST /api/widget-agentSingle-shot completion endpoint used by embedded widget iframes. Auth via X-WorldMonitor-Key (partner keys). Rate-limited per key.