Bootstrap
GET /api/bootstrap
Single round-trip hydration for the dashboard. Returns all bootstrap-registered Redis cache keys unwrapped from their seed envelopes in one response.
- Auth: browser
wm-sessioncookie,X-WorldMonitor-Key, or theX-Api-Keyalias. User-issued keys are validated for current API access. - Anonymous weather:
?keys=weatherAlertsis public only when no API key header is sent. If you attachX-WorldMonitor-Key/X-Api-Key, the request is fully validated even for weather — a malformed key returns401, a key without current API access returns403. Callers that always send a key must send a valid, entitled key (or omit the header to use the anonymous weather path). This URL isno-store, which is what makes that contract hold at the edge as well as the origin: nothing caches it, so an invalid key can never be answered by a warm anonymous entry. - Public weather:
?keys=weatherAlerts&public=1is the CDN-cached weather read. Like?tier=fast&public=1, it returns the same shared seed payload to every caller and ignores any credentials you attach — a CDN hit precedes auth, so the marker is what lets the response be cached safely. Prefer it for high-volume anonymous reads; use the bare URL when you need your key validated. - Server-to-server: call
https://api.worldmonitor.app/api/bootstrapdirectly withX-WorldMonitor-Key: wm_.... There is no separate gateway host, token exchange, activation step, or IP allow-list requirement for this endpoint. - Cache: only the explicitly-marked
?...&public=1URLs are shared-cacheable, because only they answer every caller identically.?tier=fast&public=1/?tier=slow&public=1use browsermax-age=60/max-age=300and CDNs-maxage=600/s-maxage=7200. Single-key public URLs: on-demand keys (?keys=<onDemandName>&public=1) inherit the slow profile — browsermax-age=300, CDNs-maxage=7200— unless the key declares its own, which every key published more often than that shield does:correlationCards(browsermax-age=60, CDNs-maxage=300),chinaDecisionSignals(browsermax-age=60, CDNs-maxage=900),canadaRoads(browsermax-age=60, CDNs-maxage=900),albertaRoads(browsermax-age=60, CDNs-maxage=900),manitobaRoads(browsermax-age=60, CDNs-maxage=900),marketCorrelationSeries(browsermax-age=60, CDNs-maxage=900),imdCycloneMarine(browsermax-age=60, CDNs-maxage=900),bcOpen511(browsermax-age=60, CDNs-maxage=1800),flightDelays(browsermax-age=60, CDNs-maxage=1800), andforecasts(browsermax-age=300, CDNs-maxage=3600);?keys=weatherAlerts&public=1usesCache-Control: public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900with the fast-tier CDN shield. Everything else — key-authenticated, session-authenticated, the unmarked?tier=...URLs, and the anonymous?keys=weatherAlertspath — usesCache-Control: no-storeand emits no CDN cache headers. That split is deliberate: a CDN hit precedes auth, so a URL whose answer depends on credentials must never be cacheable. - Rate limit: user API key validation on this endpoint has a fail-closed fixed 60 s per-IP pre-validation limit of 600 attempts, separate from the default API sliding-window limiter.
- Shape:
{ "data": { "earthquakes": ..., "outages": ..., "marketQuotes": ... }, "missing": [] }— ~40+ unwrapped seeded-domain payloads nested underdata, plus amissinglist for cache keys not present in Redis.
chinaDecisionSignals, the bounded six-domain China
country-summary contract. Its anonymous country/RPC representation, Pro MCP
representation, and operator health registration share stable group IDs and
provenance; see China Decision Signals.
Version
GET /api/version
Returns the latest GitHub Release of koala73/worldmonitor. Used by the desktop app to detect a newer published release and prompt the user to update. It is not the currently-deployed Vercel commit.
public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600. Returns 502 { "error": "upstream" } or 502 { "error": "fetch_failed" } when the GitHub API is unreachable.
Cache purge
POST /api/cache-purge
Internal. Invalidates Redis cache keys by explicit list or glob patterns.
- Auth:
Authorization: Bearer $RELAY_SHARED_SECRET(timing-safe compared). Anything else returns401. - Body (at least one of
keys/patternsrequired): - Limits: up to 20 explicit keys, up to 3 patterns (each must end in
*, bare*rejected), up to 200 deletions total, up to 5 SCAN iterations per pattern. - Safety: keys with prefixes
rl:/__are always skipped; patterns that would matchmilitary:bases:*,conflict:iran-events:*,conflict:ucdp-events:*(durable seeds) are skipped. - Non-production: on preview / development deploys, keys are auto-prefixed with
{env}:{git-sha}:so purges can’t affect production data. - Response:
Health
GET /api/health
Aggregated freshness report for all registered seed keys. Returns HEALTHY, WARNING, DEGRADED, UNHEALTHY, or REDIS_DOWN in the JSON status field.
All states except REDIS_DOWN return HTTP 200; REDIS_DOWN returns HTTP 503 because Redis was unreachable and the endpoint could not assess seed state. Responses are not cached (private, no-store, max-age=0 plus CDN-Cache-Control: no-store).
Monitor via UptimeRobot / Better Stack with ?compact=1 — alert on any status other than HEALTHY. The full detailed view requires an operator/enterprise API key because it includes canonical cache key names and freshness thresholds.
GET /api/seed-health
Parallel registry for Railway-cron-driven seeders with their own cadence thresholds. Distinct from /api/health — both must be updated when cadence changes. See health endpoints.
chinaDecisionSignals is refreshed by the derived-signals bundle every 15
minutes. /api/health allows 60 minutes before STALE_SEED;
/api/seed-health uses a 30-minute interval (60-minute alarm) so both operator
surfaces agree.
POST /api/seed-contract-probe
Internal probe that validates each seed producer’s envelope shape matches its consumers. Returns violations if any consumer reads a field the producer no longer emits.
User preferences
GET /api/user-prefs
POST /api/user-prefs
Per-user dashboard preferences (layout, toggles, filters). Clerk bearer required. Backed by Convex.
- Idempotency: optional
Idempotency-Keysupported onPOST /api/user-prefs. Retrying the same key with an identical body replays the original preferences response instead of applying the update again.
API key cache invalidation
POST /api/invalidate-user-api-key-cache
Invalidates a user’s entitlement cache after a subscription change (Dodo webhook → Convex → this endpoint). Internal — requires RELAY_SHARED_SECRET.
Geo utilities
GET /api/geo
Geo-IP echo: returns { "country": "<ISO2>" } derived from the CDN’s country header for the calling IP. Takes no parameters. (For reverse geocoding of coordinates, use GET /api/infrastructure/v1/reverse-geocode?lat=…&lon=….)
GET /api/reverse-geocode?lat=40.7&lon=-74.0
Reverse geocodes a lat/lon to the nearest country + city via OpenStreetMap Nominatim. Results are cached on a 0.001-degree grid for seven days, and the route is limited to 60 req/min/IP.
Account & session helpers
These routes back the dashboard and Settings UI. They are documented here so their behavior is discoverable, but they are internal helpers, not versioned product contracts — shapes can change with the UI that consumes them.GET /api/me/entitlement
Returns {"isPro": true|false} for the signed-in user. Requires a Clerk bearer token (Authorization: Bearer …); a missing or invalid token returns 401 {"error":"unauthenticated"} so callers can distinguish “not signed in” from “signed in, free tier”. Used by the /pro marketing page to swap upgrade CTAs. Always Cache-Control: private, no-store.
GET /api/user/mcp-quota
Settings-UI read of the caller’s MCP daily quota, from the same counter the MCP server enforces against. Requires a Clerk session. Returns {"used": 12, "limit": 250, "resetsAt": "<next UTC midnight>"}; limit: null means unlimited. Free-account callers see the free-allowance meter instead. Backend failures fail soft (used: 0 / plan default) rather than erroring. Cache-Control: no-store.
POST /api/user/mcp-revoke
Settings-UI revocation of one Pro MCP token. Requires a Clerk session; the user id comes from the verified session, never the body. Body: {"tokenId": "<id>"}. Returns 200 {"ok":true}; errors: 400 (invalid_json, missing_token_id), 401, 404 not_found (deliberately collapsed against token enumeration), 409 already_revoked, 503 service_unavailable with Retry-After: 5. Revocation takes effect on the MCP server within its 60-second negative-cache window.
Operational endpoints
These anonymous internal-operations surfaces are not part of the public API contract./api/analytics-health and /api/correlation-runtime-mode are origin-gated to WorldMonitor origins (other origins get a plain 403 Forbidden). /api/security/report is the bounded, wildcard-CORS exception described below.
POST /api/security/report
Anonymous browser Reporting API sink for COOP/COEP violation reports (wired via the site-wide Reporting-Endpoints: wm-coop-coep="/api/security/report" header). It intentionally uses wildcard CORS (Access-Control-Allow-Origin: *) rather than the operational origin guard. It accepts application/reports+json / application/report+json / application/json (else 415), bodies up to 32 KiB (else 413), applies the shared per-IP limiter, and answers successful reports with 204 and no body. Report URLs are reduced to origins before logging — no query strings or tokens are retained.
POST /api/analytics-health
Aggregate telemetry counter for the analytics collector’s own health. Accepts a tiny JSON body (≤1 KiB) of per-cohort write/failure counters — no event payloads, user ids, URLs, or fingerprints are accepted. Answers 204. Rate-limited 60/min (fail-closed).
GET /api/correlation-runtime-mode
Read-only control-plane switch: returns {"mode": "legacy"|"exact"|"fuzzy"} telling browser and seeder paths which correlation engine is active, without a redeploy. There is no write surface. If the backing store is unreachable it still returns 200 with "legacy". Cache-Control: no-store.
Utilities
GET /api/download?platform=<id>&variant=<id>
Redirects to the matching asset on the latest GitHub release of koala73/worldmonitor. Returns 302 to the asset URL on success, or 302 to releases/latest on any failure (unknown platform, no match, GitHub error).
platform (required, exact string):
variant (optional): full, world, tech, finance, commodity, energy, happy.
One desktop binary ships and every variant is selected in-app after install, so
each supported value resolves to the same World Monitor asset for the requested
platform — the parameter records which variant the caller came from, it does not
select a different download. An unrecognized value redirects to
releases/latest
without calling GitHub. Omitting variant resolves the same way a supported value does —
the identity filter applies on every path, so a release carrying a stray asset that merely
matches the platform suffix never wins.
Caches the 302 for 5 minutes (s-maxage=300, stale-while-revalidate=60, stale-if-error=600).
POST /api/leads/v1/submit-contact
Public enterprise contact form. Turnstile-verified, rate-limited per IP. Part of LeadsService.
POST /api/leads/v1/register-interest
Captures email for Pro-waitlist signup. Writes to Convex and sends a confirmation email. Part of LeadsService.
Browser callers must pass Turnstile. Desktop callers using source: "desktop-settings" bypass Turnstile only when the request is authenticated with the shared desktop secret:
X-WorldMonitor-Desktop-Timestamp: Unix epoch milliseconds, within 5 minutes of server time.X-WorldMonitor-Desktop-Signature:sha256=<hex HMAC-SHA256>.
<timestamp>\n<canonical JSON>, where canonical JSON contains email, source, appVersion, referredBy, website, and turnstileToken in that order. Configure WM_DESKTOP_SHARED_SECRET on both the desktop sidecar and the cloud API. During rollout, WM_DESKTOP_AUTH_ALLOW_LEGACY=true only accepts unsigned legacy desktop requests while the cloud API has no WM_DESKTOP_SHARED_SECRET configured. Once the cloud secret is set, desktop requests fail closed unless they include a valid signature, still subject to the tighter desktop rate limit.