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. The top-level status describes user-visible platform availability, so HEALTHY can coexist with contained source warnings that still serve usable last-good data.
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 availability via UptimeRobot / Better Stack with ?compact=1 and alert on any status other than HEALTHY. Strict data-quality monitors must also inspect summary.warn and problems, because contained warnings remain visible there. The full detailed view requires an operator/enterprise API key because it includes canonical cache key names and freshness thresholds.
summary.warn is the complete actionable warning census. summary.containedWarn is a subset, not an additional bucket. STALE_SEED, SEED_ERROR, STALE_CONTENT, COVERAGE_PARTIAL, COVERAGE_DEGRADED, and CHINA_DEGRADED are eligible for containment when the current sweep finds the served payload, metadata proves positive records, and all required reader diagnostics are structurally usable. Stale age remains visible as diagnostic truth but does not by itself mean that the platform stopped serving data. Availability remains HEALTHY while all actionable warnings are contained and the cohort is at or below 3% of probed keys. Missing or unusable data, malformed or unknown evidence, incompatible reader policies or cache state, REDIS_PARTIAL, ROLLOUT_PENDING, broader impact, and critical failures do not qualify.
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>", "sharedWithRestApi": false}; limit: null means unlimited, and sharedWithRestApi: true means the number reported is the account’s REST allowance, so it counts REST requests too. 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.