| --- |
| title: "Notifications & Integrations" |
| description: "Notification channels, webhook delivery, and Telegram, Slack, Discord, and YouTube integration endpoints for pushing World Monitor alerts." |
| --- |
|
|
| ## Notification channels |
|
|
| Users can register multiple delivery channels (webhook, Telegram, Slack, Discord, email) and bind alert rules to them. |
|
|
| Digest and brief notifications use the same story pool and editorial guardrails |
| documented in [News Digest and Briefing Methodology](/methodology/news-digest-and-briefing). |
|
|
| ### `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, and PRO here specifically means a **billed entitlement row** at `tier >= 1`. A Clerk session whose role is `pro` but which has no entitlement row does **not** qualify, unlike the gateway's tier-1 REST gate: notification delivery is enforced a second time inside Convex (`assertProEntitlement`), so the edge gate returns the clean `403 pro_required` rather than letting the request fail deeper with a less useful error. Invalid actions return `400 Unknown action`. Requests are forwarded to Convex via `RELAY_SHARED_SECRET`. |
|
|
| A caller without a billed row gets `403 pro_required` only when the entitlement is confirmed non-Pro. When entitlement verification is itself in doubt the gate follows the shared billing-verification contract instead: `503` with `Retry-After` and `X-Billing-Verification` for `entitlement_verification_unavailable` / `renewal_verification_pending` / `renewal_verification_failed`, and `403 subscription_lapsed` for a confirmed lapse. See [Error handling](/usage-errors). |
|
|
| Clients should honor that `503` with at least one retry at the advertised `Retry-After` before surfacing a failure β the dashboard's own service layer (`src/services/notification-channels.ts`) makes exactly one bounded retry. For `entitlement_verification_unavailable` specifically, retrying earlier than `Retry-After` is wasted: that answer is briefly negative-cached server-side, so an early retry is served the same result. The two `renewal_verification_*` codes are not negative-cached, but their delay reflects a real provider re-check or cooldown, so retrying early is still answered from the same state. |
|
|
| - **Idempotency**: optional `Idempotency-Key` supported on `POST /api/notification-channels`. Retrying the same key with an identical body replays the original response instead of applying the channel action again. |
|
|
| ## 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 two producers** (`notification-relay`, `seed-digest-notifications`). Bumping it requires coordinated updates. |
| </Warning> |
|
|
| ### `POST /api/notify` |
|
|
| Authenticated event-publish endpoint for PRO callers. Requires Clerk bearer auth and an active PRO entitlement, then enqueues the accepted event into the notification queue. Relay-internal control events such as `flush_quiet_held` and `channel_welcome` are reserved and rejected. |
|
|
| Errors: 401 (missing/invalid JWT), 403 `pro_required` β and, like every Pro-gated endpoint here, the shared billing-verification contract when entitlement verification is itself in doubt: `503` with `Retry-After` and `X-Billing-Verification` for `entitlement_verification_unavailable` / `renewal_verification_pending` / `renewal_verification_failed`, and `403 subscription_lapsed` for a confirmed lapse. See [Error handling](/usage-errors). |
|
|
| - **Idempotency**: optional `Idempotency-Key` supported. Retrying the same key with an identical body replays the original enqueue response instead of publishing the notification again. |
|
|
| ## 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 |
|
|
| 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). A 503 here can **also** be the retryable billing-verification denial β `entitlement_verification_unavailable` / `renewal_verification_pending` / `renewal_verification_failed`, carrying `Retry-After` and `X-Billing-Verification`; a confirmed lapse is `403 subscription_lapsed`. Branch on the `code` field rather than the status alone, since the misconfiguration 503 is not retryable. See [Error handling](/usage-errors). |
|
|
| ### `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, and the same error set, including the billing-verification 503/403 codes described there. |
|
|
| ### `GET /api/discord/oauth/callback` |
|
|
| Unauthenticated. Exchanges `code`, stores the guild webhook, and `postMessage`s the opener. |
|
|