Files
worldmonitor/docs/api-notifications.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

116 lines
4.8 KiB
Plaintext

---
title: "Notifications & Integrations"
description: "Notification channels, webhook delivery, and Telegram/Slack/Discord/YouTube integration endpoints."
---
## Notification channels
Users can register multiple delivery channels (webhook, Telegram, Slack, Discord, email) and bind alert rules to them.
### `GET /api/notification-channels`
Lists the caller's registered channels and alert rules.
```json
{
"channels": [
{ "id": "chn_01", "type": "webhook", "url": "https://hooks.example.com/...", "active": true },
{ "id": "chn_02", "type": "telegram", "chatId": "@alerts_xyz", "active": true }
],
"alertRules": [
{ "id": "rul_01", "channelId": "chn_01", "trigger": "brief_ready", "filter": null }
]
}
```
### `POST /api/notification-channels`
Action-dispatched writer. The body's `action` field selects the mutation:
| action | Purpose |
|--------|---------|
| `create-pairing-token` | Mint a one-time pairing token (optional `variant`) for the mobile / Tauri client to bind a push channel. |
| `set-channel` | Register or update a channel. For `webhook` channels the `webhookEnvelope` URL is validated HTTPS-only, must not resolve to a private/loopback address, and is AES-256-GCM encrypted before storage. Optional `email`, `webhookLabel` (truncated to 100 chars). |
| `set-web-push` | Register a browser Web Push subscription for the signed-in user. |
| `delete-channel` | Remove a channel by type (`email`, `webhook`, `telegram`, `web-push`, etc.). |
| `set-alert-rules` | Replace the caller's alert-rules set in one shot. |
| `set-quiet-hours` | Set do-not-disturb windows. |
| `set-digest-settings` | Configure digest cadence and channel routing. |
All actions require Clerk bearer + PRO (`tier >= 1`). Invalid actions return `400 Unknown action`. Requests are forwarded to Convex via `RELAY_SHARED_SECRET`.
## Webhook delivery contract
When an alert fires, registered webhook URLs receive:
- **Method**: `POST`
- **Headers**:
- `Content-Type: application/json`
- `X-WM-Signature: sha256=<HMAC-SHA256(body, channelSecret)>`
- `X-WM-Delivery-Id: <ulid>`
- `X-WM-Event: <event-name>`
- **Body** (envelope v1):
```json
{
"envelope": 1,
"event": "brief_ready",
"deliveryId": "01HX...",
"occurredAt": "2026-04-19T06:00:00Z",
"data": { "issueDate": "2026-04-19", "magazineUrl": "..." }
}
```
Signature verification: `hmac_sha256(rawBody, channelSecret) == X-WM-Signature[7:]`.
<Warning>
The envelope version is **shared across three producers** (`notification-relay`, `proactive-intelligence`, `seed-digest-notifications`). Bumping it requires coordinated updates.
</Warning>
### `POST /api/notify`
Internal ingestion endpoint called by Railway producers to enqueue a notification. Requires `RELAY_SHARED_SECRET`. Not a public API.
## Telegram
### `GET /api/telegram-feed?userId=...`
Returns the pre-rendered brief feed for a given Telegram-linked user. Used by the Telegram mini-app.
## YouTube
### `GET /api/youtube/embed?videoId=...`
SSR'd YouTube embed iframe with CSP-compatible wrapping. Used to bypass WKWebView autoplay restrictions on the desktop app.
### `GET /api/youtube/live?channel=<handle>` or `?videoId=<11-char-id>`
Returns live-stream metadata for a YouTube channel (`channel` — handle with or without `@` prefix) or a specific video (`videoId` — 11-char YouTube id). At least one of the two params is required; returns `400 Missing channel or videoId parameter` otherwise. Response cached 10 min for channel lookups, 1 hour for videoId lookups.
Proxies to the Railway relay first (residential proxy for YouTube scraping). On relay failure, falls back to YouTube oEmbed (for `videoId`) or direct channel scraping — both are unreliable from datacenter IPs.
## Slack integration
### `POST /api/slack/oauth/start`
Authenticated (Clerk JWT + PRO). Body is empty. Server generates a one-time CSRF state token, stores the caller's userId in Upstash keyed by that state (10-min TTL), and returns the Slack authorize URL for the frontend to open in a popup.
```json
{ "oauthUrl": "https://slack.com/oauth/v2/authorize?client_id=...&scope=incoming-webhook&..." }
```
Errors: 401 (missing/invalid JWT), 403 `pro_required`, 503 (OAuth not configured or Upstash unavailable).
### `GET /api/slack/oauth/callback`
Unauthenticated — the popup lands here after Slack redirects. Validates the state token, exchanges `code` for an incoming-webhook URL, AES-256-GCM encrypts the webhook, and stores it in Convex. Returns a tiny HTML page that `postMessage`s the opener and closes.
## Discord integration
### `POST /api/discord/oauth/start`
Authenticated (Clerk JWT + PRO). Same shape as the Slack start route — returns `{ oauthUrl }` for a popup.
### `GET /api/discord/oauth/callback`
Unauthenticated. Exchanges `code`, stores the guild webhook, and `postMessage`s the opener.