{"components":{"responses":{"Forbidden":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Key lacks the required scope."},"InvalidParameter":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"A query/body parameter is missing or malformed."},"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Resource does not exist."},"RateLimited":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Per-minute or per-day limit exceeded. Inspect the `error.code`:\n`rate_limited` for per-minute, `quota_exhausted` for per-day.\n","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}}},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid API key."}},"schemas":{"ArchivedStory":{"description":"Denormalised snapshot of a story that has aged out of the live feed. Subset of ``Story`` \u2014 fields populated by post-ingest pipelines (``body_text``, ``entities``, ``sentiment``) are not present on archived rows.","properties":{"archived_at":{"format":"date-time","nullable":true,"type":"string"},"author":{"nullable":true,"type":"string"},"category":{"type":"string"},"country":{"nullable":true,"properties":{"iso2":{"nullable":true,"type":"string"},"name":{"nullable":true,"type":"string"}},"type":"object"},"description":{"type":"string"},"id":{"description":"Hex MD5 of source URL.","type":"string"},"image_url":{"nullable":true,"type":"string"},"language":{"type":"string"},"published_at":{"format":"date-time","type":"string"},"publisher":{"properties":{"favicon_url":{"nullable":true,"type":"string"},"name":{"type":"string"},"site_url":{"nullable":true,"type":"string"}},"type":"object"},"secondary_topic":{"nullable":true,"type":"string"},"tags":{"items":{"type":"string"},"type":"array"},"title":{"type":"string"},"topic":{"nullable":true,"type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"},"Cluster":{"properties":{"ai_overview":{"nullable":true,"type":"string"},"bias":{"description":"Coverage bias for the cluster: the Left/Center/Right distribution of member stories by publisher lean, plus the side under-covering (``blindspot``). Null until the cluster has been scored.","nullable":true,"properties":{"blindspot":{"description":"The side barely covering this story, or null.","enum":["left","right"],"nullable":true,"type":"string"},"breakdown":{"description":"Counts of member stories per lean bucket.","properties":{"center":{"type":"integer"},"confident":{"description":"Counts restricted to high-confidence ratings (editorial, third-party, or automated above the confidence floor) \u2014 the subset the blindspot decision is based on.","properties":{"center":{"type":"integer"},"left":{"type":"integer"},"right":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"left":{"type":"integer"},"right":{"type":"integer"},"total":{"type":"integer"},"unknown":{"type":"integer"}},"type":"object"},"by_country":{"additionalProperties":{"properties":{"center":{"type":"integer"},"left":{"type":"integer"},"right":{"type":"integer"},"total":{"type":"integer"},"unknown":{"type":"integer"}},"type":"object"},"description":"Per-country Left/Center/Right/unknown breakdown for the dominant covering countries (keyed by ISO 3166-1 alpha-2), so a story covered across several national presses can be read per axis. Null for single-country clusters.","nullable":true,"type":"object"}},"type":"object"},"breaking_expires_at":{"format":"date-time","nullable":true,"type":"string"},"breaking_score":{"nullable":true,"type":"number"},"breaking_started_at":{"format":"date-time","nullable":true,"type":"string"},"country_count":{"type":"integer"},"dominant_country":{"nullable":true,"type":"string"},"dominant_language":{"nullable":true,"type":"string"},"dominant_tags":{"items":{"type":"string"},"type":"array"},"first_published_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"is_breaking":{"type":"boolean"},"last_published_at":{"format":"date-time","type":"string"},"members":{"items":{"properties":{"added_at":{"format":"date-time","type":"string"},"similarity_score":{"nullable":true,"type":"number"},"story":{"$ref":"#/components/schemas/Story"}},"type":"object"},"type":"array"},"publisher_count":{"type":"integer"},"representative_story":{"$ref":"#/components/schemas/Story"},"secondary_topic":{"nullable":true,"type":"string"},"sentiment":{"description":"Cluster-level sentiment + framing analysis. Populated by the LLM framing job once a cluster reaches the analysis threshold. ``analysis`` and ``framing_summary`` are free-shape JSON objects; treat unknown keys as additive.","nullable":true,"properties":{"analysis":{"additionalProperties":true,"type":"object"},"framing_summary":{"additionalProperties":true,"type":"object"},"updated_at":{"format":"date-time","nullable":true,"type":"string"}},"type":"object"},"story_count":{"type":"integer"},"topic":{"nullable":true,"type":"string"}},"type":"object"},"ClusterList":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Cluster"},"type":"array"},"links":{"$ref":"#/components/schemas/Links"},"meta":{"$ref":"#/components/schemas/Meta"}},"type":"object"},"Delivery":{"properties":{"attempts":{"type":"integer"},"created_at":{"format":"date-time","type":"string"},"delivered_at":{"format":"date-time","nullable":true,"type":"string"},"delivery_id":{"description":"Stable per-delivery UUID; appears as X-Infomundi-Delivery on every attempt.","type":"string"},"event_type":{"type":"string"},"last_error":{"nullable":true,"type":"string"},"last_status_code":{"nullable":true,"type":"integer"},"next_retry_at":{"format":"date-time","nullable":true,"type":"string"},"status":{"enum":["pending","succeeded","failed","dead"],"type":"string"}},"type":"object"},"Error":{"properties":{"error":{"properties":{"code":{"example":"invalid_parameter","type":"string"},"message":{"type":"string"}},"required":["code","message"],"type":"object"}},"type":"object"},"Links":{"properties":{"next":{"format":"uri","type":"string"},"prev":{"format":"uri","type":"string"},"self":{"format":"uri","type":"string"}},"type":"object"},"Me":{"properties":{"data":{"properties":{"client":{"properties":{"client_id":{"format":"uuid","type":"string"},"contact_email":{"format":"email","type":"string"},"created_at":{"format":"date-time","type":"string"},"daily_limit":{"type":"integer"},"name":{"type":"string"},"plan":{"type":"string"},"requests_per_minute":{"type":"integer"},"status":{"enum":["active","suspended","cancelled"],"type":"string"}},"type":"object"},"effective_limits":{"properties":{"daily":{"type":"integer"},"max_page_size":{"type":"integer"},"per_minute":{"type":"integer"}},"type":"object"},"key":{"properties":{"created_at":{"format":"date-time","type":"string"},"expires_at":{"format":"date-time","nullable":true,"type":"string"},"key_id":{"format":"uuid","type":"string"},"last_used_at":{"format":"date-time","nullable":true,"type":"string"},"mode":{"enum":["live","test"],"type":"string"},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"}},"type":"object"},"plan":{"properties":{"allowed_scopes":{"items":{"type":"string"},"type":"array"},"daily_request_limit":{"type":"integer"},"max_page_size":{"type":"integer"},"monthly_request_limit":{"type":"integer"},"name":{"type":"string"},"price_cents_monthly":{"type":"integer"},"requests_per_minute":{"type":"integer"},"slug":{"type":"string"}},"type":"object"}},"type":"object"}},"type":"object"},"Meta":{"properties":{"has_more":{"type":"boolean"},"page_size":{"type":"integer"},"returned":{"type":"integer"}},"type":"object"},"Publisher":{"properties":{"category":{"nullable":true,"type":"string"},"corroboration":{"nullable":true,"properties":{"avg_cluster_countries":{"format":"float","type":"number"},"avg_cluster_size":{"format":"float","type":"number"},"rate":{"format":"float","type":"number"},"stories_corroborated":{"type":"integer"},"stories_evaluated":{"type":"integer"},"updated_at":{"format":"date-time","type":"string"}},"type":"object"},"favicon_url":{"format":"uri","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"is_active":{"type":"boolean"},"name":{"type":"string"},"site_url":{"format":"uri","type":"string"},"source_subtype":{"nullable":true,"type":"string"},"source_type":{"type":"string"}},"type":"object"},"PublisherList":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Publisher"},"type":"array"},"links":{"$ref":"#/components/schemas/Links"},"meta":{"$ref":"#/components/schemas/Meta"}},"type":"object"},"SearchResult":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Story"},"type":"array"},"links":{"$ref":"#/components/schemas/Links"},"meta":{"properties":{"page":{"type":"integer"},"page_size":{"type":"integer"},"query":{"type":"string"},"returned":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}},"type":"object"},"Story":{"properties":{"archived":{"type":"boolean"},"author":{"nullable":true,"type":"string"},"body_extracted_at":{"description":"Timestamp the body was extracted. Same scope gate as ``body_text``.","format":"date-time","nullable":true,"type":"string"},"body_text":{"description":"Cleaned full-article body, extracted from the source URL at ingest time. **Only returned when the authenticated key carries the paid `stories:fulltext` scope** \u2014 non-fulltext keys see the same response with this field absent. Capped at ~50KB; longer articles are truncated on a character boundary. Stories that pre-date the scraper rollout, or where extraction failed (paywalls, 404s, anti-bot walls), return ``null``.","nullable":true,"type":"string"},"country":{"nullable":true,"properties":{"iso2":{"type":"string"},"name":{"type":"string"}},"type":"object"},"created_at":{"format":"date-time","type":"string"},"description":{"type":"string"},"entities":{"description":"Named entities extracted at ingest by the NER pipeline. ``null`` for stories that pre-date the pipeline or where extraction failed; ``[]`` when extraction succeeded but found no entities (e.g. a generic headline).","items":{"properties":{"count":{"minimum":1,"type":"integer"},"text":{"description":"Canonical surface form.","type":"string"},"type":{"enum":["PERSON","ORG","LOC","MISC"],"type":"string"}},"type":"object"},"nullable":true,"type":"array"},"id":{"description":"Hex MD5 of source URL.","type":"string"},"image_url":{"nullable":true,"type":"string"},"language":{"type":"string"},"published_at":{"format":"date-time","type":"string"},"publisher":{"properties":{"bias":{"description":"Publisher political lean, or null if unrated.","nullable":true,"properties":{"bucket":{"enum":["left","center","right"],"type":"string"},"confidence":{"nullable":true,"type":"number"},"lean_score":{"description":"-3 (far left) .. +3 (far right).","type":"number"},"political_lean":{"enum":["far_left","left","lean_left","center","lean_right","right","far_right"],"type":"string"},"source":{"enum":["manual","aggregated","model"],"nullable":true,"type":"string"},"updated_at":{"format":"date-time","nullable":true,"type":"string"}},"type":"object"},"corroboration_rate":{"nullable":true,"type":"number"},"favicon_url":{"nullable":true,"type":"string"},"name":{"type":"string"},"site_url":{"type":"string"}},"type":"object"},"secondary_topic":{"nullable":true,"type":"string"},"sentiment":{"description":"Sentiment + (optionally) framing analysis for the story. Two sources feed this block, in order of preference: the lightweight ingest-time sentiment analyser populates ``score`` / ``label`` / ``analysed_at`` for every recent story; the cluster-level pipeline additionally fills ``framing_tags`` and ``framing_angle`` once the story has been clustered. ``null`` when neither pass has run yet.","nullable":true,"properties":{"analysed_at":{"format":"date-time","nullable":true,"type":"string"},"framing_angle":{"description":"Short freeform description of the dominant framing angle.","nullable":true,"type":"string"},"framing_tags":{"items":{"type":"string"},"type":"array"},"label":{"enum":["positive","neutral","negative"],"nullable":true,"type":"string"},"score":{"description":"Signed sentiment score in [-1, 1].","format":"float","maximum":1.0,"minimum":-1.0,"nullable":true,"type":"number"}},"type":"object"},"tags":{"items":{"type":"string"},"type":"array"},"title":{"type":"string"},"topic":{"nullable":true,"type":"string"},"url":{"format":"uri","type":"string"}},"type":"object"},"StoryList":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Story"},"type":"array"},"links":{"$ref":"#/components/schemas/Links"},"meta":{"$ref":"#/components/schemas/Meta"}},"type":"object"},"Tracker":{"properties":{"created_at":{"format":"date-time","type":"string"},"description":{"nullable":true,"type":"string"},"filters":{"$ref":"#/components/schemas/TrackerFilters"},"is_active":{"type":"boolean"},"last_alert_fired_at":{"description":"Most recent time this tracker fired a `tracker.volume_alert`.\nUsed by the matcher to debounce \u2014 re-fire is suppressed until\na full window has passed.\n","format":"date-time","nullable":true,"type":"string"},"last_matched_at":{"format":"date-time","nullable":true,"type":"string"},"match_count":{"description":"Lifetime number of matches logged for this tracker.","type":"integer"},"name":{"type":"string"},"notify_webhook":{"type":"boolean"},"query":{"description":"Meilisearch query (supports quoted phrases, AND, OR).","type":"string"},"tracker_id":{"format":"uuid","type":"string"},"updated_at":{"format":"date-time","type":"string"},"volume_alert_threshold":{"description":"Fire a `tracker.volume_alert` event when matches in the trailing\n`volume_alert_window_minutes` window cross this count. `null`\ndisables volume alerting on the tracker.\n","maximum":100000,"minimum":1,"nullable":true,"type":"integer"},"volume_alert_window_minutes":{"default":60,"description":"Rolling window length used for volume alert evaluation.","maximum":1440,"minimum":5,"type":"integer"}},"type":"object"},"TrackerCreate":{"properties":{"backfill_hours":{"default":0,"description":"On creation, walk this many hours of recent stories before\n\"now\" when seeding the watermark, so the first match scan\npicks up existing coverage. Capped at 72.\n","maximum":72,"minimum":0,"type":"integer"},"description":{"maxLength":255,"nullable":true,"type":"string"},"filters":{"$ref":"#/components/schemas/TrackerFilters"},"is_active":{"default":true,"type":"boolean"},"name":{"maxLength":120,"type":"string"},"notify_webhook":{"default":false,"type":"boolean"},"query":{"maxLength":500,"type":"string"}},"required":["name","query"],"type":"object"},"TrackerFilters":{"description":"Optional filter blob narrowing the universe before lexical match.\nCross-key clauses AND together; within-key lists OR. Every list\nis capped at 50 items \u2014 split into multiple trackers for wider\ncoverage.\n","properties":{"categories":{"description":"Either full category names (`br_general`, `us_technology`) or\nbare slugs (`general`, `technology`). Bare slugs match the\nsame topic across every country.\n","items":{"type":"string"},"type":"array"},"countries":{"description":"ISO-3166 alpha-2 codes, case-insensitive.","items":{"maxLength":2,"minLength":2,"type":"string"},"type":"array"},"publishers":{"description":"Publisher public ids (hex). Use the `/publishers` endpoint to\ndiscover ids.\n","items":{"type":"string"},"type":"array"},"regions":{"description":"Region names from our geography table (e.g. `Europe`,\n`Americas`). Expanded to member country list at query time \u2014\nunknown names are silently ignored.\n","items":{"type":"string"},"type":"array"},"source_types":{"description":"Only consider stories originating from these source platforms.\n","items":{"enum":["rss","twitter","telegram"],"type":"string"},"type":"array"},"topics":{"description":"Story topic slugs (e.g. `politics`, `business`).","items":{"type":"string"},"type":"array"}},"type":"object"},"TrackerPreviewRequest":{"properties":{"days":{"default":7,"description":"Trailing days to count matches over.","maximum":30,"minimum":1,"type":"integer"},"filters":{"$ref":"#/components/schemas/TrackerFilters"},"query":{"maxLength":500,"type":"string"}},"required":["query"],"type":"object"},"TrackerPreviewResult":{"properties":{"days":{"type":"integer"},"filters":{"$ref":"#/components/schemas/TrackerFilters"},"match_count_total":{"description":"Stories matching in the window.","type":"integer"},"query":{"type":"string"},"sample":{"description":"Up to 10 most-recent matching stories. Shape matches `/stories`.","items":{"$ref":"#/components/schemas/Story"},"type":"array"},"sample_count":{"type":"integer"}},"type":"object"},"TrackerStats":{"properties":{"by_day":{"description":"Day-grained volume series. Preserved for backwards compatibility\nwith day-only sparklines; prefer `series` for new code since it\nhonours the requested `granularity` and carries sentiment.\n","items":{"properties":{"count":{"type":"integer"},"date":{"format":"date","type":"string"}},"type":"object"},"type":"array"},"entity_aggregation_cap":{"type":"integer"},"entity_aggregation_capped":{"description":"True when the window held more matches than `entity_aggregation_cap`,\nso entity rollups are computed from the most recent slice only.\n","type":"boolean"},"granularity":{"description":"Bucket size used to compute `series`.","enum":["hour","day","week"],"type":"string"},"series":{"description":"Granularity-bucketed volume + sentiment series. `bucket` is an\nISO-8601 timestamp (hourly), date (daily), or ISO Monday of\nthe week (weekly). `sentiment_avg` is `null` when no story in\nthe bucket has a sentiment score yet.\n","items":{"properties":{"bucket":{"type":"string"},"count":{"type":"integer"},"sentiment_avg":{"maximum":1,"minimum":-1,"nullable":true,"type":"number"},"sentiment_breakdown":{"properties":{"negative":{"type":"integer"},"neutral":{"type":"integer"},"positive":{"type":"integer"},"unscored":{"type":"integer"}},"type":"object"}},"type":"object"},"type":"array"},"top_countries":{"items":{"properties":{"count":{"type":"integer"},"country":{"description":"ISO-3166 alpha-2.","type":"string"}},"type":"object"},"type":"array"},"top_entities":{"description":"Most-mentioned named entities across matched stories.","items":{"properties":{"count":{"type":"integer"},"text":{"type":"string"}},"type":"object"},"type":"array"},"top_entity_types":{"description":"Coarse entity-type rollup (PER, ORG, LOC, etc.).","items":{"properties":{"count":{"type":"integer"},"type":{"type":"string"}},"type":"object"},"type":"array"},"top_publishers":{"items":{"properties":{"count":{"type":"integer"},"name":{"nullable":true,"type":"string"},"publisher_id":{"format":"uuid","nullable":true,"type":"string"},"site_url":{"nullable":true,"type":"string"}},"type":"object"},"type":"array"},"top_topics":{"items":{"properties":{"count":{"type":"integer"},"topic":{"type":"string"}},"type":"object"},"type":"array"},"total":{"type":"integer"},"window":{"properties":{"from":{"format":"date-time","type":"string"},"to":{"format":"date-time","type":"string"}},"type":"object"}},"type":"object"},"TrackerUpdate":{"properties":{"description":{"maxLength":255,"nullable":true,"type":"string"},"filters":{"$ref":"#/components/schemas/TrackerFilters"},"is_active":{"type":"boolean"},"name":{"maxLength":120,"type":"string"},"notify_webhook":{"type":"boolean"},"query":{"maxLength":500,"type":"string"},"volume_alert_threshold":{"description":"Set to a positive integer to enable volume alerting; pass `null`\n(or `0`) to disable. Crossing the threshold within\n`volume_alert_window_minutes` fires a `tracker.volume_alert`\nevent.\n","maximum":100000,"minimum":1,"nullable":true,"type":"integer"},"volume_alert_window_minutes":{"description":"Rolling window length (minutes) for volume alert evaluation.","maximum":1440,"minimum":5,"type":"integer"}},"type":"object"},"UsageDay":{"properties":{"date":{"format":"date","type":"string"},"error_4xx":{"type":"integer"},"error_5xx":{"type":"integer"},"success":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"UsageEndpointRow":{"properties":{"count":{"type":"integer"},"endpoint_route":{"description":"Flask endpoint name (e.g. `api_v1.list_stories`). Stable across\nURL changes as long as the view function keeps its name.\n","type":"string"},"error_4xx_count":{"type":"integer"},"error_5xx_count":{"type":"integer"},"max_latency_ms":{"type":"integer"},"mean_latency_ms":{"nullable":true,"type":"number"},"success_count":{"type":"integer"}},"type":"object"},"UsageSeries":{"properties":{"data":{"items":{"$ref":"#/components/schemas/UsageDay"},"type":"array"},"meta":{"properties":{"days":{"type":"integer"},"effective_daily_limit":{"type":"integer"},"from":{"format":"date","type":"string"},"scope":{"enum":["client","key"],"type":"string"},"to":{"format":"date","type":"string"},"totals":{"properties":{"error_4xx":{"type":"integer"},"error_5xx":{"type":"integer"},"success":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}},"type":"object"}},"type":"object"},"Webhook":{"properties":{"consecutive_failures":{"type":"integer"},"created_at":{"format":"date-time","type":"string"},"description":{"nullable":true,"type":"string"},"event_types":{"items":{"type":"string"},"type":"array"},"is_active":{"type":"boolean"},"last_delivery_at":{"format":"date-time","nullable":true,"type":"string"},"platform":{"description":"Delivery flavour. `generic` posts a signed envelope; the\nothers post native chat-platform JSON.\n","enum":["generic","slack","discord","teams"],"type":"string"},"subscription_id":{"format":"uuid","type":"string"},"url":{"format":"uri","type":"string"},"uses_hmac":{"description":"True when this subscription is HMAC-signed (`platform=generic`).\nFalse for chat-platform rows.\n","type":"boolean"}},"type":"object"}},"securitySchemes":{"ApiKey":{"bearerFormat":"imk_live_<prefix>.<secret>","scheme":"bearer","type":"http"}}},"info":{"contact":{"email":"support@infomundi.net","name":"Infomundi API support","url":"https://infomundi.net"},"description":"The Infomundi B2B API exposes the same news ingestion and clustering\npipeline that powers the public Infomundi website. Use it to integrate\nmulti-source news coverage, cross-country cluster context, and live\nbreaking-news signals into your own product.\n\n## Authentication\nAll endpoints (except `/health`, `/openapi.json`, `/openapi.yaml`, and\n`/docs`) require an API key passed as a Bearer token:\n```\nAuthorization: Bearer imk_live_<prefix>.<secret>\n```\nKeys belong to an account (\"client\"); a single client can mint many\nkeys (typically one per environment). Generate keys from the admin\ndashboard (or via the self-serve developer portal if your deploy has\nit enabled).\n\n## Rate limits and quotas\nThree ceilings apply:\n  * **Per-minute** burst limit (per key) set by your plan's\n    `requests_per_minute`. Exceeding it returns `429` with a\n    `Retry-After` header.\n  * **Per-day** quota (per key) set by your plan's\n    `daily_request_limit`. Exhausting it returns `429 quota_exhausted`\n    with `scope: daily` until 00:00 UTC.\n  * **Per-month** quota (per client \u2014 shared across all keys) set by\n    your plan's `monthly_request_limit`. Exhausting it returns\n    `429 quota_exhausted` with `scope: monthly` until the 1st of next\n    month UTC. Daily reservations are auto-refunded if the monthly\n    ceiling trips on the same request.\n\nAll ceilings are echoed on every response:\n```\nX-RateLimit-Limit-Minute      / Remaining-Minute / Reset-Minute\nX-RateLimit-Limit-Day         / Remaining-Day    / Reset-Day\nX-RateLimit-Limit-Month       / Remaining-Month  / Reset-Month\n```\nThe `Reset-*` headers are unix-second timestamps of the next window\nboundary, suitable for direct comparison against your clock.\n\nKeys in `test` mode bypass daily/monthly enforcement (they're for\nintegration testing) but their traffic still shows up in `/me/usage`.\n\n## Pagination\nList endpoints use opaque cursors. Pass the `next` link verbatim\n(or extract its `cursor` query parameter) to fetch the next page.\nTreat the cursor as opaque \u2014 its shape is internal.\n\n## Webhook deliveries\nWhen a subscribed event fires, we POST a signed JSON envelope to your\n`url`. The body is:\n```json\n{\n  \"id\": \"9a2b...\",\n  \"type\": \"breaking_news.started\",\n  \"created_at\": \"2026-05-25T11:32:09Z\",\n  \"data\": { /* event-specific payload */ }\n}\n```\nEvery request carries these headers:\n```\nX-Infomundi-Event:     <event_type>\nX-Infomundi-Delivery:  <delivery uuid, unique per attempt>\nX-Infomundi-Timestamp: <unix seconds>\nX-Infomundi-Signature: sha256=<hex>\n```\nTo verify, compute `HMAC_SHA256(signing_secret, f\"{ts}.{raw_body}\")`\nand compare against the hex after `sha256=`. Reject requests where\n`|now - ts| > 300s` to prevent replay.\n\nNon-2xx responses are retried with exponential backoff\n(`30s \u2192 2m \u2192 10m \u2192 1h \u2192 6h \u2192 24h`); after exhaustion the delivery is\nmarked `dead`. Subscriptions with sustained consecutive failures are\nautomatically deactivated \u2014 re-create the subscription to resume.\n\n### Treat payload fields as untrusted text\nFree-text fields inside `data` (story titles, descriptions,\npublisher names, tracker labels) originate from upstream RSS feeds\nand customer-controlled tracker configuration. They are passed\nthrough verbatim and may contain HTML, control characters, or\nformatting that would be unsafe to render or forward without\nescaping. Always escape these fields at the point they're displayed\n(HTML, Markdown, terminals, Slack/Discord notifications, etc.) \u2014\na valid signature only authenticates the *origin* of the payload,\nnot the safety of its contents.\n\n## Errors\nErrors always follow the shape:\n```json\n{\n  \"error\": {\n    \"code\": \"invalid_parameter\",\n    \"message\": \"Parameter 'from' must be ISO-8601.\",\n    \"parameter\": \"from\"\n  }\n}\n```\nThe `code` is a stable machine handle. The `message` is for humans\nand may change between releases. Additional fields (`parameter`,\n`required_scope`, etc.) are added per error class.\n","license":{"name":"Proprietary"},"summary":"News, clusters, and breaking-news intelligence as a service.","title":"Infomundi API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/archive/stories":{"get":{"description":"Read-only window onto stories that have aged out of the live\nfeed and now live in the archive table. Use this for research\nbackfills, brand-historical coverage, and compliance audits.\nRequires the paid `archive:read` scope (Pro / Enterprise plans).\n\nResponse shape is a subset of `/stories` \u2014 fields populated by\npost-ingest pipelines (`body_text`, `entities`, `sentiment`)\nare not available on archived rows. Filters that depend on\nthose fields (sentiment, entity) are likewise not accepted.\n","parameters":[{"in":"query","name":"publisher","schema":{"type":"string"}},{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"topic","schema":{"type":"string"}},{"description":"ISO-3166 alpha-2.","in":"query","name":"country","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"to","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ArchivedStory"},"type":"array"},"links":{"$ref":"#/components/schemas/Links"},"meta":{"$ref":"#/components/schemas/Meta"}},"type":"object"}}},"description":"Page of archived stories."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"List historical (archived) stories.","tags":["stories"]}},"/audit":{"get":{"description":"Cursor-paginated newest-first. Pass `cursor` from the previous\nresponse's `links.next` to walk through history. Optional\n`action` filter accepts an exact dotted action (`key.created`)\nor a category prefix (`key` \u2192 all `key.*`).\n\nRequired scope: `audit:read` (Enterprise plan).\n","parameters":[{"in":"query","name":"cursor","schema":{"type":"string"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}},{"in":"query","name":"action","schema":{"type":"string"}}],"responses":{"200":{"description":"Audit-log page."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"}},"summary":"Recent audit-log entries for the authenticated client.","tags":["audit"]}},"/audit/exports":{"get":{"description":"Lists the per-client push destinations for signed JSONL drops\nof the audit log. Two destination kinds: `s3` (PutObject to a\ncustomer-controlled bucket) and `https` (POST to a\nSplunk/Datadog/Sumo HEC-style endpoint).\n\nRequired scope: `audit:read`.\n","responses":{"200":{"description":"Export configs."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenScope"}},"summary":"List configured SIEM export destinations.","tags":["audit"]},"post":{"description":"Creates the push config. The fresh signing secret is returned\nonce and used by the customer's SIEM ingester to verify the\n`X-Infomundi-Signature` HMAC on each drop.\n\nRequired scope: `audit:manage`.\n","responses":{"201":{"description":"Export config created."},"400":{"$ref":"#/components/responses/InvalidParameter"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"description":"A config already exists for this destination."}},"summary":"Create a new SIEM export destination.","tags":["audit"]}},"/breaking":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterList"}}},"description":"Up-to-the-minute set of breaking clusters."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"Currently breaking clusters.","tags":["breaking"]}},"/categories":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"properties":{"name":{"example":"br_general","type":"string"}},"type":"object"},"type":"array"},"meta":{"properties":{"count":{"type":"integer"}},"type":"object"}},"type":"object"}}},"description":"Category list."}},"summary":"List all categories (full taxonomy).","tags":["categories"]}},"/clusters":{"get":{"parameters":[{"in":"query","name":"topic","schema":{"type":"string"}},{"description":"Only breaking clusters.","in":"query","name":"breaking","schema":{"type":"boolean"}},{"description":"Clusters one side is under-covering. 'left'/'right' = the side missing coverage; 'any' = any flagged cluster.","in":"query","name":"blindspot","schema":{"enum":["left","right","any"],"type":"string"}},{"description":"Minimum story count.","in":"query","name":"min_size","schema":{"minimum":1,"type":"integer"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterList"}}},"description":"Page of clusters."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"List story clusters.","tags":["clusters"]}},"/clusters/{cluster_id}":{"get":{"parameters":[{"in":"path","name":"cluster_id","required":true,"schema":{"type":"string"}},{"description":"Set to false to omit the member story list.","in":"query","name":"members","schema":{"default":true,"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Cluster"}},"type":"object"}}},"description":"Cluster detail."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Get a cluster (with members by default).","tags":["clusters"]}},"/docs":{"get":{"responses":{"200":{"description":"Standalone HTML page rendering this spec."}},"security":[],"summary":"Interactive Swagger UI.","tags":["meta"]}},"/health":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"service":{"example":"infomundi-api","type":"string"},"status":{"example":"ok","type":"string"},"time":{"format":"date-time","type":"string"},"version":{"example":"v1","type":"string"}},"type":"object"}}},"description":"Service is up."}},"security":[],"summary":"Liveness probe.","tags":["meta"]}},"/me":{"get":{"description":"Returns the client this key belongs to, the plan and its effective\nlimits, plus key metadata (scopes, last-used timestamp). Useful\nfor sanity-checking which environment a key is wired to and which\nscopes are actually granted.\n\nNo scope required \u2014 every key can read itself.\n","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}},"description":"Identity payload."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Introspect the authenticated client + key.","tags":["me"]}},"/me/dpa":{"get":{"description":"Returns the DPA / data-residency posture for this client:\nconfigured residency region, AI-processing toggle,\nno-training attestation, signed DPA reference, and the URLs\nfor our subprocessors + DPA legal pages.\nNo scope required \u2014 every key on the client can read this.\n","responses":{"200":{"description":"DPA attestation payload."},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Data Processing Agreement attestation.","tags":["me"]}},"/me/usage":{"get":{"description":"Returns a zero-filled time series with one row per UTC day. Default\nscope (`client`) sums across every key the client owns; `scope=key`\nrestricts to just the authenticated key.\n\nEach day includes `total`, `success`, `error_4xx`, `error_5xx`\ncounts so customers can chart error rates without polling\nper-endpoint.\n","parameters":[{"in":"query","name":"days","schema":{"default":30,"maximum":90,"minimum":1,"type":"integer"}},{"in":"query","name":"scope","schema":{"default":"client","enum":["client","key"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSeries"}}},"description":"Usage series."},"400":{"$ref":"#/components/responses/InvalidParameter"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Per-day usage rollup for the authenticated client/key.","tags":["me"]}},"/me/usage/by-endpoint":{"get":{"description":"Breaks the same window as `/me/usage` down by Flask route name and\nadds latency aggregates (mean + max). Lets a customer's monitoring\nanswer \"which endpoints am I leaning on, where are errors\nconcentrating, what's getting slow?\".\n\nTop-N sorted by `count` desc \u2014 pass `limit` to widen up to 200,\nor filter to a single key via `api_key=<public_id>`.\n\nNo scope required.\n","parameters":[{"in":"query","name":"days","schema":{"default":7,"maximum":90,"minimum":1,"type":"integer"}},{"in":"query","name":"limit","schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},{"description":"Restrict to one ApiKey's traffic.","in":"query","name":"api_key","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/UsageEndpointRow"},"type":"array"},"meta":{"properties":{"api_key":{"nullable":true,"type":"string"},"days":{"type":"integer"},"from":{"format":"date","type":"string"},"limit":{"type":"integer"},"row_count":{"type":"integer"},"to":{"format":"date","type":"string"}},"type":"object"}},"type":"object"}}},"description":"Top-N endpoints over the window."},"400":{"$ref":"#/components/responses/InvalidParameter"},"401":{"$ref":"#/components/responses/Unauthorized"}},"summary":"Per-endpoint usage rollup for the authenticated client.","tags":["me"]}},"/openapi.json":{"get":{"responses":{"200":{"description":"OpenAPI 3.1 document."}},"security":[],"summary":"This spec as JSON.","tags":["meta"]}},"/publishers":{"get":{"parameters":[{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"source_type","schema":{"enum":["rss","twitter","telegram"],"type":"string"}},{"in":"query","name":"active","schema":{"type":"boolean"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublisherList"}}},"description":"Page of publishers."}},"summary":"List publishers.","tags":["publishers"]}},"/search":{"get":{"parameters":[{"in":"query","name":"q","required":true,"schema":{"maxLength":200,"minLength":1,"type":"string"}},{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"topic","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"default":1,"minimum":1,"type":"integer"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResult"}}},"description":"Matching stories."},"400":{"$ref":"#/components/responses/InvalidParameter"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Search backend temporarily unavailable."}},"summary":"Full-text search over stories.","tags":["search"]}},"/stories":{"get":{"description":"Returns stories in reverse chronological order (newest first).\nUse cursor pagination via the `next` link.\n","parameters":[{"description":"Category name (e.g. br_general).","in":"query","name":"category","schema":{"type":"string"}},{"description":"Topic slug (e.g. politics, technology).","in":"query","name":"topic","schema":{"type":"string"}},{"description":"ISO 3166-1 alpha-2.","in":"query","name":"country","schema":{"maxLength":2,"minLength":2,"type":"string"}},{"description":"Lower bound (inclusive).","in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"description":"Upper bound (exclusive).","in":"query","name":"to","schema":{"format":"date-time","type":"string"}},{"description":"Restrict results to stories whose LLM-analysed sentiment matches the given label. Implies the story was clustered and analysed; un-analysed stories are excluded.","in":"query","name":"sentiment","schema":{"enum":["positive","neutral","negative"],"type":"string"}},{"description":"Inclusive lower bound on sentiment score (-1..1).","in":"query","name":"min_sentiment_score","schema":{"format":"float","maximum":1.0,"minimum":-1.0,"type":"number"}},{"description":"Inclusive upper bound on sentiment score (-1..1).","in":"query","name":"max_sentiment_score","schema":{"format":"float","maximum":1.0,"minimum":-1.0,"type":"number"}},{"description":"Comma-separated list (up to 10) of entity surface forms. Returns stories whose extracted ``entities`` array contains **any** of the named values (exact match on ``text``). Entities are extracted at ingest by the NER pipeline; un-analysed stories are excluded.","in":"query","name":"entity","schema":{"type":"string"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}},{"description":"Continuation token from a prior `next` link.","in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryList"}}},"description":"Page of stories."},"400":{"$ref":"#/components/responses/InvalidParameter"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"List stories.","tags":["stories"]}},"/stories/{story_id}":{"get":{"parameters":[{"in":"path","name":"story_id","required":true,"schema":{"description":"Hex MD5 of the source URL.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Story"}},"type":"object"}}},"description":"The story."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"Get a single story.","tags":["stories"]}},"/stories/{story_id}/image":{"post":{"description":"Force an immediate image fetch for a single story. Most stories are\nimaged automatically within minutes of ingest; use this for the long\ntail (fresh stories, niche publishers) when you need the image now.\n\nPulls the article's `og:image`, transcodes it, stores it, and returns\nthe ready URL. **Idempotent** \u2014 a story that's already imaged returns\ninstantly with `fetched: false` and no upstream work.\n\n**Synchronous**: the request blocks on the upstream fetch and transcode\n(a few seconds). Requires the `stories:read` scope. Read `image_url`\nback later with no work via `GET /stories/{story_id}`.\n","parameters":[{"in":"path","name":"story_id","required":true,"schema":{"description":"Hex MD5 of the source URL.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"properties":{"fetched":{"description":"True if this call fetched the image; false if it already existed.","type":"boolean"},"id":{"description":"Story public id.","type":"string"},"image_url":{"description":"Stored image URL, or null when unavailable.","nullable":true,"type":"string"},"status":{"enum":["ready","unavailable"],"type":"string"}},"type":"object"}},"type":"object"}}},"description":"The image fetch completed. `data.status` is `ready` when an image is available (newly fetched or pre-existing) or `unavailable` when the source has no usable image."},"400":{"$ref":"#/components/responses/InvalidParameter"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"summary":"Fetch a story's image on demand.","tags":["stories"]}},"/trackers":{"get":{"description":"Returns every tracker the authenticated client owns. Trackers\nare saved monitoring rules \u2014 a free-text Meilisearch query paired\nwith optional country/region/category/topic/publisher filters.\n\nRequired scope: `trackers:read`.\n","parameters":[{"description":"Restrict to active (`true`) or paused (`false`) trackers.","in":"query","name":"active","schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Tracker"},"type":"array"},"meta":{"properties":{"count":{"type":"integer"},"max_trackers":{"description":"Effective cap for this client/plan.","type":"integer"}},"type":"object"}},"type":"object"}}},"description":"Tracker list."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"summary":"List your saved trackers.","tags":["trackers"]},"post":{"description":"Create a saved monitoring rule. The `query` field is passed\nverbatim to Meilisearch, so you can use quoted phrases and\nboolean operators \u2014 e.g. `\"Acme Corp\" OR \"AcmeCo\"`.\n\nFilters narrow the universe before lexical match. Combine\ncountries with regions (region names expand to their member\ncountry list) for broad geographic scoping, then narrow further\nwith categories/topics/publishers/source_types as needed.\n\nWith `notify_webhook: true`, every new match fans out as a\n`tracker.matched` webhook event to subscribers \u2014 set up at\nleast one subscription via `POST /webhooks` to receive them.\n\nRequired scope: `trackers:manage`.\n","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackerCreate"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Tracker"}},"type":"object"}}},"description":"Tracker created."},"400":{"$ref":"#/components/responses/InvalidParameter"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Tracker cap for this plan reached."}},"summary":"Create a new tracker.","tags":["trackers"]}},"/trackers/preview":{"post":{"description":"Counts stories matching the supplied `query` + `filters` over the\ntrailing `days` window and returns a small sample. Lets customers\niterate on a query before committing \u2014 handy for the\ncreate/edit form, or for programmatic query tuning.\n\nRequired scope: `trackers:read` (every paid plan plus the\ndeveloper tier \u2014 query iteration shouldn't gate behind the\n`trackers:manage` upgrade).\n","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackerPreviewRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TrackerPreviewResult"}},"type":"object"}}},"description":"Preview result."},"400":{"$ref":"#/components/responses/InvalidParameter"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Search backend temporarily unavailable. Safe to retry."}},"summary":"Dry-run a tracker query without saving anything.","tags":["trackers"]}},"/trackers/{tracker_id}":{"delete":{"description":"Cascades to the match log (`api_tracker_matches`), so previously\ndelivered webhooks are not affected but historical mentions are\nno longer queryable via `/trackers/{id}/stats`.\n\nRequired scope: `trackers:manage`.\n","parameters":[{"in":"path","name":"tracker_id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Deleted (or never existed for this client)."}},"summary":"Delete a tracker. Idempotent.","tags":["trackers"]},"get":{"parameters":[{"in":"path","name":"tracker_id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Tracker"}},"type":"object"}}},"description":"Tracker config."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Fetch one tracker's configuration.","tags":["trackers"]},"patch":{"description":"All fields optional; supply only the ones you want to change.\nMutating `query` or `filters` does NOT retro-actively re-scan\nhistory \u2014 the watermark stays put, so only future stories are\nevaluated against the new rule.\n\nRequired scope: `trackers:manage`.\n","parameters":[{"in":"path","name":"tracker_id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrackerUpdate"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Tracker"}},"type":"object"}}},"description":"Updated tracker."},"400":{"$ref":"#/components/responses/InvalidParameter"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Mutate a tracker in place.","tags":["trackers"]}},"/trackers/{tracker_id}/mentions":{"get":{"description":"Live Meilisearch query \u2014 results include freshly ingested stories\neven before the background match worker has logged them. Use\n`/stats` for aggregated history out of our local log.\n\nRequired scope: `trackers:read`.\n","parameters":[{"in":"path","name":"tracker_id","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"to","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"page","schema":{"default":1,"minimum":1,"type":"integer"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResult"}}},"description":"Page of matching stories."},"404":{"$ref":"#/components/responses/NotFound"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Search backend temporarily unavailable. Safe to retry."}},"summary":"List stories currently matching the tracker.","tags":["trackers"]}},"/trackers/{tracker_id}/stats":{"get":{"description":"Returns a per-day volume series plus top-N breakdowns by country,\ntopic, and publisher. Reads from our local match log \u2014 the same\nlog that drives webhook fanout \u2014 so the numbers stay in sync with\nwhat we already pushed to your endpoints.\n\nRequired scope: `trackers:read`.\n","parameters":[{"in":"path","name":"tracker_id","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"days","schema":{"default":7,"maximum":90,"minimum":1,"type":"integer"}},{"in":"query","name":"from","schema":{"format":"date-time","type":"string"}},{"in":"query","name":"to","schema":{"format":"date-time","type":"string"}},{"description":"Bucket size for the `series` field. `hour` requires the\nwindow to be 7 days or fewer; `week` requires at least\n14 days.\n","in":"query","name":"granularity","schema":{"default":"day","enum":["hour","day","week"],"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/TrackerStats"},"meta":{"properties":{"entity_aggregation_cap":{"type":"integer"},"entity_aggregation_capped":{"type":"boolean"},"granularity":{"enum":["hour","day","week"],"type":"string"},"tracker_id":{"format":"uuid","type":"string"}},"type":"object"}},"type":"object"}}},"description":"Aggregated stats."},"400":{"$ref":"#/components/responses/InvalidParameter"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Aggregate volume + breakdowns for a tracker.","tags":["trackers"]}},"/webhooks":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Webhook"},"type":"array"}},"type":"object"}}},"description":"List."}},"summary":"List your webhook subscriptions.","tags":["webhooks"]},"post":{"description":"Choose a `platform`:\n\n* `generic` (default) \u2014 we POST a signed JSON envelope to\n  your endpoint and you verify the `X-Infomundi-Signature`\n  header. The response includes `signing_secret` exactly once.\n  Store it now \u2014 there is no way to retrieve it later. Rotate\n  by deleting and recreating the subscription.\n* `slack` \u2014 register a Slack incoming-webhook URL\n  (`https://hooks.slack.com/services/...`). Events arrive as\n  native Slack messages. No signing secret is issued; the URL\n  itself is the credential.\n* `discord` \u2014 register a Discord webhook URL\n  (`https://discord.com/api/webhooks/...`). Events arrive as\n  Discord rich embeds. No signing secret.\n* `teams` \u2014 register a Microsoft Teams incoming-webhook URL\n  (`*.webhook.office.com`). Events arrive as MessageCards.\n  No signing secret.\n","requestBody":{"content":{"application/json":{"schema":{"properties":{"description":{"maxLength":255,"type":"string"},"event_types":{"items":{"enum":["breaking_news.started","breaking_news.ended","story.published","cluster.created","cluster.updated","cluster.summary_ready","cluster.blindspot","tracker.matched","tracker.volume_alert"],"type":"string"},"type":"array"},"platform":{"default":"generic","description":"Delivery flavour. `generic` posts a signed envelope; the\nothers post native chat-platform JSON.\n","enum":["generic","slack","discord","teams"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"properties":{"signing_secret":{"description":"HMAC-SHA256 secret. Shown ONCE. Present only\nwhen `platform=generic`.\n","nullable":true,"type":"string"}},"type":"object"}]}},"type":"object"}}},"description":"Created. For `platform=generic`, the body includes a one-time\n`signing_secret`. For chat platforms, no signing secret is\nreturned (the URL is the credential).\n"}},"summary":"Create a webhook subscription.","tags":["webhooks"]}},"/webhooks/{subscription_id}":{"delete":{"parameters":[{"in":"path","name":"subscription_id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Deleted (or never existed for this client)."}},"summary":"Delete a webhook subscription. Idempotent.","tags":["webhooks"]},"patch":{"description":"Toggle `is_active`, replace the `event_types` filter, or set/clear\n`description`. URL and signing secret are immutable \u2014 rotate by\ndeleting and recreating. Re-activating clears the consecutive-\nfailures auto-disable counter.\n","parameters":[{"in":"path","name":"subscription_id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"description":{"maxLength":255,"nullable":true,"type":"string"},"event_types":{"items":{"enum":["breaking_news.started","breaking_news.ended","story.published","cluster.created","cluster.updated","cluster.summary_ready","cluster.blindspot","tracker.matched","tracker.volume_alert"],"type":"string"},"type":"array"},"is_active":{"type":"boolean"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"$ref":"#/components/schemas/Webhook"}},"type":"object"}}},"description":"Updated subscription (without the signing secret)."},"400":{"$ref":"#/components/responses/InvalidParameter"},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"Update a webhook subscription in place.","tags":["webhooks"]}},"/webhooks/{subscription_id}/deliveries":{"get":{"description":"Debug \"why didn't I get the event?\" without filing support. Each\nrow shows status, attempt count, last HTTP code, last error, and\nthe next scheduled retry time. Returns newest first; cursor\npagination.\n","parameters":[{"in":"path","name":"subscription_id","required":true,"schema":{"format":"uuid","type":"string"}},{"in":"query","name":"status","schema":{"enum":["pending","succeeded","failed","dead"],"type":"string"}},{"in":"query","name":"page_size","schema":{"default":25,"maximum":500,"minimum":1,"type":"integer"}},{"in":"query","name":"cursor","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Delivery"},"type":"array"},"links":{"$ref":"#/components/schemas/Links"},"meta":{"$ref":"#/components/schemas/Meta"}},"type":"object"}}},"description":"Page of deliveries."},"404":{"$ref":"#/components/responses/NotFound"}},"summary":"List recent delivery attempts for a subscription.","tags":["webhooks"]}}},"security":[{"ApiKey":[]}],"servers":[{"description":"Production","url":"https://infomundi.net/api/v1"},{"description":"Development","url":"https://dev.infomundi.net/api/v1"}],"tags":[{"description":"Individual news stories sourced from publishers.","name":"stories"},{"description":"Groups of stories covering the same event from different sources/countries.","name":"clusters"},{"description":"Real-time breaking-news clusters.","name":"breaking"},{"description":"Directory of publishers (RSS + social sources).","name":"publishers"},{"description":"Category taxonomy.","name":"categories"},{"description":"Full-text search over stories.","name":"search"},{"description":"Subscribe to push notifications for live events.","name":"webhooks"},{"description":"Brand, competitor, and entity monitoring. Save a named query +\nfilter set, then poll for matching mentions or receive push\nnotifications via the `tracker.matched` webhook event.\n","name":"trackers"},{"description":"Self-introspection \u2014 your client, plan, key, and usage.","name":"me"},{"description":"Enterprise audit-log pull (`GET /audit`) and SIEM push\nconfiguration (S3 / HTTPS JSONL drops). Reading requires\n`audit:read`; creating, updating, deleting, or rotating an\nexport config requires `audit:manage`.\n","name":"audit"},{"description":"Health, spec, and documentation.","name":"meta"}]}
