{"openapi":"3.1.0","info":{"title":"Haiven Notification Hub API","description":"Multi-channel notification dispatcher for Haiven agent services.\n\n## Overview\nThe Notification Hub accepts a structured notification payload and routes it to one or\nmore delivery channels (email via SMTP, ntfy.sh push, or Home Assistant TTS webhook)\nbased on a YAML routing table keyed by source agent.\n\n## Channel Resolution\nWhen a notification is received, the hub resolves the effective channel set in this order:\n1. `channels` field in the request (explicit override, bypasses routing table)\n2. Routes table entry for `source_agent`\n3. `default` route entry\n4. Hard fallback: `[\"email\"]`\n\n## Supported Channels\n- **email**: SMTP delivery via smtp-relay (Mailpit). Sends HTML email with markdown rendering.\n- **ntfy**: Push notification via self-hosted ntfy.sh. Header-based publish API.\n- **ha_tts**: Home Assistant webhook for TTS announcements on media players.\n\n## Routing Configuration\nRoutes are defined in `/etc/haiven/notification-routes.yaml` (host: `/mnt/storage/notification-hub/routes/`).\nLoaded once at startup. Missing or malformed file falls back to `default: [\"email\"]` without failing.\n\n## Base URLs\n- Production (Traefik): https://hub.haiven.site\n- Internal (Docker): http://notification-hub:8000\n\n## Authentication\nNo authentication required. Service is LAN-only on the `web` + `backend` Docker networks.\nA Traefik rate limit of 10 req/s (burst 20) is applied.\n","version":"1.0.0","contact":{"name":"Haiven Infrastructure","url":"https://home.haiven.site"}},"servers":[{"url":"https://hub.haiven.site","description":"Production (Traefik, LAN only)"},{"url":"http://notification-hub:8000","description":"Internal (Docker network)"},{"url":"http://localhost:8040","description":"Local development (port forwarded)"}],"tags":[{"name":"Notifications","description":"Send notifications to configured channels"},{"name":"Health","description":"Service health and readiness probes"},{"name":"Observability","description":"Prometheus metrics endpoint"}],"paths":{"/notify":{"post":{"summary":"Dispatch a notification","description":"Accept a structured notification and dispatch it to the resolved channel set.\n\n**Channel resolution order:**\n1. `channels` field (explicit override) if non-empty\n2. Routes table entry for `source_agent`\n3. `default` route entry\n4. Hard fallback: `[\"email\"]`\n\nAll resolved channels are called concurrently. The `status` field in the response\nreflects the aggregate outcome:\n- `ok`: all resolved channels succeeded\n- `partial`: at least one channel succeeded, at least one failed\n- `failed`: no channels succeeded\n\nChannels with missing required configuration (e.g. `HUB_NTFY_URL` not set for ntfy)\nare silently skipped and counted as failures.\n","operationId":"postNotify","tags":["Notifications"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyRequest"},"examples":{"basic_email":{"summary":"Send a simple email notification","value":{"source_agent":"briefing","title":"Daily Briefing Ready","body":"## Morning Summary\n\nYou have **5 open tasks**...","priority":"normal"}},"all_channels":{"summary":"Force delivery to all channels","value":{"source_agent":"system","title":"Disk Alert","body":"Disk usage on /mnt exceeded 90%.","priority":"urgent","channels":["email","ntfy","ha_tts"]}},"with_metadata":{"summary":"Include metadata for channel customization","value":{"source_agent":"research","title":"Research Complete","body":"Your research session has finished.","priority":"normal","metadata":{"session_id":"abc-123","query":"RAG chunking strategies"}}}}}}},"responses":{"200":{"description":"Notification dispatched (check `status` field for delivery outcome)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotifyResponse"},"examples":{"all_succeeded":{"summary":"All channels delivered successfully","value":{"notification_id":"550e8400-e29b-41d4-a716-446655440000","channels_dispatched":["email","ntfy"],"status":"ok"}},"partial":{"summary":"Some channels failed","value":{"notification_id":"550e8400-e29b-41d4-a716-446655440001","channels_dispatched":["email"],"status":"partial"}},"all_failed":{"summary":"All channels failed","value":{"notification_id":"550e8400-e29b-41d4-a716-446655440002","channels_dispatched":[],"status":"failed"}}}}}},"422":{"description":"Validation error — `title` or `body` field is empty or missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}},"/health":{"get":{"summary":"Health check","description":"Liveness probe — returns 200 when the process is running and the notification\nrouter has been initialized. Does not check external dependencies (SMTP relay,\nntfy server, Home Assistant).\n","operationId":"getHealth","tags":["Health"],"responses":{"200":{"description":"Service is running","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"],"description":"Always \"ok\" when the service is reachable"}}},"example":{"status":"ok"}}}}}}},"/metrics":{"get":{"summary":"Prometheus metrics","description":"Minimal Prometheus metrics endpoint satisfying scrape auto-discovery via Docker labels.\n\nThe container has `prometheus.scrape=true`, `prometheus.port=8000`, and\n`prometheus.path=/metrics` labels set for automatic Prometheus discovery.\n\nCurrently exposes:\n- `notification_hub_up` (gauge): Always 1 when the service is running\n","operationId":"getMetrics","tags":["Observability"],"responses":{"200":{"description":"Prometheus text format metrics","content":{"text/plain":{"schema":{"type":"string"},"example":"# HELP notification_hub_up Service is running\n# TYPE notification_hub_up gauge\nnotification_hub_up 1\n"}}}}}}},"components":{"schemas":{"NotifyRequest":{"type":"object","required":["source_agent","title","body"],"properties":{"source_agent":{"type":"string","description":"Agent identifier used to look up the routing table entry.\nBuilt-in routing entries: `briefing`, `research`, `eod`.\nUnknown agents fall through to the `default` route.\n","examples":["briefing","research","eod","system"]},"priority":{"type":"string","enum":["low","normal","high","urgent"],"default":"normal","description":"Notification urgency level.\n- `low`: Background status, completed batch jobs\n- `normal`: Standard briefings and routine updates\n- `high`: Action required\n- `urgent`: System alerts, failures needing immediate attention\nPriority affects ntfy channel behavior (sound, vibration, persistence).\nEmail and ha_tts channels do not differentiate priority levels.\n"},"title":{"type":"string","minLength":1,"description":"Notification title. Used as email subject line and ntfy title header."},"body":{"type":"string","minLength":1,"description":"Notification body. Markdown is supported and rendered to HTML for email delivery.\nFor ha_tts, markdown symbols are stripped and text truncated to 300 characters.\n"},"channels":{"type":"array","items":{"type":"string","enum":["email","ntfy","ha_tts"]},"default":[],"description":"Explicit channel override. When non-empty, bypasses the routing table entirely.\nLeave empty to use the routing table for this source_agent.\n"},"metadata":{"type":"object","additionalProperties":true,"default":{},"description":"Arbitrary key-value pairs forwarded to channel implementations.\nThe ntfy channel reads `metadata.source_agent` to set a notification tag.\n"}}},"NotifyResponse":{"type":"object","required":["notification_id","channels_dispatched","status"],"properties":{"notification_id":{"type":"string","format":"uuid","description":"Unique identifier for this notification. Useful for log correlation."},"channels_dispatched":{"type":"array","items":{"type":"string"},"description":"List of channel names that successfully received the notification."},"status":{"type":"string","enum":["ok","partial","failed"],"description":"Aggregate delivery status:\n- `ok`: All resolved channels succeeded\n- `partial`: At least one succeeded, at least one failed\n- `failed`: No channels succeeded\n"}}},"ValidationError":{"type":"object","properties":{"detail":{"type":"string","description":"Human-readable description of the validation failure"}},"example":{"detail":"title and body are required"}}}}}