{
  "openapi": "3.1.0",
  "info": {
    "title": "Asha News Agent API",
    "version": "1.9.0",
    "description": "Read-focused endpoints for AI agents and digest consumers, plus the two no-key actions an agent can take on a reader's behalf: minting a share link and starting a double-opt-in newsletter signup. Human-readable source transparency lives at https://asha.news/sources.",
    "x-legacy-deprecations-doc": "/API_DEPRECATIONS.md"
  },
  "externalDocs": {
    "description": "Human-readable Asha News source directory",
    "url": "https://asha.news/sources"
  },
  "servers": [
    {
      "url": "https://asha.news/api/v1",
      "description": "Production absolute base. Use this when consuming the spec on its own; clients that only read servers need an absolute URL to build requests."
    },
    {
      "url": "/api/v1",
      "description": "Same-origin relative base for preview/staging hosts and same-origin browsers."
    }
  ],
  "tags": [
    {
      "name": "Sources",
      "description": "Source registry and source-health endpoints for agent citation context.",
      "externalDocs": {
        "description": "Asha News source directory",
        "url": "https://asha.news/sources"
      }
    },
    {
      "name": "Coverage",
      "description": "Evidence-first coverage gap and blindspot surfaces backed by persisted posture analysis."
    },
    {
      "name": "Conflict Monitor",
      "description": "Live Middle East conflict monitor: official alerts, geolocated OSINT events, active clusters, EN/AR theater brief, and per-tier freshness. Live push is SSE at /api/conflict/stream (public namespace /api/conflict, not /api/v1)."
    },
    {
      "name": "Palestine",
      "description": "Palestine archive: live upstream metadata mirror, memorial, claim library, agentic claim checker, and hasbara trend scan. Metadata only; no media hosted."
    },
    {
      "name": "Agent Actions",
      "description": "No-key actions an agent can take for a reader: mint a stable share link, or start a double-opt-in newsletter signup. Neither needs an API key; the newsletter path never activates a subscription without the address owner clicking the emailed link."
    },
    {
      "name": "Fact Check",
      "description": "Public fact-check surfaces under /api/fact-check (claim + social-post checking)."
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "AgentKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Agent API key (asha_v1_...) issued by POST /agents/signup. Sent as Authorization: Bearer or X-Asha-Agent-Key."
      }
    },
    "schemas": {
      "ReadinessResponse": {
        "type": "object",
        "required": [
          "ok",
          "product",
          "archive",
          "release"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True only when schema, worker, scheduler, ingestion, R2, and build identity checks pass."
          },
          "product": {
            "type": "string",
            "const": "asha.news"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "release": {
            "type": "object",
            "properties": {
              "git_sha": {
                "type": "string"
              },
              "required_schema": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "applied_schema": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "schema_compatible": {
                "type": "boolean"
              }
            }
          },
          "archive": {
            "type": "object",
            "properties": {
              "ok": {
                "type": "boolean"
              },
              "mode": {
                "type": "string"
              },
              "missing_tables": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "runtime": {
                "type": "object",
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "worker": {
                    "$ref": "#/components/schemas/RuntimeServiceHealth"
                  },
                  "scheduler": {
                    "$ref": "#/components/schemas/RuntimeServiceHealth"
                  },
                  "ingestion": {
                    "type": "object",
                    "properties": {
                      "ok": {
                        "type": "boolean"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "fresh",
                          "stale",
                          "missing",
                          "future_clock_skew"
                        ]
                      },
                      "age_ms": {
                        "type": [
                          "number",
                          "null"
                        ]
                      },
                      "last_raw_item_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  },
                  "r2": {
                    "type": "object",
                    "properties": {
                      "ok": {
                        "type": "boolean"
                      },
                      "required": {
                        "type": "boolean"
                      },
                      "configured": {
                        "type": "boolean"
                      },
                      "missing": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "RuntimeServiceHealth": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "age_ms": {
            "type": [
              "number",
              "null"
            ]
          },
          "max_age_ms": {
            "type": "number"
          },
          "build_match": {
            "type": "boolean"
          },
          "git_sha": {
            "type": "string"
          },
          "instance_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Cluster": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "number"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "topic": {
            "type": "string"
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sanitized lead image for the cluster, when a usable one exists: the newest member article's og/thumbnail image, derived at read time."
          },
          "article_count": {
            "type": "integer"
          },
          "source_count": {
            "type": "integer"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "source_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "story_cluster",
              "generated_brief",
              "article_backed_cluster",
              null
            ],
            "description": "Where this search result came from."
          },
          "relevance_score": {
            "type": [
              "number",
              "null"
            ],
            "description": "In-process relevance score for the query. Higher is more relevant."
          },
          "asha_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "freshness_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "fresh",
              "updated",
              "stale",
              "historical",
              "ambiguous",
              null
            ]
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "InstrumentNewsItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "number"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "world",
              "politics",
              "markets",
              "tech",
              "ai",
              "science",
              "health",
              "sport",
              "entertainment",
              null
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "bias": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "InstrumentPrice": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string"
          },
          "quote_symbol": {
            "type": "string"
          },
          "price": {
            "type": [
              "number",
              "null"
            ]
          },
          "change": {
            "type": [
              "number",
              "null"
            ]
          },
          "change_pct": {
            "type": [
              "number",
              "null"
            ]
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "market_state": {
            "type": [
              "string",
              "null"
            ]
          },
          "as_of": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SourceDirectoryEntry": {
        "type": "object",
        "required": [
          "id",
          "name",
          "pipeline",
          "kind",
          "status",
          "directory_url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable public source-directory anchor id."
          },
          "name": {
            "type": "string"
          },
          "pipeline": {
            "type": "string",
            "examples": [
              "Asha monitoring network",
              "Asha RSS registry"
            ]
          },
          "kind": {
            "type": "string",
            "examples": [
              "Digest source",
              "Source lane",
              "RSS feed"
            ]
          },
          "status": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "language_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "scope": {
            "type": [
              "string",
              "null"
            ]
          },
          "posture": {
            "type": [
              "string",
              "null"
            ]
          },
          "trust": {
            "type": [
              "string",
              "null"
            ]
          },
          "use": {
            "type": [
              "string",
              "null"
            ]
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "feed_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "item_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "priority": {
            "type": [
              "string",
              "null"
            ]
          },
          "bias": {
            "type": [
              "string",
              "null"
            ]
          },
          "credibility": {
            "type": [
              "integer",
              "null"
            ]
          },
          "directory_url": {
            "type": "string",
            "format": "uri",
            "description": "Human-readable anchor for this source row."
          }
        }
      },
      "SourceDirectoryCatalog": {
        "type": "object",
        "required": [
          "type",
          "human_url",
          "total_entries",
          "monitoring_resources",
          "source_snapshot"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "asha_public_source_directory"
          },
          "human_url": {
            "type": "string",
            "format": "uri"
          },
          "source_directory_updated": {
            "type": "string",
            "format": "date"
          },
          "total_entries": {
            "type": "integer"
          },
          "categories": {
            "type": "integer"
          },
          "languages": {
            "type": "integer"
          },
          "regions": {
            "type": "integer"
          },
          "asha_visible_entries": {
            "type": "integer"
          },
          "monitoring_resources": {
            "type": "object",
            "properties": {
              "rss_feeds": {
                "type": "string",
                "examples": [
                  "435+"
                ]
              },
              "languages": {
                "type": "integer"
              },
              "map_layers": {
                "type": "integer"
              },
              "early_signal_channels": {
                "type": "integer"
              },
              "allowed_proxy_domains": {
                "type": "integer"
              }
            }
          },
          "source_snapshot": {
            "type": "object",
            "properties": {
              "active_named_sources": {
                "type": "integer"
              },
              "feed_status_entries": {
                "type": "integer"
              },
              "digest_categories": {
                "type": "integer"
              },
              "digest_items": {
                "type": "integer"
              },
              "verified_at": {
                "type": "string"
              }
            }
          }
        }
      },
      "SourceDirectoryResponse": {
        "type": "object",
        "required": [
          "generated_at",
          "source_directory_updated",
          "total",
          "count",
          "catalog",
          "results"
        ],
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "source_directory_updated": {
            "type": "string",
            "format": "date"
          },
          "total": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "fallback": {
            "type": [
              "object",
              "null"
            ]
          },
          "catalog": {
            "$ref": "#/components/schemas/SourceDirectoryCatalog"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceDirectoryEntry"
            }
          }
        }
      },
      "SourceHealthRecord": {
        "type": "object",
        "properties": {
          "source_id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "source_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "health_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "healthy",
              "degraded",
              "failed",
              "unknown"
            ]
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_failure_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "consecutive_failures": {
            "type": [
              "integer",
              "null"
            ]
          },
          "last_latency_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ]
          },
          "error_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "fallback_behavior": {
            "type": "string",
            "enum": [
              "normal_ingest_priority",
              "kept_in_registry_lower_priority_until_next_success",
              "kept_in_registry_routed_after_healthy_sources",
              "kept_in_registry_until_telemetry_available",
              "derived_from_archive"
            ]
          },
          "telemetry_status": {
            "type": "string",
            "enum": [
              "current",
              "stale",
              "unknown"
            ]
          },
          "telemetry_age_hours": {
            "type": [
              "number",
              "null"
            ]
          },
          "telemetry_stale": {
            "type": "boolean"
          },
          "operator_action": {
            "type": "string",
            "enum": [
              "no_action_required",
              "run_source_health_job_or_migration",
              "refresh_stale_telemetry",
              "inspect_error_and_retry_after_healthy_sources",
              "lower_priority_and_retry_until_next_success",
              "keep_visible_and_collect_first_telemetry"
            ]
          },
          "fetched_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "duplicate_rate": {
            "type": [
              "number",
              "null"
            ]
          },
          "stale_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "stale": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "derived": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      },
      "SourceHealthResponse": {
        "type": "object",
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "count": {
            "type": "integer"
          },
          "summary": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "healthy": {
                "type": "integer"
              },
              "degraded": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "unknown": {
                "type": "integer"
              },
              "derived": {
                "type": "integer"
              },
              "telemetry_current": {
                "type": "integer"
              },
              "telemetry_stale": {
                "type": "integer"
              },
              "telemetry_unknown": {
                "type": "integer"
              }
            }
          },
          "maintenance": {
            "$ref": "#/components/schemas/SourceHealthMaintenance"
          },
          "fallback": {
            "type": [
              "object",
              "null"
            ]
          },
          "warning": {
            "type": [
              "string",
              "null"
            ]
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SourceHealthRecord"
            }
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "description": "One unified search result across archived articles, active story clusters, published generated briefs, or article-backed fallback clusters.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "article",
              "story_cluster",
              "generated_brief",
              "article_backed_cluster"
            ]
          },
          "id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "asha_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "short_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "short_link_status_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "topic": {
            "type": [
              "string",
              "null"
            ]
          },
          "article_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "source_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "freshness_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "fresh",
              "updated",
              "stale",
              "historical",
              "ambiguous",
              null
            ]
          },
          "source_published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "endpoint": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "share_pack_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "relevance_score": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "query": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchResult"
            }
          },
          "groups": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/SearchResult"
              }
            }
          }
        }
      },
      "ArticleFreshness": {
        "type": "object",
        "description": "Separates publisher/source time from Asha ingest/check time. Agents should not treat Asha first-seen or last-checked timestamps as publisher freshness.",
        "properties": {
          "source_published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "source_updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "asha_first_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "asha_last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "freshness_status": {
            "type": "string",
            "enum": [
              "fresh",
              "updated",
              "stale",
              "historical",
              "ambiguous"
            ]
          }
        }
      },
      "Article": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "number"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "canonical_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "world",
              "politics",
              "markets",
              "tech",
              "ai",
              "science",
              "health",
              "sport",
              "entertainment",
              null
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Publisher/source publication timestamp. Null means the source publication time is unknown; use asha_first_seen_at only as Asha archive context."
          },
          "source_published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "source_updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "asha_first_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "asha_last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "freshness_status": {
            "type": "string",
            "enum": [
              "fresh",
              "updated",
              "stale",
              "historical",
              "ambiguous"
            ]
          },
          "freshness": {
            "$ref": "#/components/schemas/ArticleFreshness"
          },
          "breaking_news": {
            "type": "boolean",
            "description": "True only when the source item is both flagged as breaking and source-published inside the fresh window."
          },
          "posture_label": {
            "type": [
              "string",
              "null"
            ]
          },
          "hasbara_risk": {
            "type": [
              "number",
              "null"
            ]
          },
          "geographic_bias": {
            "type": [
              "number",
              "null"
            ]
          },
          "bias": {
            "type": [
              "string",
              "null"
            ]
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "archive": {
            "type": "object"
          },
          "language": {
            "type": [
              "string",
              "null"
            ],
            "description": "Article language code ('en', 'ar', ...) resolved at ingest; null for rows persisted before language tracking. Becomes 'ar' when the item text was served from a validated Arabic translation (see translated)."
          },
          "translated": {
            "type": "boolean",
            "description": "Present and true only when title/summary were replaced from a cached, language-validated Arabic translation (?lang=ar). Absent means the item carries its original-language text."
          },
          "translation_scope": {
            "type": "string",
            "enum": [
              "lite",
              "full"
            ],
            "description": "Present only when translated is true. 'lite': title/summary are translated but any body text stays in the original language (feed items carry no body, so they are always 'lite'). 'full': the served body is translated too."
          },
          "original_title": {
            "type": "string",
            "description": "The original (pre-translation) title; present only when translated is true."
          }
        }
      },
      "SharePackCitation": {
        "type": "object",
        "description": "One citation from a published Asha brief. A missing URL means the stored citation did not include a public source URL; clients must not invent one.",
        "properties": {
          "index": {
            "type": "integer"
          },
          "article_id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "source_id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "source": {
            "type": "string"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SharePack": {
        "type": "object",
        "description": "Share-ready package for a published Asha brief/story. found=false means no final published Asha share pack is available.",
        "properties": {
          "found": {
            "type": "boolean"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "matched_by": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "id_or_slug",
              "search",
              "latest",
              "story_search",
              "story_id",
              null
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "story_id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "asha_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "short_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "A stored or generated routable Asha short URL. Generated share-pack keys use /s/a-{slug} for published cited briefs and /s/st-{story_id} for active cited stories."
          },
          "one_liner": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary_short": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary_long": {
            "type": [
              "string",
              "null"
            ]
          },
          "why_it_matters": {
            "type": [
              "string",
              "null"
            ]
          },
          "key_points": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "citations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SharePackCitation"
            }
          },
          "source_urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          },
          "freshness_status": {
            "type": "string",
            "enum": [
              "fresh",
              "updated",
              "stale",
              "historical",
              "ambiguous"
            ]
          },
          "freshness": {
            "$ref": "#/components/schemas/ArticleFreshness"
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "publish_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "review_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "social_posts": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "x": {
                "type": "string"
              },
              "threads": {
                "type": "string"
              },
              "linkedin": {
                "type": "string"
              },
              "telegram": {
                "type": "string"
              }
            }
          },
          "fallback": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      },
      "BlindspotCluster": {
        "type": "object",
        "description": "A persisted coverage gap from story_clusters posture analysis. This schema exposes stored fields only; clients must not infer blindspots when flagged is false or status is unavailable.",
        "properties": {
          "id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "topic": {
            "type": "string"
          },
          "article_count": {
            "type": "integer"
          },
          "source_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "asha_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "flagged": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "mci": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Manufactured Consensus Index when persisted by the posture pipeline."
          },
          "missing_frame": {
            "type": [
              "string",
              "null"
            ]
          },
          "headline": {
            "type": [
              "string",
              "null"
            ]
          },
          "dominant_omissions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "counter_sources": {
            "type": "array",
            "items": {}
          },
          "evidence": {
            "type": [
              "object",
              "null"
            ]
          },
          "posture_distribution": {
            "type": [
              "object",
              "null"
            ]
          },
          "consensus_blindspot": {
            "type": [
              "object",
              "null"
            ]
          },
          "coverage_gaps": {
            "type": [
              "object",
              "array",
              "null"
            ]
          },
          "bias_distribution": {
            "type": [
              "object",
              "null"
            ]
          },
          "source_type": {
            "type": "string",
            "enum": [
              "consensus_blindspot",
              "legacy_coverage_gap"
            ]
          }
        }
      },
      "BlindspotResponse": {
        "type": "object",
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "no_flagged_clusters",
              "unavailable"
            ]
          },
          "methodology": {
            "type": "string",
            "const": "manufactured_consensus_index"
          },
          "description": {
            "type": "string"
          },
          "filters": {
            "type": "object"
          },
          "count": {
            "type": "integer"
          },
          "warning": {
            "type": [
              "string",
              "null"
            ]
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlindspotCluster"
            }
          }
        }
      },
      "SourceHealthMaintenance": {
        "type": "object",
        "properties": {
          "owner": {
            "type": "string"
          },
          "loop": {
            "type": "string"
          },
          "expected_interval_minutes": {
            "type": "integer"
          },
          "stale_after_minutes": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "needs_attention"
            ]
          },
          "fallback_active": {
            "type": "boolean"
          },
          "actions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ShortLinkStatus": {
        "type": "object",
        "description": "JSON status for a stored or generated Asha short-link key. found=false means the key is invalid, absent, non-final, or lacks required source evidence.",
        "properties": {
          "found": {
            "type": "boolean"
          },
          "key": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "short_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "target_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "generated_article",
              "story_cluster",
              null
            ]
          },
          "target_id": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "route": {
            "type": [
              "string",
              "null"
            ]
          },
          "asha_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "generated_share_pack_key",
              "stored_short_url",
              null
            ]
          },
          "publish_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "review_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "evidence": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "has_evidence": {
                "type": "boolean"
              },
              "citation_count": {
                "type": "integer"
              },
              "source_article_count": {
                "type": "integer"
              },
              "article_count": {
                "type": "integer"
              },
              "source_count": {
                "type": "integer"
              }
            }
          }
        }
      },
      "GeneratedArticleSubmission": {
        "type": "object",
        "required": [
          "title",
          "content",
          "citations",
          "source_article_ids",
          "model_provider",
          "model_name",
          "prompt_version",
          "confidence"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 8,
            "maxLength": 500
          },
          "slug": {
            "type": "string",
            "maxLength": 180,
            "description": "Optional; derived from the date and title when omitted."
          },
          "summary": {
            "type": "string",
            "maxLength": 4000
          },
          "content": {
            "type": "string",
            "minLength": 200,
            "maxLength": 80000
          },
          "category": {
            "type": "string",
            "maxLength": 120
          },
          "keywords": {
            "type": "array",
            "maxItems": 30,
            "description": "Specific search phrases, lowercased and deduplicated on write. Use natural topic terms; keyword stuffing does not improve publication eligibility.",
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "tags": {
            "type": "array",
            "maxItems": 30,
            "description": "Topical labels used for internal retrieval and published article metadata.",
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "citations": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "properties": {
                "article_id": {
                  "type": "string"
                },
                "source": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Public https evidence URL. At least one citation must resolve to one."
                },
                "published_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "source_article_ids": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "model_provider": {
            "type": "string",
            "maxLength": 120
          },
          "model_name": {
            "type": "string",
            "maxLength": 120
          },
          "prompt_version": {
            "type": "string",
            "maxLength": 120
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "quality_gate_result": {
            "type": "object",
            "description": "Optional self-reported gate metadata; Asha re-runs its own gate regardless. Reserved keys are silently dropped: revision machinery (validation, agent_submission, agent_revisions, agent_revision_seq) and editor-owned verification state (required_checks, min_citations, min_sources, editorial_reverification, editorial_edit_invalidated_at)."
          },
          "idempotency_key": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "pattern": "^[\\x20-\\x7E]{1,200}$",
            "description": "Body alternative to the Idempotency-Key header (the header wins when both are set)."
          }
        }
      },
      "GeneratedArticleRevision": {
        "type": "object",
        "description": "Partial revision of an agent's own still-pending submission. Every field is optional — omitted fields keep their stored values; supplied fields must carry these types or the request answers 422 invalid_field_type. Slug is immutable and must not be sent (422 slug_immutable). The merged document is re-validated by the same gate as POST /generated.",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 8,
            "maxLength": 500
          },
          "summary": {
            "type": "string",
            "maxLength": 4000
          },
          "content": {
            "type": "string",
            "minLength": 200,
            "maxLength": 80000
          },
          "category": {
            "type": "string",
            "maxLength": 120
          },
          "keywords": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "tags": {
            "type": "array",
            "maxItems": 30,
            "items": {
              "type": "string",
              "maxLength": 80
            }
          },
          "citations": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "properties": {
                "article_id": {
                  "type": "string"
                },
                "source": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Public https evidence URL. At least one citation must resolve to one."
                },
                "published_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "source_article_ids": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "model_provider": {
            "type": "string",
            "maxLength": 120
          },
          "model_name": {
            "type": "string",
            "maxLength": 120
          },
          "prompt_version": {
            "type": "string",
            "maxLength": 120
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "quality_gate_result": {
            "type": "object",
            "description": "Optional self-reported gate metadata, merged over the stored gate. Reserved keys are silently dropped: the route-owned revision machinery (validation, agent_submission, agent_revisions, agent_revision_seq) and the editor-owned verification state (required_checks, min_citations, min_sources, editorial_reverification, editorial_edit_invalidated_at)."
          }
        }
      },
      "CoverageMatch": {
        "type": "object",
        "properties": {
          "asha_url": {
            "type": "string",
            "format": "uri",
            "description": "Live public Asha article URL for this published brief."
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "match_type": {
            "type": "string",
            "enum": [
              "canonical_source_url",
              "source_article_id",
              "title_similarity",
              "topic_search"
            ]
          },
          "confidence": {
            "type": "string",
            "enum": [
              "exact",
              "strong",
              "weak"
            ]
          },
          "citations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "source": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CoverageResponse": {
        "type": "object",
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "query": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "nullable": true
              },
              "q": {
                "type": "string",
                "nullable": true
              },
              "since": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "until": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "covered": {
            "type": "boolean",
            "description": "True only when the best match is exact or strong. Weak matches never set covered."
          },
          "match_type": {
            "type": "string",
            "enum": [
              "canonical_source_url",
              "source_article_id",
              "title_similarity",
              "topic_search"
            ],
            "nullable": true,
            "description": "Best match's type, or null when nothing matched."
          },
          "confidence": {
            "type": "string",
            "enum": [
              "exact",
              "strong",
              "weak"
            ],
            "nullable": true
          },
          "asha_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Live Asha URL to link when covered=true; null otherwise, so a weak guess can never be auto-posted."
          },
          "short_url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CoverageMatch"
            }
          },
          "scan": {
            "type": "object",
            "description": "How exhaustive this answer is. The strong tiers are index-backed lookups over the whole published archive; the weak text tiers read a bounded recency window.",
            "properties": {
              "published_scanned": {
                "type": "integer",
                "description": "Published rows read by the recency scan that feeds the weak text tiers."
              },
              "scan_limit": {
                "type": "integer",
                "description": "Bound on that scan. published_scanned reaching it means the scan was truncated."
              },
              "indexed_lookup": {
                "type": "boolean",
                "description": "True when the index-backed strong tiers ran and saw every published row they could match."
              },
              "complete": {
                "type": "boolean",
                "description": "True when nothing relevant could have been missed: either the recency scan was not truncated, or the query is URL-only (no q, so no weak tier applies) and indexed_lookup is true."
              }
            }
          },
          "recommendation": {
            "type": "string",
            "enum": [
              "reuse_existing",
              "create_new",
              "needs_human_review"
            ],
            "description": "reuse_existing only on an exact/strong match; weak or ambiguous matches yield needs_human_review; no match yields create_new ONLY when scan.complete is true. An inconclusive look returns needs_human_review instead, because 'create a new article' asserts that nothing exists and a truncated look cannot support that."
          }
        }
      },
      "ShareLinkRequest": {
        "type": "object",
        "required": [
          "target_type",
          "target_id"
        ],
        "properties": {
          "target_type": {
            "type": "string",
            "enum": [
              "article",
              "generated",
              "cluster",
              "route"
            ],
            "description": "What is being shared."
          },
          "target_id": {
            "type": "string",
            "description": "Article id, generated-brief id or slug, cluster id, or an allowlisted site path when target_type=route."
          }
        }
      },
      "ShareLink": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "The short code, stable per target."
          },
          "short_url": {
            "type": "string",
            "format": "uri",
            "description": "https://asha.news/s/{code}"
          },
          "route": {
            "type": "string",
            "description": "The canonical Asha route the code redirects to."
          }
        }
      },
      "NewsletterSignupRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The subscriber's own address. Confirmation is emailed here."
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "daily",
                "breaking",
                "sunday"
              ]
            },
            "default": [
              "daily"
            ],
            "description": "Which sends to request."
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "ar"
            ],
            "default": "en",
            "description": "Issue language."
          },
          "source": {
            "type": "string",
            "description": "Optional caller label; stored as agent:{label} for provenance."
          }
        }
      },
      "NewsletterSignupPending": {
        "type": "object",
        "properties": {
          "subscribed": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always false: nothing is active until the emailed link is clicked."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_confirmation"
            ]
          },
          "confirmation_required": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "message": {
            "type": "string"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/openapi": {
      "get": {
        "summary": "Get this OpenAPI document",
        "responses": {
          "200": {
            "description": "OpenAPI document"
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Get API health",
        "responses": {
          "200": {
            "description": "Health payload"
          }
        }
      }
    },
    "/readiness": {
      "get": {
        "summary": "Get release, schema, worker, scheduler, ingestion, and asset-storage readiness",
        "responses": {
          "200": {
            "description": "The exact release and all required runtime dependencies are ready",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessResponse"
                }
              }
            }
          },
          "503": {
            "description": "At least one schema, runtime, build, ingestion, or R2 dependency is not ready",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessResponse"
                }
              }
            }
          }
        }
      }
    },
    "/archive/status": {
      "get": {
        "summary": "Get archive status and counts",
        "responses": {
          "200": {
            "description": "Archive status payload"
          }
        }
      }
    },
    "/feed": {
      "get": {
        "summary": "Get archive-backed public feed",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "world",
                "politics",
                "markets",
                "tech",
                "ai",
                "science",
                "health",
                "sport",
                "entertainment"
              ]
            }
          },
          {
            "name": "vertical",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "world",
                "politics",
                "markets",
                "tech",
                "ai",
                "science",
                "health",
                "sport",
                "entertainment"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "timeframe",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "6h",
                "24h",
                "3d",
                "7d",
                "30d",
                "all"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "Response language. `ar` replaces title/summary from the cached Arabic translation when one exists (item gains translated:true, original_title, language:'ar'); items without a cached translation are returned in English unchanged and queued for background translation. Never blocks the request on a model call.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ar"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed items served from the canonical archive, with fallback metadata when the requested freshness window is empty",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "filters": {
                      "type": "object"
                    },
                    "fallback": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Article"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/articles": {
      "get": {
        "summary": "Search archive-backed articles",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "world",
                "politics",
                "markets",
                "tech",
                "ai",
                "science",
                "health",
                "sport",
                "entertainment"
              ]
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "timeframe",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Article"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/articles/{id}": {
      "get": {
        "summary": "Get one archive-backed article",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article payload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Article"
                }
              }
            }
          },
          "404": {
            "description": "Article not found"
          }
        }
      }
    },
    "/search": {
      "get": {
        "summary": "Unified public search for agents",
        "description": "Searches archived articles, active story clusters, and published generated Asha briefs in one no-key read endpoint. Use this as the first search call when an agent does not yet know whether it needs an article, story, or generated brief. Use source_url for original-source citations and share_pack_url when the result can produce final share copy.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Topic, title, source, market symbol, region, or keyword."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25
            }
          },
          {
            "name": "types",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated result families: articles, stories, generated, or all."
          }
        ],
        "responses": {
          "200": {
            "description": "Unified search results grouped by type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing query"
          }
        }
      }
    },
    "/sources": {
      "get": {
        "tags": [
          "Sources"
        ],
        "summary": "Get public source directory catalog",
        "description": "Machine-readable source directory aligned with https://asha.news/sources, including Asha-visible source rows and source-resource scope metadata.",
        "externalDocs": {
          "description": "Human-readable source directory",
          "url": "https://asha.news/sources"
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Asha public source-directory catalog, aligned with https://asha.news/sources.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceDirectoryResponse"
                },
                "examples": {
                  "sourceDirectory": {
                    "value": {
                      "generated_at": "2026-05-24T12:00:00.000Z",
                      "source_directory_updated": "2026-05-24",
                      "total": 126,
                      "count": 1,
                      "fallback": null,
                      "catalog": {
                        "type": "asha_public_source_directory",
                        "human_url": "https://asha.news/sources",
                        "source_directory_updated": "2026-05-24",
                        "total_entries": 126,
                        "categories": 22,
                        "languages": 5,
                        "regions": 9,
                        "asha_visible_entries": 126,
                        "monitoring_resources": {
                          "rss_feeds": "435+",
                          "languages": 21,
                          "map_layers": 45,
                          "early_signal_channels": 26,
                          "allowed_proxy_domains": 291
                        },
                        "source_snapshot": {
                          "active_named_sources": 55,
                          "feed_status_entries": 250,
                          "digest_categories": 16,
                          "digest_items": 250,
                          "verified_at": "2026-05-13 03:45 +03"
                        }
                      },
                      "results": [
                        {
                          "id": "wm-digest-mit-tech-review",
                          "name": "MIT Tech Review",
                          "pipeline": "Asha monitoring network",
                          "kind": "Digest source",
                          "status": "active",
                          "category": "Technology",
                          "categories": [
                            "Technology"
                          ],
                          "language": "English",
                          "language_code": "en",
                          "region": "Global",
                          "scope": "Global",
                          "posture": "high-signal upstream",
                          "trust": "Named digest source monitored for source-aware clustering.",
                          "use": "Evidence-aware clustering, discovery, and source diversity.",
                          "source_url": null,
                          "feed_url": null,
                          "item_count": null,
                          "priority": "source-diversity",
                          "bias": null,
                          "credibility": null,
                          "directory_url": "https://asha.news/sources#wm-digest-mit-tech-review"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sources/health": {
      "get": {
        "tags": [
          "Sources"
        ],
        "summary": "Get source health records",
        "description": "Freshness and health records for source monitoring. Empty registry/health tables may return fallback metadata derived from archive articles.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Source health list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourceHealthResponse"
                },
                "examples": {
                  "sourceHealth": {
                    "value": {
                      "generated_at": "2026-05-24T12:00:00.000Z",
                      "count": 1,
                      "summary": {
                        "total": 1,
                        "healthy": 1,
                        "degraded": 0,
                        "failed": 0,
                        "unknown": 0,
                        "derived": 0,
                        "telemetry_current": 1,
                        "telemetry_stale": 0,
                        "telemetry_unknown": 0
                      },
                      "maintenance": {
                        "owner": "repo_archive_worker",
                        "loop": "source_health",
                        "expected_interval_minutes": 30,
                        "stale_after_minutes": 90,
                        "status": "ok",
                        "fallback_active": false,
                        "actions": [
                          {
                            "type": "no_action_required",
                            "description": "Source-health telemetry is current for the returned rows."
                          }
                        ]
                      },
                      "fallback": null,
                      "results": [
                        {
                          "source_id": "al-jazeera",
                          "source_name": "Al Jazeera",
                          "source_url": "https://www.aljazeera.com",
                          "health_status": "observed",
                          "status": "healthy",
                          "last_success_at": "2026-05-24T11:56:00.000Z",
                          "last_checked_at": "2026-05-24T11:56:00.000Z",
                          "consecutive_failures": 0,
                          "error_type": null,
                          "fallback_behavior": "normal_ingest_priority",
                          "telemetry_status": "current",
                          "telemetry_age_hours": 0.07,
                          "telemetry_stale": false,
                          "operator_action": "no_action_required",
                          "fetched_count": 12,
                          "duplicate_rate": 0,
                          "stale_count": 0,
                          "stale": false,
                          "updated_at": "2026-05-24T11:56:00.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/generated": {
      "get": {
        "summary": "Get generated Asha articles/briefs",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "preview",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "When true, return non-final source-backed preview briefs if no published generated articles are stored. Omitted or false returns published rows only."
          },
          {
            "name": "variant",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Preview digest variant passed to the upstream/source digest fallback."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ar"
              ],
              "default": "en"
            },
            "description": "Response language. Published briefs are stored as one row per language (linked en+ar pairs; the Arabic slug ends in `-ar`). The list collapses each story to the variant matching `lang`, falling back to whichever language exists, and every row carries a `language` field. Also forwarded to the preview digest fallback."
          }
        ],
        "responses": {
          "200": {
            "description": "Published generated article list, one row per story in the requested language (each row carries `language`). Draft, ready, review-required, and preview rows are not public article pages. Every row carries the Asha News AI Agent byline (`author`, `author_name`), `ai_disclosure`, and `author_url` pointing at the public AI author profile https://asha.news/authors/asha-news-ai — relay byline and disclosure together. Each row carries `image_url` plus `image_kind` declaring provenance: `source` = the image comes from one of the article’s own cited sources (imagery is never attached on topic similarity); `illustration` = Asha’s per-vertical AI-generated engraving stood in because no cited source had usable art. An `illustration` is abstract symbolic art, never a depiction of the event — any surface rendering one MUST label it as an illustration. Both are null when there is no image. Each row also carries `visuals` — source-grounded stat blocks ({type:'stats', title, unit, items:[{label, value, source, ref}]}); every value is a figure taken verbatim from the cited source named by `ref`, and the array is empty when the story has no comparable numbers."
          },
          "400": {
            "description": "Unsupported lang value (supported_langs lists the accepted codes)."
          }
        }
      },
      "post": {
        "summary": "Submit a generated article for human review (agent key with the 'publish' scope). Externally submitted articles NEVER auto-publish: the row is forced to review_status='pending_review' and publish_status='review_required' regardless of the quality gate, and only an Asha editor's approval makes it public.",
        "description": "Body is validated by the same gate as POST /generated/validate and returns the same 422 body on failure. INSERT-ONLY: a slug that already exists is refused with 409, never overwritten. Send an Idempotency-Key so a retried cron run replays its original 202 (with idempotent_replay=true) instead of minting a duplicate draft; keys are scoped per agent key. Accepted submissions carry the Asha News AI Agent byline and the submitting agent's provenance. Rate limited per API key.",
        "security": [
          {
            "AgentKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200,
              "pattern": "^[\\x20-\\x7E]{1,200}$"
            },
            "description": "Opaque retry key, 1-200 printable ASCII characters, scoped to your agent key. Resubmitting the identical payload under the same key returns the original 202 with idempotent_replay=true and inserts nothing; the same key with a different payload is refused with 409 idempotency_key_reused. 'Identical' is a SHA-256 fingerprint over title, slug, summary, content, category, citations, source_article_ids, keywords, and tags INCLUDING array order — reordering an array between retries is a different payload. A replay reports the submission's original queued state, so use GET /coverage to learn whether an editor has since published it. The header wins over body.idempotency_key."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GeneratedArticleSubmission"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued for human review. Returns id, slug, status='queued_for_human_review', the AI author byline with its disclosure, and the validation report. An idempotent retry of an accepted submission returns this same body plus idempotent_replay=true. Not publicly visible until approved."
          },
          "401": {
            "description": "Missing or invalid agent key"
          },
          "403": {
            "description": "Key lacks the 'publish' scope (self-serve keys are read-only; ask an Asha admin)"
          },
          "409": {
            "description": "Either slug_taken (the slug already exists at Asha and is never overwritten; submit under a different slug) or idempotency_key_reused (this Idempotency-Key was already used for a different payload)."
          },
          "422": {
            "description": "Failed the generated-article quality gate (body is the validation report), a field exceeded its length cap — fields are rejected rather than truncated so the stored gate report always describes the stored text — or invalid_idempotency_key."
          },
          "429": {
            "description": "Submission rate limit reached for this key"
          },
          "413": {
            "description": "Payload exceeds 256kb"
          }
        }
      }
    },
    "/coverage": {
      "get": {
        "summary": "Deterministic existing-coverage lookup",
        "description": "Answers \"does Asha already have a live public article for this?\" so an agent links owned media instead of an aggregator, and only drafts a new story when coverage is genuinely missing. Matching is deterministic — canonical source URL (normalized+hashed like ingestion), source article identity, then title/topic text — with a stable total order, so repeated cron runs reach the same reuse-or-create decision. The two strong tiers are index-backed across the ENTIRE published archive, so a story published long ago is still found by its source URL; the weak text tiers read a bounded recency window and every response reports its exhaustiveness in `scan`. Only published briefs with public evidence ever appear; drafts and in-review rows are never disclosed in any form. Pair with the Idempotency-Key header on POST /generated for the create path.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "A canonical source URL the agent is considering covering. Tracking parameters are stripped by the same normalizer ingestion uses."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Topic or entity text. At least one of url or q is required."
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO-8601 lower bound on the matched brief's published_at."
          },
          {
            "name": "until",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO-8601 upper bound on the matched brief's published_at."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Coverage verdict with ranked matches. covered=true and a top-level asha_url appear only for exact/strong matches; weak matches are listed but recommend needs_human_review. Check scan.complete before trusting an empty result as 'no coverage'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CoverageResponse"
                }
              }
            }
          },
          "400": {
            "description": "Neither url nor q was provided, or since/until is not an ISO-8601 date."
          }
        }
      }
    },
    "/share-pack": {
      "get": {
        "summary": "Get a share-ready package for a published Asha brief/story",
        "description": "Returns a source-backed share pack only for published generated Asha briefs or active/published story clusters with stored citations. The query endpoint prefers published generated briefs, then can fall through to a cited story cluster by query. Eligible packs include a stored or generated routable Asha short URL. If a matching generated item is still ready/review-only, missing citations, or no cited Asha story is available, found=false is returned with a clear reason and optional article candidates.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search query, slug, generated-article id, or story topic. Omit to return the latest published generated-brief share pack."
          }
        ],
        "responses": {
          "200": {
            "description": "Share-pack payload or explicit empty/fallback response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharePack"
                },
                "examples": {
                  "publishedPack": {
                    "value": {
                      "found": true,
                      "generated_at": "2026-07-01T09:00:00.000Z",
                      "query": "gaza",
                      "matched_by": "search",
                      "story_id": "gen-1",
                      "title": "Gaza hospitals face new evacuation pressure",
                      "asha_url": "https://asha.news/article/gaza-hospitals-face-new-evacuation-pressure",
                      "short_url": "https://asha.news/s/a-gaza-hospitals-face-new-evacuation-pressure",
                      "one_liner": "Hospitals in Gaza face new evacuation pressure as aid groups warn care is collapsing.",
                      "summary_short": "Hospitals in Gaza face new evacuation pressure as aid groups warn care is collapsing.",
                      "summary_long": "Asha's published brief summarizes the current source trail and why it matters.",
                      "why_it_matters": "Hospital capacity shapes civilian survival, aid access, and how official claims should be checked against ground reporting.",
                      "key_points": [
                        "Hospitals in Gaza face renewed evacuation pressure."
                      ],
                      "citations": [
                        {
                          "index": 1,
                          "source": "Al Jazeera",
                          "title": "Hospitals in Gaza face evacuation pressure",
                          "url": "https://example.com/source-story",
                          "published_at": "2026-07-01T08:30:00.000Z"
                        }
                      ],
                      "source_urls": [
                        "https://example.com/source-story"
                      ],
                      "freshness_status": "fresh",
                      "confidence": 0.91,
                      "publish_status": "published",
                      "review_status": "approved",
                      "social_posts": {
                        "x": "Gaza hospitals face new evacuation pressure\nHospitals in Gaza face new evacuation pressure as aid groups warn care is collapsing.\n1 cited source: Al Jazeera\nhttps://asha.news/article/gaza-hospitals-face-new-evacuation-pressure"
                      }
                    }
                  },
                  "empty": {
                    "value": {
                      "found": false,
                      "query": "not-final",
                      "reason": "matched_generated_article_is_ready_not_published",
                      "story_id": null,
                      "citations": [],
                      "source_urls": [],
                      "freshness_status": "ambiguous",
                      "confidence": null,
                      "social_posts": null
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/short-links/{key}": {
      "get": {
        "summary": "Check an Asha short-link key without following the redirect",
        "description": "Returns the resolved public Asha target for stored short URLs and generated share-pack keys. Non-final generated briefs, inactive stories, uncited content, absent keys, and unsafe keys return found=false instead of fabricating a target.",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key after /s/, for example a-gaza-hospitals or st-story-1."
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved short-link status or explicit found=false response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShortLinkStatus"
                },
                "examples": {
                  "generatedBrief": {
                    "value": {
                      "found": true,
                      "key": "a-gaza-hospitals-face-new-evacuation-pressure",
                      "short_url": "https://asha.news/s/a-gaza-hospitals-face-new-evacuation-pressure",
                      "target_type": "generated_article",
                      "target_id": "gen-1",
                      "slug": "gaza-hospitals-face-new-evacuation-pressure",
                      "route": "/article/gaza-hospitals-face-new-evacuation-pressure",
                      "asha_url": "https://asha.news/article/gaza-hospitals-face-new-evacuation-pressure",
                      "source": "generated_share_pack_key",
                      "publish_status": "published",
                      "review_status": "approved",
                      "evidence": {
                        "has_evidence": true,
                        "citation_count": 2,
                        "source_article_count": 2
                      }
                    }
                  },
                  "empty": {
                    "value": {
                      "found": false,
                      "key": "a-missing",
                      "reason": "short_link_not_found",
                      "short_url": "https://asha.news/s/a-missing"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsafe or invalid short-link key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShortLinkStatus"
                }
              }
            }
          }
        }
      }
    },
    "/stories/{id}/share-pack": {
      "get": {
        "summary": "Get a share-ready package for an active Asha story cluster",
        "description": "Returns a source-backed share pack for an active story cluster only when linked article/source evidence is stored. Eligible packs include a stored or generated routable Asha short URL. If the story is missing or has no cited articles, found=false or 404 is returned without fabricating citations.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Story cluster id."
          }
        ],
        "responses": {
          "200": {
            "description": "Story share-pack payload or explicit empty response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharePack"
                },
                "examples": {
                  "storyPack": {
                    "value": {
                      "found": true,
                      "generated_at": "2026-07-01T09:00:00.000Z",
                      "query": "story-1",
                      "matched_by": "story_id",
                      "story_id": "story-1",
                      "title": "Aid convoy attack draws conflicting accounts",
                      "asha_url": "https://asha.news/story/story-1",
                      "short_url": "https://asha.news/s/st-story-1",
                      "one_liner": "Asha grouped reports on an aid convoy attack and the conflicting official and ground accounts.",
                      "summary_short": "Asha grouped reports on an aid convoy attack and the conflicting official and ground accounts.",
                      "summary_long": "Why it matters: Aid convoy attacks shape humanitarian access and must be checked against source evidence.",
                      "citations": [
                        {
                          "index": 1,
                          "article_id": "article-1",
                          "source": "Reuters",
                          "title": "Aid convoy attack reported by medics",
                          "url": "https://example.com/source-story",
                          "published_at": "2026-07-01T08:30:00.000Z"
                        }
                      ],
                      "source_urls": [
                        "https://example.com/source-story"
                      ],
                      "freshness_status": "fresh",
                      "publish_status": "active",
                      "social_posts": {
                        "x": "Aid convoy attack draws conflicting accounts\nAsha grouped reports on an aid convoy attack and the conflicting official and ground accounts.\n1 cited source: Reuters\nhttps://asha.news/story/story-1"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Story cluster not found"
          }
        }
      }
    },
    "/share-links": {
      "post": {
        "tags": [
          "Agent Actions"
        ],
        "summary": "Mint the stable short link for a shareable Asha target",
        "description": "Returns the canonical https://asha.news/s/{code} short link for an article, generated brief, story cluster, or allowlisted site route. Idempotent: the same target always resolves to the same code, so call it freely rather than caching codes. No API key. Verify a code you did not mint with GET /short-links/{key} instead of following the redirect. Also mounted at /api/share-links for the site's own share buttons.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareLinkRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Minted or existing short link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLink"
                },
                "examples": {
                  "article": {
                    "value": {
                      "code": "q2m4xyz",
                      "short_url": "https://asha.news/s/q2m4xyz",
                      "route": "/article/2f6a1c58-1f4e-4a2b-9a77-6d0d0f1b7c31"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "target_type or target_id missing, or a route target outside the allowlist"
          },
          "404": {
            "description": "The target does not exist"
          },
          "503": {
            "description": "Share links are temporarily unavailable"
          }
        }
      }
    },
    "/newsletter/subscriptions": {
      "post": {
        "tags": [
          "Agent Actions"
        ],
        "summary": "Start a double-opt-in newsletter signup",
        "description": "Double opt-in. Records a PENDING subscription and emails the address a confirmation link; nothing is delivered until the button on that page is pressed (POST /newsletter/confirm), so an unconfirmed signup is inert. Only submit an address its owner gave you in the current conversation, and tell them to check their inbox — you cannot confirm on their behalf. The response is identical whether or not the address is already subscribed, so it cannot be used to test whether someone is on the list. Rate-limited separately from the read pool. When email delivery is not configured on the server this endpoint answers 503 newsletter_signup_unavailable instead of accepting signups whose confirmation email could never arrive. The site's own single-opt-in signup form is a different endpoint and is not part of this contract.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewsletterSignupRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Confirmation email queued; the subscription is pending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewsletterSignupPending"
                },
                "examples": {
                  "pending": {
                    "value": {
                      "subscribed": false,
                      "status": "pending_confirmation",
                      "confirmation_required": true,
                      "message": "A confirmation email is on its way. The subscription starts only after the link in it is clicked.",
                      "options": [
                        "daily"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A valid email address is required"
          },
          "429": {
            "description": "Too many newsletter signup requests from this caller"
          },
          "503": {
            "description": "Signup unavailable — either a temporary outage, or email delivery is not configured (error code newsletter_signup_unavailable) so the confirmation email cannot be sent"
          }
        }
      }
    },
    "/newsletter/confirm": {
      "get": {
        "tags": [
          "Agent Actions"
        ],
        "summary": "Open a newsletter confirmation link",
        "description": "The link Asha emails after POST /newsletter/subscriptions. GET never changes state — mail scanners and link-preview bots follow every GET in an email, so activation on GET would auto-confirm subscriptions the owner never approved. Browsers get a page whose button submits POST /newsletter/confirm; JSON clients get status confirmation_pending with instructions to POST. This is the address owner's action, not the agent's — do not drive it yourself.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The confirmation token from the emailed link."
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmation prompt (no state change)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscribed": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "confirmation_pending"
                      ]
                    },
                    "confirmation_required": {
                      "type": "boolean"
                    },
                    "method": {
                      "type": "string",
                      "enum": [
                        "POST"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A valid confirmation token is required"
          }
        }
      },
      "post": {
        "tags": [
          "Agent Actions"
        ],
        "summary": "Redeem a newsletter confirmation token",
        "description": "Flips the pending row to active. This is the address owner's action — the button on the emailed link's page submits it; do not call it on their behalf. Accepts the token as JSON or form body (or query parameter). Redeeming twice within the token's 72-hour window is idempotent. Browsers get a confirmation page; JSON clients get JSON.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The confirmation token from the emailed link."
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscription activated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscribed": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active"
                      ]
                    },
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A valid confirmation token is required"
          },
          "404": {
            "description": "Token unknown, expired, or belonging to an unsubscribed address"
          }
        }
      }
    },
    "/blindspot": {
      "get": {
        "tags": [
          "Coverage"
        ],
        "summary": "Get persisted Asha Blindspots / coverage gaps",
        "description": "Returns only stored consensus_blindspot or legacy coverage-gap rows from active story clusters. The endpoint does not infer blindspots when posture data is missing.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            },
            "description": "Max flagged clusters to return. Default 20."
          },
          {
            "name": "topic",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Substring filter over cluster topic, title, and summary."
          },
          {
            "name": "min_mci",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            },
            "description": "Minimum persisted Manufactured Consensus Index. Rows without MCI are excluded when this is set."
          },
          {
            "name": "posture",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Require this posture key inside posture_distribution.by_posture."
          }
        ],
        "responses": {
          "200": {
            "description": "Persisted coverage gaps, or an explicit empty/unavailable response without inferred data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlindspotResponse"
                },
                "examples": {
                  "available": {
                    "value": {
                      "generated_at": "2026-07-01T09:00:00.000Z",
                      "status": "available",
                      "methodology": "manufactured_consensus_index",
                      "filters": {
                        "topic": "palestine",
                        "min_mci": 0.7,
                        "posture": "",
                        "limit": 20
                      },
                      "count": 1,
                      "warning": null,
                      "results": [
                        {
                          "id": "cluster-gaza",
                          "title": "Gaza ceasefire framing converges",
                          "topic": "palestine",
                          "article_count": 8,
                          "source_count": 7,
                          "asha_url": "https://asha.news/story/cluster-gaza",
                          "flagged": true,
                          "mci": 0.73,
                          "missing_frame": "palestinian_evidence_based",
                          "source_type": "consensus_blindspot"
                        }
                      ]
                    }
                  },
                  "empty": {
                    "value": {
                      "generated_at": "2026-07-01T09:00:00.000Z",
                      "status": "no_flagged_clusters",
                      "methodology": "manufactured_consensus_index",
                      "count": 0,
                      "results": [],
                      "warning": "No persisted consensus_blindspot rows matched; this endpoint does not infer blindspots without stored posture data."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/generated/{id}": {
      "patch": {
        "summary": "Revise your own still-pending generated-article submission (agent key with the 'publish' scope). Revisions NEVER touch published content: only rows owned by the calling key with review_status='pending_review' are editable, and every revision is forced back into the human review queue.",
        "description": "Omitted fields keep their stored values; the merged document is re-validated by the same gate as POST /generated and returns the same 422 body on failure. Supplied fields must carry their documented JSON types (422 invalid_field_type). Slug is immutable (422 slug_immutable). Rows owned by other keys — and unknown or malformed ids — answer 404 without confirming existence. Rows already decided by an editor answer 409 submission_not_editable, including under races: the ownership, pending-review, and revision-seq checks are atomic with the update, so a concurrent revision or editor decision also answers 409 — re-read and retry. Each revision appends to an audit trail (quality_gate_result.agent_revisions, capped at 20), preserves stored gate metadata and the original submission provenance. The merged document is validated together with its stored quality gate — the declared required_checks and thresholds bind every revision, and a submission whose evidence an editor has invalidated answers 422 until a human re-verifies it. Retries are safe: a replay whose merged values equal the stored row is acknowledged with 202 without writing a new revision. Rate limited per API key (429).",
        "security": [
          {
            "AgentKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The id returned by your POST /generated submission."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GeneratedArticleRevision"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Revision stored and re-queued for human review (or an identical replay acknowledged without a new revision). Returns id, slug, status='queued_for_human_review', the revision number, the AI author byline with its disclosure, and the fresh validation report."
          },
          "401": {
            "description": "Missing or invalid agent key."
          },
          "403": {
            "description": "Key lacks the 'publish' scope (admin-granted only)."
          },
          "404": {
            "description": "No submission with this id belongs to the calling key. Deliberately identical for unknown, malformed, and other-agent ids."
          },
          "409": {
            "description": "submission_not_editable — the row was decided by an editor or revised concurrently; re-read it and retry if it is still pending."
          },
          "413": {
            "description": "Payload exceeds the 256KB submission budget."
          },
          "422": {
            "description": "Field over its length limit, wrong-typed (invalid_field_type), slug_immutable, or the merged document failed the validation gate (same body as POST /generated/validate)."
          },
          "429": {
            "description": "agent_publish_rate_limited — the hourly publish-scope budget is shared with POST /generated; the standard RateLimit headers say when the window resets."
          },
          "503": {
            "description": "Generated-article store unavailable."
          }
        }
      }
    },
    "/generated/validate": {
      "post": {
        "summary": "Validate generated article gates before publication",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Generated article is valid"
          },
          "422": {
            "description": "Generated article failed validation"
          }
        }
      }
    },
    "/digest": {
      "get": {
        "summary": "Get digest (personal or public)",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "personal",
                "public"
              ]
            }
          },
          {
            "name": "topic",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bias",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "left",
                "center",
                "right"
              ]
            }
          },
          {
            "name": "timeframe",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "6h",
                "24h",
                "3d",
                "7d",
                "30d"
              ]
            }
          },
          {
            "name": "contentType",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "markdown"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ar"
              ],
              "default": "en"
            },
            "description": "Response language, echoed in `filters.lang`. `ar` replaces cluster title/summary from the cached Arabic translation of the cluster's lead article when one exists (cluster gains translated:true, original_title, language:'ar') and localizes digest_text scaffolding; clusters without a cached translation stay in English. Cache-only — never triggers a model call."
          }
        ],
        "responses": {
          "200": {
            "description": "Digest payload",
            "content": {
              "application/json": {
                "examples": {
                  "digestExample": {
                    "value": {
                      "generated_at": "2026-02-28T10:00:00.000Z",
                      "scope": "public",
                      "filters": {
                        "topic": "markets",
                        "timeframe": "24h",
                        "source": "",
                        "bias": "all",
                        "contentType": "all"
                      },
                      "user": null,
                      "clusters": [
                        {
                          "id": 123,
                          "title": "Oil extends gains amid supply concerns",
                          "summary": "Energy markets reacted to fresh supply disruptions.",
                          "topic": "markets",
                          "article_count": 12,
                          "source_count": 9,
                          "created_at": "2026-02-28T09:30:00.000Z",
                          "status": "active"
                        }
                      ],
                      "digest_text": "Asha News public digest (1 clusters)"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Auth required for personal digest"
          }
        }
      }
    },
    "/digest/share-token": {
      "post": {
        "summary": "Enable/disable or rotate public digest token for authenticated user",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "rotate": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Share token state updated",
            "content": {
              "application/json": {
                "examples": {
                  "enabled": {
                    "value": {
                      "success": true,
                      "enabled": true,
                      "token": "abc123",
                      "public_path": "/api/v1/public/token/abc123"
                    }
                  },
                  "disabled": {
                    "value": {
                      "success": true,
                      "enabled": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "404": {
            "description": "User profile not found"
          }
        }
      }
    },
    "/public/{userId}": {
      "get": {
        "summary": "Get public digest for a user",
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public digest payload"
          },
          "403": {
            "description": "Public digest disabled"
          },
          "404": {
            "description": "User not found"
          }
        }
      }
    },
    "/public/token/{token}": {
      "get": {
        "summary": "Get public digest by share token",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Public digest payload"
          },
          "404": {
            "description": "Token not found"
          }
        }
      }
    },
    "/clusters/search": {
      "get": {
        "summary": "Search story clusters",
        "description": "Searches active story clusters, published generated Asha briefs, and article-backed fallback clusters. Results are relevance-ranked and include source_type when available.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cluster search results",
            "content": {
              "application/json": {
                "examples": {
                  "clusterSearch": {
                    "value": {
                      "query": "btc",
                      "count": 1,
                      "results": [
                        {
                          "id": 88,
                          "title": "Bitcoin jumps above key resistance",
                          "summary": "Crypto markets rallied after macro data release.",
                          "topic": "markets",
                          "article_count": 7,
                          "source_count": 6,
                          "created_at": "2026-02-28T10:15:00.000Z",
                          "status": "active"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing query"
          }
        }
      }
    },
    "/instruments/{symbol}/news": {
      "get": {
        "summary": "Get instrument-related news",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "Translate visible news titles and summaries when a cached or bounded on-demand translation is available.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ar",
                "uk",
                "de"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instrument news results",
            "content": {
              "application/json": {
                "examples": {
                  "instrumentNews": {
                    "value": {
                      "symbol": "BTCUSD",
                      "aliases": [
                        "btc",
                        "bitcoin",
                        "btcusd"
                      ],
                      "language": "de",
                      "count": 2,
                      "translation_pending_count": 0,
                      "translation_cache_available": true,
                      "results": [
                        {
                          "id": 1001,
                          "title": "Bitcoin setzt Wochenrally fort",
                          "summary": "Analysten verweisen auf ETF-Zuflüsse und die Risikostimmung.",
                          "language": "de",
                          "translated": true,
                          "original_title": "Bitcoin extends weekly rally",
                          "source": "Reuters",
                          "url": "https://example.com/article",
                          "category": "markets",
                          "published_at": "2026-02-28T11:00:00.000Z",
                          "bias": "center"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/instruments/prices": {
      "get": {
        "summary": "Get market ticker price snapshot for supported symbols",
        "parameters": [
          {
            "name": "symbols",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated symbols, e.g. BTCUSD,ETHUSD,XAUUSD"
          }
        ],
        "responses": {
          "200": {
            "description": "Price snapshot results",
            "content": {
              "application/json": {
                "examples": {
                  "priceSnapshot": {
                    "value": {
                      "source": "live",
                      "updated_at": "2026-02-28T11:01:00.000Z",
                      "count": 2,
                      "results": [
                        {
                          "symbol": "BTCUSD",
                          "quote_symbol": "BTC-USD",
                          "price": 64321.5,
                          "previous_close": 63809.2,
                          "change": 512.3,
                          "change_pct": 0.8,
                          "currency": "USD",
                          "market_state": "REGULAR",
                          "as_of": "2026-02-28T11:00:55.000Z"
                        },
                        {
                          "symbol": "ETHUSD",
                          "quote_symbol": "ETH-USD",
                          "price": 3412.1,
                          "previous_close": 3434.8,
                          "change": -22.7,
                          "change_pct": -0.66,
                          "currency": "USD",
                          "market_state": "REGULAR",
                          "as_of": "2026-02-28T11:00:55.000Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/voices": {
      "get": {
        "summary": "Get curated source/voice channels",
        "parameters": [
          {
            "name": "curation",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "editor",
                "algorithmic"
              ]
            }
          },
          {
            "name": "topic",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to voices whose topics array includes this value."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Curated voices payload"
          },
          "500": {
            "description": "Voices lookup failed"
          }
        }
      }
    },
    "/posts": {
      "get": {
        "summary": "Get verified posts",
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "x",
                "threads",
                "bluesky",
                "truth_social",
                "mastodon"
              ]
            }
          },
          {
            "name": "account_class",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "independent",
                "institutional",
                "government",
                "journalist"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verified posts payload"
          },
          "500": {
            "description": "Posts lookup failed"
          }
        }
      }
    },
    "/breaking": {
      "get": {
        "summary": "Breaking news: active alerts plus breaking-flagged articles, with a published poll contract (60s interval, since deltas, ETag conditional requests)",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only items published after this ISO timestamp"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "timeframe",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "6h",
                "24h",
                "3d"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Breaking items with age_minutes, expires_at, image_url, and the poll contract"
          }
        }
      }
    },
    "/agents/signup": {
      "post": {
        "summary": "Register an agent and receive an API key (asha_v1_...). The key is shown once; store it. Required for webhook subscriptions.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "agentName"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "agentName": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "useCase": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "organization": {
                    "type": "string",
                    "maxLength": 120
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Agent registered; response includes the one-time apiKey"
          },
          "400": {
            "description": "Invalid email, agent name, use case, or organization"
          },
          "503": {
            "description": "Key issuance temporarily unavailable"
          }
        }
      }
    },
    "/webhooks": {
      "post": {
        "summary": "Register a breaking-news webhook (agent key required). Deliveries are HMAC-SHA256 signed (X-Asha-Signature); max 5 active webhooks per key; public https URLs only.",
        "security": [
          {
            "AgentKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public https endpoint to receive signed POSTs"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "breaking"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscription created; response includes the signing secret (shown once)"
          },
          "400": {
            "description": "Invalid URL (non-https, private/internal host) or unsupported events"
          },
          "401": {
            "description": "Missing or invalid agent key"
          },
          "429": {
            "description": "Webhook limit reached for this key"
          }
        }
      },
      "get": {
        "summary": "List this agent key's webhook subscriptions (secrets omitted)",
        "security": [
          {
            "AgentKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Subscriptions with delivery health"
          },
          "401": {
            "description": "Missing or invalid agent key"
          }
        }
      }
    },
    "/webhooks/{id}": {
      "delete": {
        "summary": "Delete a webhook subscription owned by this agent key",
        "security": [
          {
            "AgentKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found or not owned by this key"
          }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "summary": "Send a signed test event to a webhook subscription now. A successful test re-enables an auto-disabled subscription.",
        "security": [
          {
            "AgentKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivered"
          },
          "404": {
            "description": "Not found"
          },
          "502": {
            "description": "Delivery failed"
          }
        }
      }
    },
    "/brief/daily": {
      "get": {
        "summary": "Server-rendered daily brief: categorized sections with publish time + age per item, guaranteed images, and a coverage graph; Palestine desk leads",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "markdown",
                "html"
              ]
            },
            "description": "markdown for chat relay, html for a styled self-contained page, json for structured data"
          },
          {
            "name": "per_section",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10
            }
          },
          {
            "name": "timeframe",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "6h",
                "24h",
                "3d"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Finished brief artifact in the requested format; relay without re-assembly"
          }
        }
      }
    },
    "/articles/{id}/share": {
      "get": {
        "summary": "Ready-to-post social share payloads for an article, formatted per platform with length limits, hashtags, image, and intent URLs",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "x",
                "bluesky",
                "threads",
                "mastodon",
                "telegram",
                "whatsapp",
                "facebook",
                "linkedin"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Share payloads; post text + url verbatim and preserve attribution"
          },
          "404": {
            "description": "Article not found"
          }
        }
      }
    },
    "/editorial-guidelines": {
      "get": {
        "summary": "Machine-readable editorial voice contract: evidence-first, openly pro-Palestine, citations preserved",
        "responses": {
          "200": {
            "description": "Editorial guidelines JSON (markdown variant at /.well-known/editorial-guidelines.md)"
          }
        }
      }
    },
    "/conflict/state": {
      "get": {
        "tags": [
          "Conflict Monitor"
        ],
        "summary": "Conflict Monitor snapshot: recent official alerts, latest geolocated events, active/mature clusters, EN/AR theater brief, per-tier freshness heartbeats, and a degraded flag",
        "description": "Polling fallback for the SSE stream. Without since: full snapshot {seq, alerts, events, brief, clusters, freshness, degraded}. With since={seq}: only envelopes newer than that sequence number ({seq, envelopes, brief, freshness, degraded}). Also served publicly at /api/conflict/state.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Envelope sequence cursor; return only envelopes with seq greater than this."
          }
        ],
        "responses": {
          "200": {
            "description": "Conflict state document. seq is the latest envelope sequence — reuse it as the next since cursor or as Last-Event-ID on the SSE stream."
          }
        }
      }
    },
    "/conflict/alerts": {
      "get": {
        "tags": [
          "Conflict Monitor"
        ],
        "summary": "Recent conflict alerts (sirens, launch reports, all-clears) as SSE envelopes, keyset-paginated by envelope seq",
        "description": "Alerts from official sources carry confidence \"official\"; OSINT early warnings are always \"unconfirmed\" — never present them as official. When the in-memory ring is empty (fresh process) and no since is given, rows are backfilled from the archive with seq null (source \"archive\").",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Return only alert envelopes with seq greater than this."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            },
            "description": "Max alerts (default 20)."
          }
        ],
        "responses": {
          "200": {
            "description": "{seq, count, source, alerts: [{seq, type, ts, payload}]} — payload has id, source, kind, areas, areas_ar, started_at, confidence"
          }
        }
      }
    },
    "/conflict/events": {
      "get": {
        "tags": [
          "Conflict Monitor"
        ],
        "summary": "Geolocated conflict events (strike, explosion, intercept, movement, statement, thermal_confirmation) with severity, verification status, and EN/AR summaries",
        "description": "Without since: latest rows from the events archive, newest first. With since={seq}: delta of event envelopes newer than the cursor. bbox filters to a lon/lat window.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Envelope sequence cursor for delta polls."
          },
          {
            "name": "bbox",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Bounding box \"w,s,e,n\" in degrees (lon west, lat south, lon east, lat north), e.g. 34,29,36,34."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "Max events (default 50)."
          }
        ],
        "responses": {
          "200": {
            "description": "{seq, count, events} — each event has id, type, lat, lon, occurred_at, severity (1-5), status (reported|corroborated|confirmed), channels, cluster_id, summary_en, summary_ar"
          },
          "400": {
            "description": "Malformed bbox (expected four comma-separated numbers)"
          }
        }
      }
    },
    "/conflict/brief": {
      "get": {
        "tags": [
          "Conflict Monitor"
        ],
        "summary": "AI theater brief for the Middle East monitor, English or Arabic",
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "ar"
              ]
            },
            "description": "Brief language (default en)."
          }
        ],
        "responses": {
          "200": {
            "description": "{lang, text, updated_at, source_count}"
          },
          "404": {
            "description": "No brief generated yet for this language"
          }
        }
      }
    },
    "/fact-check/post": {
      "post": {
        "tags": [
          "Fact Check"
        ],
        "servers": [
          {
            "url": "https://asha.news/api",
            "description": "Public fact-check namespace \u2014 the social-post checker lives under /api/fact-check, NOT /api/v1."
          },
          {
            "url": "/api",
            "description": "Same-origin relative base for the public fact-check namespace."
          }
        ],
        "summary": "Fact-check a social media post from a link or a screenshot",
        "description": "Accepts JSON {\"url\": \"<post link>\"} for an X/Twitter, Telegram, TikTok, Instagram, or Facebook post, OR multipart/form-data with an `image` file (JPEG/PNG/WebP screenshot, \u22645\u00a0MB; EXIF/XMP stripped server-side). Extracts the checkable claims, checks them against the archive, the curated claim library, and live search, and returns a citation-backed rebuttal whose every sentence carries numbered source references, plus a paste-ready `share_text`. Public (no key), bounded by the AI rate limiter. status \"unreadable\" means the platform blocked reading \u2014 upload a screenshot instead; the response never fabricates content.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Direct link to a supported social-media post."
                  }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "image": {
                    "type": "string",
                    "format": "binary",
                    "description": "Screenshot of the post (JPEG/PNG/WebP, \u22645\u00a0MB)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check result: post text, extracted claims, numbered sources, a grounded rebuttal, and share_text.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "413": {
            "description": "Screenshot exceeds the 5\u00a0MB cap."
          },
          "415": {
            "description": "Uploaded bytes are not a valid JPEG/PNG/WebP image."
          },
          "422": {
            "description": "Unsupported post URL, or a screenshot the vision model could not read."
          },
          "503": {
            "description": "Screenshot reading is not configured on this deployment."
          }
        }
      }
    },
    "/conflict/stream": {
      "get": {
        "tags": [
          "Conflict Monitor"
        ],
        "servers": [
          {
            "url": "https://asha.news/api",
            "description": "Public conflict namespace — the SSE stream lives under /api/conflict, NOT /api/v1."
          },
          {
            "url": "/api",
            "description": "Same-origin relative base for the public conflict namespace."
          }
        ],
        "summary": "Server-Sent Events stream of Conflict Monitor envelopes (text/event-stream)",
        "description": "On connect the server sends retry: 3000 and a `snapshot` event (same document as /conflict/state), then forwards each envelope as `id: <seq>`, `event: <alert|event|brief|cluster>`, `data: <envelope JSON>`. Resume with the Last-Event-ID header (or ?lastEventId=): cursors still inside the replay ring get exactly the missed envelopes; older cursors get a fresh snapshot first. A `: ping` comment every 30s keeps the connection alive. Per-IP and global connection caps return 429 — fall back to polling /api/v1/conflict/state with since.",
        "parameters": [
          {
            "name": "lastEventId",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Resume cursor for clients that cannot set the Last-Event-ID header."
          }
        ],
        "responses": {
          "200": {
            "description": "text/event-stream of snapshot + typed envelopes"
          },
          "429": {
            "description": "Connection cap reached; use the polling fallback"
          }
        }
      }
    },
    "/palestine": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Palestine desk timeline (evidence-classed events, freshness indicator) plus `desk` — the desk section map: Now / The Record / Israel Exposed wings, each page with key, label, group, path, canonical_url, and blurb.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "verified",
                "cross_verified",
                "pending",
                "leak"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "generated_at, count, freshness {latest_event_at, age_hours, stale}, timeline rows, and desk.wings — the canonical section taxonomy shared with the human UI."
          }
        }
      }
    },
    "/palestine/meta": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Palestine archive corpus stats + dataset provenance (upstream metadata mirror counts, per-file sha256/etag/fetched_at).",
        "responses": {
          "200": {
            "description": "Counts, newest_item_at, and dataset provenance."
          }
        }
      }
    },
    "/palestine/search": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Universal Palestine search across live upstream media (82k+), the memorial (60k names), and the curated claim library.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "media",
                "victims",
                "claims"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated media/victims blocks plus matching claim-library records."
          }
        }
      }
    },
    "/palestine/archive/items": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Full media record list from the Archive Genocide corpus (metadata only; media never hosted by Asha News).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "archive",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated archive-item pointers with upstream confidence and viewer_url."
          }
        }
      }
    },
    "/palestine/archive/items/{id}": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Stable archive-item record (AG-<id>) with description, provenance, and ranked media candidates.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Full item record."
          },
          "404": {
            "description": "Item not found."
          }
        }
      }
    },
    "/palestine/archive/media/{id}/candidates": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Ranked external playback candidates for an archive item (upstream viewer + mirrors). No media bytes are proxied.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ordered candidate list."
          },
          "404": {
            "description": "Item not found."
          }
        }
      }
    },
    "/palestine/archive/groups": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Upstream archive source groups with live per-group record counts (provenance, not verification).",
        "responses": {
          "200": {
            "description": "Groups sorted by item_count."
          }
        }
      }
    },
    "/palestine/victims": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Search the Killed in Gaza memorial (Arabic/English names) — 60,198 identified records, preserved verbatim.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated memorial records."
          }
        }
      }
    },
    "/palestine/victims/{id}": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Individual memorial record (AGV-<id>); nothing inferred beyond the published dataset.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Memorial record."
          },
          "404": {
            "description": "Record not found."
          }
        }
      }
    },
    "/palestine/updates": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Newest records indexed from the upstream archive (the live feed). Asha validation is a separate editorial layer.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest archive-item records."
          }
        }
      }
    },
    "/palestine/claims": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "The curated Claims & Rebuttals library: canonical talking points, rhetorical tactic, verdict, evidence-anchored short answer.",
        "responses": {
          "200": {
            "description": "Claim families + claim records."
          }
        }
      }
    },
    "/palestine/fact-checks": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Curated Palestine fact checks with verdict, evidence for/against, and correction history.",
        "responses": {
          "200": {
            "description": "Fact-check records."
          }
        }
      }
    },
    "/palestine/genocide-case": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "The curated genocide-case record: institutions (each finding characterized exactly to its instrument — provisional measures, warrants, determinations), separately attributed figures, case timeline, and FAQ. Same data as the /palestine/genocide-case page.",
        "responses": {
          "200": {
            "description": "generated_at, canonical_url, updated, title, standfirst, how_to_read_this, institutions[], numbers[], timeline[], faq[]."
          }
        }
      }
    },
    "/palestine/check": {
      "post": {
        "tags": [
          "Palestine"
        ],
        "summary": "Agentic claim checker: match a claim to the claim library and (when a provider is configured) add a library-grounded AI assessment. Never invents citations.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "claim": {
                    "type": "string"
                  }
                },
                "required": [
                  "claim"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matched claims + fact checks, plus a labeled ai_assessment and ai_status."
          },
          "400": {
            "description": "Claim too short."
          }
        }
      }
    },
    "/palestine/hasbara-trends": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Latest hasbara trend scan: the anti-Palestinian talking points circulating most on X/media, each linked to a claim-library debunk.",
        "description": "Each covered point also carries `tweet_reply` — a paste-ready X reply (text, weighted x_chars ≤280, intent_url) composed only from the curated library short answer plus its receipts URL — and `x_posts`, the X status links the scan surfaced pushing the claim. Uncovered points have tweet_reply:null (documented by the autopilot, never improvised). Relay replies verbatim with the receipts URL intact.",
        "responses": {
          "200": {
            "description": "Ranked talking points with coverage + debunk, per-point tweet_reply and x_posts; status states staleness/availability."
          }
        }
      }
    },
    "/palestine/stories": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Palestine desk stories: AI-drafted by the autopilot from real archive record clusters (cite AG-* IDs), then approved by a human editor. Drafts awaiting review are not served.",
        "responses": {
          "200": {
            "description": "Approved stories, newest first. `status` is 'available' or 'awaiting_editorial_review'; a count of 0 means nothing has cleared review yet, not that the desk is down. Carries the Asha News AI Agent byline and its `ai_disclosure`."
          }
        }
      }
    },
    "/palestine/stories/{slug}": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "One editor-approved story with body, cited archive record IDs, and its AI disclosure.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Story."
          },
          "404": {
            "description": "Not found, or still awaiting editorial review (`reason`)."
          }
        }
      }
    },
    "/palestine/claims/{slug}": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "One claim rebuttal (curated or autopilot-drafted).",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Claim."
          },
          "404": {
            "description": "Not found."
          }
        }
      }
    },
    "/palestine/autopilot": {
      "get": {
        "tags": [
          "Palestine"
        ],
        "summary": "Palestine autopilot status: last 24/7 cycle (data refreshed, stories/claims generated), interval, store counts.",
        "responses": {
          "200": {
            "description": "Autopilot status."
          }
        }
      }
    }
  }
}
