{"openapi":"3.1.0","info":{"title":"ig1-events","description":"IG1 platform event bus (Kafka publish + ring-buffer polling)","version":"0.5.7"},"paths":{"/health/live":{"get":{"tags":["health"],"summary":"Liveness","operationId":"liveness_health_live_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health/ready":{"get":{"tags":["health"],"summary":"Readiness","description":"Ready only while the producer and consumer are connected to Kafka.","operationId":"readiness_health_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/events":{"get":{"tags":["events"],"summary":"List Events","description":"Poll recent events from the in-memory ring buffer, newest last.\n\nScoped to the caller's resolved tenant (W9): customers see only their\nown project's events, admins see every event including the untenanted\nplatform-wide ones. Unmapped callers get the verbatim 403 the rest of\nthe surface uses — never a silent full-buffer read.","operationId":"list_events_v1_events_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"ISO-8601 lower bound on the event timestamp (exclusive)","title":"Since"},"description":"ISO-8601 lower bound on the event timestamp (exclusive)"},{"name":"topic","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to one topic","title":"Topic"},"description":"Filter to one topic"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/events/{topic}":{"post":{"tags":["events"],"summary":"Publish Event","description":"Schema-validated publish — PLATFORM SERVICE CREDENTIALS ONLY (W9).\n\n`source` is the authenticated publisher's user_id; the envelope's\ntenant stamp is derived server-side from the validated payload\n(schemas.tenant_from_payload), never from a caller-named envelope\nfield. Unknown topics 404; schema violations 422; a non-platform\ncredential 403 (fail closed — see ig1_events/auth.require_publisher).","operationId":"publish_event_v1_events__topic__post","security":[{"HTTPBearer":[]}],"parameters":[{"name":"topic","in":"path","required":true,"schema":{"type":"string","title":"Topic"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks":{"get":{"tags":["webhooks"],"summary":"List Webhooks","description":"List the caller-visible webhooks (tier 0+; admin: all, customer: own\nproject only). Secrets/hashes are never included.","operationId":"list_webhooks","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["webhooks"],"summary":"Create Webhook","description":"Create a subscription (tier 1+). The signing secret is generated\nserver-side and returned ONCE in this response — it is never stored in\nthe clear (the store keeps its sha256 hex, which is also the HMAC key;\nsubscribers verify with sha256_hex(secret) — see ig1_events/webhooks.py).\nThe URL must be https AND must resolve to a routable address: loopback,\nlink-local, RFC1918 and unique-local targets are refused here and again\nbefore every delivery (the W9 SSRF guard — webhooks.validate_webhook_url\n/ assert_deliverable).","operationId":"create_webhook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/webhooks/{webhook_id}":{"delete":{"tags":["webhooks"],"summary":"Delete Webhook","description":"Delete a subscription (tier 2). Cross-tenant ids answer 404.","operationId":"delete_webhook","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{webhook_id}/deliveries":{"get":{"tags":["webhooks"],"summary":"List Webhook Deliveries","description":"The per-webhook delivery log (tier 0+): one row per ATTEMPT, newest\nfirst, bounded (last 100). THIS INSTANCE'S attempts — the log is not\npersisted and the replicas split the bus between them, which the\nresponse's `scope` field states.","operationId":"list_webhook_deliveries","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{webhook_id}/test":{"post":{"tags":["webhooks"],"summary":"Test Webhook","description":"Send a synthetic test event through the signed delivery path (tier\n1+) — the envelope is POSTed to the subscriber with the real signature\nheader and the attempt(s) land in the delivery log. The test event is\nnever published to the bus.","operationId":"test_webhook","security":[{"HTTPBearer":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/events/topics":{"get":{"tags":["events"],"summary":"List Topics","description":"The topic list the console offers when subscribing.\n\nServed rather than hardcoded in the client: a console that carries its own\ncopy drifts the day a topic is added, and the drift is silent — the user\nsimply never sees the new topic offered.","operationId":"list_topics","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/events/topics/{topic}/history":{"get":{"tags":["events"],"summary":"Get Topic History","description":"The DURABLE audit trail, read from the topic itself (phase 54).\n\n`since` defaults to the 30-day retention class, `until` to now. One\npage is at most 500 rows, oldest first; `next_cursor` continues the\nwindow exactly where this page ended (per-partition offsets), and is\nabsent when the window is exhausted. The read rule is the ring's own:\na customer sees only their resolved project's rows; a platform admin\nsees everything, including the untenanted rows.","operationId":"get_topic_history","security":[{"HTTPBearer":[]}],"parameters":[{"name":"topic","in":"path","required":true,"schema":{"type":"string","title":"Topic"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"ISO-8601 window start (default: 30 days ago — the audit retention class)","title":"Since"},"description":"ISO-8601 window start (default: 30 days ago — the audit retention class)"},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"ISO-8601 window end (default: now)","title":"Until"},"description":"ISO-8601 window end (default: now)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Rows per page, oldest first (max 500)","default":200,"title":"Limit"},"description":"Rows per page, oldest first (max 500)"},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Continuation token from the previous page's `next_cursor`","title":"Cursor"},"description":"Continuation token from the previous page's `next_cursor`"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/alertmanager":{"post":{"tags":["alerts"],"summary":"Receive Alertmanager","description":"Alertmanager v4 webhook sink — the in-cluster destination that replaces\nthe empty vendor URL.\n\nAUTH IS A SHARED BEARER, not the OIDC path every other route uses:\nAlertmanager speaks `http_config.authorization.credentials_file` and cannot\nperform a client_credentials flow. The token is generated by phase-13 and\nmounted into both this service and Alertmanager from the same Secret.\n\nReturns 202 with what was accepted. A malformed body is a 400 — loudly,\nbecause the whole class of bug this endpoint exists to end is an alert path\nthat fails quietly.","operationId":"receive_alertmanager","parameters":[{"name":"source","in":"query","required":false,"schema":{"type":"string","description":"which Alertmanager sent this","default":"k3s","title":"Source"},"description":"which Alertmanager sent this"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/alerts/watchdog":{"get":{"tags":["alerts"],"summary":"Watchdog Status","description":"The dead-man's switch, as a number something else can alert on.\n\nThe Watchdog alert fires constantly BY DESIGN, so its arrival proves the\nwhole path — Prometheus evaluating, Alertmanager routing, this service\nreceiving. Its silence is the signal, and silence cannot page itself: this\nendpoint exists so a monitor in ANOTHER failure domain (kolla's Prometheus\non the OpenStack control trio) can scrape the age and fire when it grows.","operationId":"watchdog_status","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/integrations":{"get":{"tags":["integrations"],"summary":"List Integrations","description":"The destinations this caller may see (tier 0+).\n\nAdmins see every scope; a customer sees only their own project's, never a\nplatform one — where the fleet's alerts go is not a customer's business.","operationId":"list_integrations","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]},"post":{"tags":["integrations"],"summary":"Create Integration","description":"Create a destination.\n\nThe tier gate here is 0 on purpose and the REAL check is\nauthorize_scope, which needs tier 2 for a tenant scope and platform-admin\nfor a platform scope. Putting the whole rule in one function means the\ncreate side and the read side (visible_to) cannot drift apart — a\nrequire_tier(2) decorator plus a scope check inside would be two rules.","operationId":"create_integration","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/integrations/{integration_id}":{"patch":{"tags":["integrations"],"summary":"Update Integration","description":"Edit a destination. scope/project_id/kind are immutable — see\nstore.MUTABLE_FIELDS for why a kind flip is delete-and-recreate.","operationId":"update_integration","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["integrations"],"summary":"Delete Integration","operationId":"delete_integration","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/{integration_id}/deliveries":{"get":{"tags":["integrations"],"summary":"Integration Deliveries","description":"Recent delivery ATTEMPTS, newest first.\n\nPer-pod and best-effort — the durable record of what happened is the bus.\nSaid plainly here because a drawer that silently shows one replica's view\nwould otherwise read as \"nothing was delivered\".","operationId":"integration_deliveries","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/integrations/{integration_id}/test":{"post":{"tags":["integrations"],"summary":"Test Integration","description":"Send a synthetic event through the REAL delivery path.\n\nNot a dry run: it renders the same way, authenticates the same way and\ncrosses the same network, so a green result proves the credential and the\nroute rather than the row's existence. The synthetic event is severity\n`info` and status `resolved` precisely so it cannot open a live PagerDuty\nincident someone then has to close.","operationId":"test_integration","security":[{"HTTPBearer":[]}],"parameters":[{"name":"integration_id","in":"path","required":true,"schema":{"type":"string","title":"Integration Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["root"],"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IntegrationCreate":{"properties":{"kind":{"type":"string","title":"Kind"},"name":{"type":"string","maxLength":120,"minLength":1,"title":"Name"},"topics":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Topics"},"scope":{"type":"string","title":"Scope","default":"tenant"},"min_severity":{"type":"string","title":"Min Severity","default":"none"},"enabled":{"type":"boolean","title":"Enabled","default":true},"config":{"additionalProperties":true,"type":"object","title":"Config","default":{}}},"additionalProperties":false,"type":"object","required":["kind","name","topics"],"title":"IntegrationCreate","description":"Create body. `scope` is a REQUEST, never a grant: integrations.\nauthorize_scope decides what the caller may actually own and returns the\nproject_id to stamp. A client cannot name the project (phase-20 rule)."},"IntegrationPatch":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"topics":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Topics"},"min_severity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Min Severity"},"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"}},"additionalProperties":false,"type":"object","title":"IntegrationPatch"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookCreate":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":8,"title":"Url"},"topics":{"items":{"type":"string"},"type":"array","maxItems":32,"minItems":1,"title":"Topics"},"enabled":{"type":"boolean","title":"Enabled","default":true}},"type":"object","required":["url","topics"],"title":"WebhookCreate","description":"The create body — url/topics/enabled only; the tenant is the resolved\ncredential's project (never caller-supplied) and the secret is generated\nserver-side (returned ONCE in the response)."}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}