> ## Documentation Index
> Fetch the complete documentation index at: https://www.worldmonitor.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Platform Endpoints

> Bootstrap, health, version, cache-purge, and user-preference endpoints — the plumbing every World Monitor client talks to on startup and cleanup.

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`), `liveVideoResolved` (browser `max-age=300`, 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](/docs/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.

```json theme={null}
{
  "version": "2.6.7",
  "tag": "v2.6.7",
  "url": "https://github.com/koala73/worldmonitor/releases/tag/v2.6.7",
  "prerelease": false
}
```

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):
  ```json theme={null}
  {
    "keys":     ["market:stocks-bootstrap:v1", "infra:outages:v1"],
    "patterns": ["market:sectors:*"],
    "dryRun":   false
  }
  ```
* **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**:
  ```json theme={null}
  { "matched": 4, "deleted": 4, "keys": ["..."], "dryRun": false, "truncated": false }
  ```

## Health

### `GET /api/health`

Aggregated freshness report for **all registered seed keys**. Returns `HEALTHY`, `WARNING`, `DEGRADED`, `UNHEALTHY`, `REDIS_DOWN`, or `REFRESH_PENDING` 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.

Evaluated verdicts return HTTP 200. HTTP 503 with `REDIS_DOWN` means the required Redis health read failed; While a refresh is pending, a request that does not own it gets the last published verdict with HTTP 200 and `"stale": true`. HTTP 503 with `REFRESH_PENDING` means no verdict is retained while refresh is pending. Retry pending responses after `Retry-After: 3`; they contain no evaluated summary or `checkedAt`. The scheduled freshness monitor retries only pending responses within a bounded 45-second window (at most 12 requests), then fails if no verdict becomes available. 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.

```json theme={null}
{
  "status": "HEALTHY",
  "checkedAt": "2026-08-07T12:00:00Z",
  "summary": {
    "total": 312,
    "ok": 312,
    "warn": 0,
    "containedWarn": 0,
    "onDemandWarn": 0,
    "staleContent": 0,
    "rolloutPending": 0,
    "crit": 0
  },
  "checks": {
    "marketQuotes": { "status": "OK", "records": 78, "seedAgeMin": 12 },
    "earthquakes": { "status": "OK", "records": 142, "seedAgeMin": 8 }
  }
}
```

`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](/docs/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.

```json theme={null}
{
  "layout": "classic",
  "enabledLayers": ["conflict", "aviation", "maritime"],
  "defaultCountry": "US"
}
```

* **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](https://github.com/koala73/worldmonitor/releases/latest) on any failure (unknown platform, no match, GitHub error).

**`platform`** (required, exact string):

| value | matches |
| - | - |
| `windows-exe` | `*_x64-setup.exe` |
| `windows-msi` | `*_x64_en-US.msi` |
| `macos-arm64` | `*_aarch64.dmg` |
| `macos-x64` | `*_x64.dmg` (excluding `*setup*`) |
| `linux-appimage` | `*_amd64.AppImage` |
| `linux-appimage-arm64` | `*_aarch64.AppImage` |

**`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](https://github.com/koala73/worldmonitor/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.
