Skip to main content

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.
{
  "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:
actionPurpose
create-pairing-tokenMint a one-time pairing token (optional variant) for the mobile / Tauri client to bind a push channel.
set-channelRegister 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-pushRegister a browser Web Push subscription for the signed-in user.
delete-channelRemove a channel by type (email, webhook, telegram, web-push, etc.).
set-alert-rulesReplace the caller’s alert-rules set in one shot.
set-quiet-hoursSet do-not-disturb windows.
set-digest-settingsConfigure 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):
    {
      "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:].
The envelope version is shared across three producers (notification-relay, proactive-intelligence, seed-digest-notifications). Bumping it requires coordinated updates.

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.
{ "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 postMessages 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 postMessages the opener.