Auth matrix
forceKey: true — which endpoints ignore browser session cookies?
Some endpoints explicitly reject anonymous browser session cookies and require a user API key, enterprise API key, or Pro Clerk bearer even from inside the dashboard:
/api/v2/shipping/route-intelligence/api/v2/shipping/webhooks/api/widget-agent- Vendor / partner endpoints
X-WorldMonitor-Key is the canonical header.
Browser session mode
CORS decides whether a browser is allowed to read the response, butOrigin is not authentication. Browser public reads authenticate with a short-lived wms_ session token minted by /api/wm-session and carried in the wm-session HttpOnly cookie.
- Allowed origins get
Access-Control-Allow-Origin: <echoed>and can use credentialed browser cookies. - Disallowed origins are rejected by the edge function guard before the route body runs.
- Requests with no
Originheader, such ascurlor server-to-server calls, are not blocked by CORS; they still need the route’s normal credentials.
API key mode
Generate a key
API-tier subscribers get a key automatically on subscription. To rotate, contact support.Use it
wm_ followed by 40 lowercase hex characters. Enterprise keys are opaque operator-issued strings and are only distributed out of band. Keep keys out of client-side code — use a server-side proxy if you need to call from the browser to a forceKey endpoint.
X-WorldMonitor-Key is the canonical header. API-key-authenticated endpoints also accept X-Api-Key as an alias for compatibility with generic API clients, including standalone edge functions that use validateApiKey() and gateway-backed routes. Do not send user API keys as bearer tokens or query-string parameters unless an endpoint explicitly documents that form.
For /api/bootstrap, server-side callers should use https://api.worldmonitor.app/api/bootstrap with one of these API-key headers. There is no separate gateway host, token-exchange step, activation step, or IP allow-list requirement for standard server-to-server access. The endpoint’s anonymous weather path (?keys=weatherAlerts) is public only when no key header is sent — once you attach X-WorldMonitor-Key/X-Api-Key, the request is validated even for weather, so a key without current API access returns 403 rather than falling back to anonymous data.
Server-side validation
The edge function callsvalidateApiKey(req, { forceKey?: boolean }):
- Desktop origins must send an enterprise key in
X-WorldMonitor-Key. - If
forceKeyis false, a validwms_browser session cookie satisfies the anonymous/public gate. - Enterprise keys are checked against
WORLDMONITOR_VALID_KEYS. - User keys with the
wm_+ 40-hex shape are validated against the user-key table and currentapiAccessentitlement. Gateway-backed routes use the gateway fallback;/api/bootstrapperforms the same user-key lookup in its Edge-safe platform helper. - If none passes → 401.
OAuth bearer (MCP only)
Full flow documented at OAuth 2.1 Server. For client setup, see MCP.Clerk session (authenticated dashboard)
The dashboard exchanges Clerk’s__session cookie for a JWT and sends it on user-specific API calls:
jose with a cached JWKS — no round-trip to Clerk per request. Implemented in server/auth-session.ts. See Authentication overview for full details.
Entitlement / tier gating
Valid key ≠ PRO. Authentication and entitlement are orthogonal. Every PRO-gated endpoint runs a separateisCallerPremium(req) check (server/_shared/premium-check.ts) that does not accept Origin or an anonymous browser session as proof of PRO.
isCallerPremium returns true only when one of these is present:
- A valid
X-WorldMonitor-Key(env-allowlisted fromWORLDMONITOR_VALID_KEYS, or a user-ownedwm_-prefixed key whose Convex record has theapiAccessentitlement), or - A Clerk
Authorization: Bearer …token whose user has roleproor Dodo entitlement tier ≥ 1.
premiumFetch() (src/services/premium-fetch.ts) handles this by injecting one of those credentials on every request. Desktop app uses WORLDMONITOR_API_KEY from the runtime config. Server-to-server callers must send the header explicitly.
Tier is resolved from Convex on each call, so a subscription change takes effect on the next request (after cache invalidation).
