Files
worldmonitor/docs/usage-errors.mdx
Elie Habib e4c95ad9be docs(mintlify): cover MCP, OAuth, non-RPC endpoints, and usage (#3209)
* docs(mintlify): cover MCP, OAuth, non-RPC endpoints, and usage

Audit against api/ + proto/ revealed 9 OpenAPI specs missing from nav,
the scenario/v1 service undocumented, and MCP (32 tools + OAuth 2.1 flow)
with no user-facing docs. The stale Docs_To_Review/API_REFERENCE.md still
pointed at pre-migration endpoints that no longer exist.

- Wire 9 orphaned specs into docs.json: ConsumerPrices, Forecast, Health,
  Imagery, Radiation, Resilience, Sanctions, Thermal, Webcam
- Hand-write ScenarioService.openapi.yaml (3 RPCs) until it's proto-backed
  (tracked in issue #3207)
- New MCP page with tool catalog + client setup (Claude Desktop/web, Cursor)
- New MDX for OAuth, Platform, Brief, Commerce, Notifications, Shipping v2,
  Proxies
- New Usage group: quickstart, auth matrix, rate limits, errors
- Remove docs/Docs_To_Review/API_REFERENCE.md and EXTERNAL_APIS.md
  (referenced dead endpoints); add README flagging dir as archival

* docs(mintlify): move scenario docs out of generated docs/api/ tree

The pre-push hook enforces that docs/api/ is proto-generated only.
Replace the hand-written ScenarioService.openapi.yaml with a plain
MDX page (docs/api-scenarios.mdx) until the proto migration lands
(tracked in issue #3207).

* docs(mintlify): fix factual errors flagged in PR review

Reviewer caught 5 endpoints where I speculated on shape/method/limits
instead of reading the code. All fixes cross-checked against the
source:

- api-shipping-v2: route-intelligence is GET with query params
  (fromIso2, toIso2, cargoType, hs2), not POST with a JSON body.
  Response shape is {primaryRouteId, chokepointExposures[],
  bypassOptions[], warRiskTier, disruptionScore, ...}.
- api-commerce: /api/product-catalog returns {tiers, fetchedAt,
  cachedUntil, priceSource} with tier groups free|pro|api_starter|
  enterprise, not the invented {currency, plans}. Document the
  DELETE purge path too.
- api-notifications: Slack/Discord /oauth/start are POST + Clerk
  JWT + PRO (returning {oauthUrl}), not GET redirects. Callbacks
  remain GET.
- api-platform: /api/version returns the latest GitHub Release
  ({version, tag, url, prerelease}), not deployed commit/build
  metadata.
- api-oauth + mcp: /api/oauth/register limit is 5/60s/IP (match
  code), not 10/hour.

Also caught while double-checking: /api/register-interest and
/api/contact are 5/60min and 3/60min respectively (1-hour window,
not 1-minute). Both require Turnstile. Removed the fabricated
limits for share-url, notification-channels, create-checkout
(they fall back to the default per-IP limit).

* docs(mintlify): second-round fixes — verify every claim against source

Reviewer caught 7 more cases where I described API behavior I hadn't
read. Each fix below cross-checked against the handler.

- api-commerce (product-catalog): tiers are flat objects with
  monthlyPrice/annualPrice/monthlyProductId/annualProductId on paid
  tiers, price+period for free, price:null for enterprise. There is
  no nested plans[] array.
- api-commerce (referral/me): returns {code, shareUrl}, not counts.
  Code is a deterministic 8-char HMAC of the Clerk userId; binding
  into Convex is fire-and-forget via ctx.waitUntil.
- api-notifications (notification-channels): actual action set is
  create-pairing-token, set-channel, set-web-push, delete-channel,
  set-alert-rules, set-quiet-hours, set-digest-settings. Replaced
  the made-up list.
- api-shipping-v2 (webhooks): alertThreshold is numeric 0-100
  (default 50), not a severity string. Subscriber IDs are wh_+24hex;
  secret is raw 64-char hex (no whsec_ prefix). POST registration
  returns 201. Added the management routes: GET /{id},
  POST /{id}/rotate-secret, POST /{id}/reactivate.
- api-platform (cache-purge): auth is Authorization: Bearer
  RELAY_SHARED_SECRET, not an admin-key header. Body takes keys[]
  and/or patterns[] (not {key} or {tag}), with explicit per-request
  caps and prefix-blocklist behavior.
- api-platform (download): platform+variant query params, not
  file=<id>. Response is a 302 to a GitHub release asset; documented
  the full platform/variant tables.
- mcp: server also accepts direct X-WorldMonitor-Key in addition to
  OAuth bearer. Fixed the curl example which was incorrectly sending
  a wm_live_ API key as a bearer token.
- api-notifications (youtube/live): handler reads channel or videoId,
  not channelId.
- usage-auth: corrected the auth-matrix row for /api/mcp to reflect
  that OAuth is one of two accepted modes.

* docs(mintlify): fix Greptile review findings

- mcp.mdx: 'Five' slow tools → 'Six' (list contains 6 tools)
- api-scenarios.mdx: replace invalid JSON numeric separator
  (8_400_000_000) with plain integer (8400000000)

Greptile's third finding — /api/oauth/register rate-limit contradiction
across api-oauth.mdx / mcp.mdx / usage-rate-limits.mdx — was already
resolved in commit 4f2600b2a (reviewed commit was eb5654647).
2026-04-19 15:03:16 +04:00

73 lines
3.3 KiB
Plaintext

---
title: "Errors"
description: "Standard error shape, common status codes, and retry strategies."
---
## Error shape
All error responses are JSON with `Content-Type: application/json`:
```json
{ "error": "brief_not_found" }
```
Some endpoints include additional fields:
```json
{
"error": "invalid_request",
"error_description": "iso2 must be a 2-letter uppercase country code",
"field": "iso2"
}
```
OAuth endpoints follow [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) error codes: `invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`.
## Status codes
| Code | Meaning | Retry? |
|------|---------|--------|
| `200` | OK | — |
| `202` | Accepted — job enqueued (e.g. `scenario/v1/run`). Poll `statusUrl`. | — |
| `304` | Not Modified (conditional cache hit) | — |
| `400` | Bad request — validation error | No — fix input |
| `401` | Missing / invalid auth | No — fix auth |
| `403` | Authenticated but not entitled (usually `pro_required`) | No — upgrade |
| `404` | Resource / tool / brief / entity not found | No |
| `405` | Method not allowed | No |
| `409` | Conflict (duplicate webhook registration, etc.) | No |
| `413` | Payload too large | No |
| `429` | Rate limited | Yes — honor `Retry-After` |
| `500` | Server bug | Yes — with backoff, then report |
| `502` | Upstream / Convex / Dodo failure | Yes — exponential backoff |
| `503` | Service unavailable — missing env or dependency down | Yes — exponential backoff |
| `504` | Upstream timeout | Yes — with backoff |
## Common error strings
| `error` value | Meaning |
|---------------|---------|
| `UNAUTHENTICATED` | No valid Clerk JWT or API key. |
| `pro_required` | Authenticated, but account is not PRO. |
| `invalid_payload` | Body failed schema validation. |
| `invalid_date_shape` | Date param not `YYYY-MM-DD`. |
| `brief_not_found` | No composed brief for the requested `{userId, issueDate}`. |
| `Rate limit exceeded` | 429 fired; honor `Retry-After`. |
| `Service temporarily unavailable` | Upstash or another hard dependency missing at request time. |
| `service_unavailable` | Signing secret / required env not configured. |
| `Failed to enqueue scenario job` | Redis pipeline failure on `/api/scenario/v1/run`. |
## Retry strategy
**Idempotent reads** (`GET`): retry 429/5xx with exponential backoff (1s, 2s, 4s, cap 30s, 5 attempts). Most GET responses are cached at the edge, so the retry usually goes faster.
**Writes**: never auto-retry 4xx. For 5xx on writes, inspect: `POST /api/brief/share-url` and `POST /api/v2/shipping/webhooks` are idempotent; `POST /api/scenario/v1/run` is **not** — it enqueues a new job on each call. Retrying a 5xx on `run` may double-charge the rate-limit counter.
**MCP**: the server returns tool errors in the JSON-RPC result with `isError: true` and a text explanation — those are not HTTP errors. Handle them at the tool-call layer.
## Debugging
- Every edge response includes `x-vercel-id` and `x-worldmonitor-deploy` headers — include those when reporting issues.
- Sentry alerts forward to [status.worldmonitor.app](https://status.worldmonitor.app/).
- `GET /api/health` and `GET /api/seed-health` show per-seed freshness; a stale seed is the most common root cause of unexpected empty payloads.