Skip to main content
These endpoints are not part of a domain RPC service — they sit at the root of the API surface and handle platform concerns.

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-session cookie, X-WorldMonitor-Key, or the X-Api-Key alias. User-issued keys are validated for current API access.
  • Anonymous weather: ?keys=weatherAlerts is public only when no API key header is sent. If you attach X-WorldMonitor-Key / X-Api-Key, the request is fully validated even for weather — a malformed key returns 401, a key without current API access returns 403. Callers that always send a key must send a valid, entitled key (or omit the header to use the anonymous weather path). This URL is no-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=1 is 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/bootstrap directly with X-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=1 URLs are shared-cacheable, because only they answer every caller identically. ?tier=fast&public=1 / ?tier=slow&public=1 use browser max-age=60 / max-age=300 and CDN s-maxage=600 / s-maxage=7200. Single-key public URLs: on-demand keys (?keys=<onDemandName>&public=1) inherit the slow profile — browser max-age=300, CDN s-maxage=7200 — unless the key declares its own, which every key published more often than that shield does: correlationCards (browser max-age=60, CDN s-maxage=300), chinaDecisionSignals (browser max-age=60, CDN s-maxage=900), canadaRoads (browser max-age=60, CDN s-maxage=900), albertaRoads (browser max-age=60, CDN s-maxage=900), manitobaRoads (browser max-age=60, CDN s-maxage=900), marketCorrelationSeries (browser max-age=60, CDN s-maxage=900), imdCycloneMarine (browser max-age=60, CDN s-maxage=900), bcOpen511 (browser max-age=60, CDN s-maxage=1800), flightDelays (browser max-age=60, CDN s-maxage=1800), and forecasts (browser max-age=300, CDN s-maxage=3600); ?keys=weatherAlerts&public=1 uses Cache-Control: public, s-maxage=600, stale-while-revalidate=120, stale-if-error=900 with the fast-tier CDN shield. Everything else — key-authenticated, session-authenticated, the unmarked ?tier=... URLs, and the anonymous ?keys=weatherAlerts path — uses Cache-Control: no-store and 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 under data, plus a missing list for cache keys not present in Redis.
Use this on initial page load to avoid 40 parallel RPC calls. The on-demand tier includes 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.
Cached 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 returns 401.
  • Body (at least one of keys / patterns required):
  • 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 match military: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-Key supported on POST /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>.
The HMAC input is <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.