openapi: 3.0.3
info:
  title: foot.io API
  version: "2"
  description: |
    Public endpoints for the foot.io football database.

    The supported REST surface is an invite-only closed beta. Requests send
    both the public Supabase project `apikey` and an issued `x-api-key`; issued
    keys are stored as SHA-256 hashes and shown once. The project key by itself
    is an unsupported browser-preview path. Postgres RLS restricts canonical
    season-, match-, event-, lineup- and match-stat rows to the current season
    of competitions on the free list — 196 competitions served at least one
    current-season match, measured 6 Aug 2026 — for that preview and for
    issued `free` keys. Keys assigned `basic`, `pro` or `enterprise` read the
    full historical depth of the same licence-gated row set.
    Public catalogue and aggregate coverage resources may remain broader so
    gaps are inspectable. Coverage and provenance remain uneven by era.

    Licence gate (since 2026-07-29): every tier, Enterprise included, reads
    only rows foot.io holds an explicit right to redistribute. Measured
    10 Aug 2026: 1,177,209 matches held, 733,863 (62.3%) readable at every
    tier; 42.9% of held match events and 32.9% of held lineups pass the gate;
    763 of 926 competitions serve at least one match. Not served at any tier:
    transfers (159,823 rows held, zero pass), shot-level data including all
    xG fields, and top-5-league events and lineups. Player market
    values and injuries were withdrawn on 2026-07-28. There is no competition
    gating between paid tiers: the cheapest paid key reads exactly the same
    rows as the most expensive one.

    PostgREST auto-exposes every column on every table — this spec
    only documents the canonical commercial surface.

    Data caveat: a handful of advanced stat columns exist in the schema
    but are effectively empty (0% filled) pending the FBref Big-5
    advanced backfill — on `match_player_stats`: `xag`, `touches`,
    `progressive_passes`, `progressive_carries`; on
    `player_season_stats`: `progressive_passes`, `progressive_carries`,
    `touches`, `shot_creating_actions`, `goal_creating_actions`. They
    remain selectable (so queries won't break) but return NULL. `xg` is
    populated in the held tables from shot-event rollups, but no xG field
    is currently served at any tier — the licence gate withholds all
    shot-level data and xG (measured 6 Aug 2026).
servers:
  - url: https://amidfjrgsztslrpeaiec.supabase.co/rest/v1
    description: Direct PostgREST (beta surface; usage not metered)
  - url: https://amidfjrgsztslrpeaiec.supabase.co/functions/v1/data-api/v1
    description: Metered gateway - same query syntax, atomic rate/quota enforcement, X-RateLimit-* headers
components:
  securitySchemes:
    ProjectKeyAuth:
      type: apiKey
      in: header
      name: apikey
      description: Public Supabase project key published in the quickstart.
    BetaKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Invite-only foot.io beta key; its assigned tier controls depth.
  schemas:
    # ─── Core entities ────────────────────────────────────────────
    Country:
      type: object
      properties:
        id:           { type: integer }
        name:         { type: string }
        iso_alpha2:   { type: string, minLength: 2, maxLength: 2 }
        iso_alpha3:   { type: string, minLength: 3, maxLength: 3 }
        confederation:
          type: string
          enum: [UEFA, CONMEBOL, CONCACAF, CAF, AFC, OFC]
    Competition:
      type: object
      properties:
        id:           { type: integer }
        name:         { type: string }
        short_name:   { type: string }
        country_id:   { type: integer }
        competition_type:
          type: string
          enum: [league, cup, super_cup, international, friendly, qualifier, playoff]
        tier:         { type: integer }
        gender:       { type: string, enum: [male, female] }
        is_active:    { type: boolean }
        logo_url:     { type: string, format: uri, nullable: true }
        external_ids: { $ref: "#/components/schemas/ExternalIds" }
    Season:
      type: object
      properties:
        id:             { type: integer }
        competition_id: { type: integer }
        name:           { type: string }
        start_date:     { type: string, format: date }
        end_date:       { type: string, format: date }
        is_current:     { type: boolean }
        external_ids:   { $ref: "#/components/schemas/ExternalIds" }
    Stadium:
      type: object
      properties:
        id:           { type: integer }
        name:         { type: string }
        city:         { type: string }
        country_id:   { type: integer }
        capacity:     { type: integer }
        latitude:     { type: number, format: float }
        longitude:    { type: number, format: float }
        opened_year:  { type: integer }
        surface:      { type: string }
        external_ids: { $ref: "#/components/schemas/ExternalIds" }
    Team:
      type: object
      properties:
        id:           { type: integer }
        name:         { type: string }
        short_name:   { type: string }
        code:         { type: string, minLength: 3, maxLength: 3 }
        country_id:   { type: integer }
        founded_year: { type: integer }
        stadium_id:   { type: integer, nullable: true }
        logo_url:     { type: string, format: uri, nullable: true }
        is_national_team: { type: boolean }
        external_ids: { $ref: "#/components/schemas/ExternalIds" }
    Player:
      type: object
      properties:
        id:                    { type: integer }
        name:                  { type: string }
        full_name:             { type: string }
        date_of_birth:         { type: string, format: date }
        country_id:            { type: integer }
        position:              { type: string, enum: [GK, DF, MF, FW] }
        detailed_position:     { type: string }
        height_cm:             { type: integer }
        weight_kg:             { type: integer }
        preferred_foot:        { type: string, enum: [left, right, both] }
        photo_url:             { type: string, format: uri, nullable: true }
        is_retired:            { type: boolean }
        external_ids:          { $ref: "#/components/schemas/ExternalIds" }

    # ─── Match + per-match payloads ───────────────────────────────
    Match:
      type: object
      properties:
        id:                    { type: integer }
        season_id:             { type: integer }
        matchday:              { type: integer, nullable: true }
        stage:                 { type: string, nullable: true }
        match_date:            { type: string, format: date, nullable: true,
                                 description: "Known calendar date. Historical sources that publish only a round retain null rather than an invented date." }
        source_occurrence:     { type: string, nullable: true,
                                 description: "Stable publisher occurrence/round discriminator for repeated fixtures whose calendar date is unknown." }
        kick_off:              { type: string, format: time, nullable: true }
        home_team_id:          { type: integer }
        away_team_id:          { type: integer }
        home_score:            { type: integer, nullable: true }
        away_score:            { type: integer, nullable: true }
        home_score_ht:         { type: integer, nullable: true }
        away_score_ht:         { type: integer, nullable: true }
        home_score_aet:        { type: integer, nullable: true }
        away_score_aet:        { type: integer, nullable: true }
        home_penalties:        { type: integer, nullable: true }
        away_penalties:        { type: integer, nullable: true }
        status:                { type: string, enum: [scheduled, live, finished, postponed, cancelled, abandoned] }
        attendance:            { type: integer, nullable: true }
        stadium_id:            { type: integer, nullable: true }
        referee_id:            { type: integer, nullable: true }
        # ─── V2 / TheSportsDB additions ───
        thumb_url:             { type: string, format: uri, nullable: true,
                                 description: "Match thumbnail image (TheSportsDB)" }
        poster_url:            { type: string, format: uri, nullable: true,
                                 description: "Promotional poster art for the match" }
        highlight_video_url:   { type: string, format: uri, nullable: true,
                                 description: "YouTube link to match highlights" }
        external_ids:          { $ref: "#/components/schemas/ExternalIds" }
      required: [id, season_id, home_team_id, away_team_id, status]
    MatchEvent:
      type: object
      description: One row per goal / card / substitution / VAR decision.
      properties:
        id:                  { type: integer }
        match_id:            { type: integer }
        team_id:             { type: integer, nullable: true }
        player_id:           { type: integer, nullable: true }
        related_player_id:   { type: integer, nullable: true,
                               description: "e.g. assist for a goal, off-player for a sub" }
        event_type:
          type: string
          enum: [goal, own_goal, penalty_scored, penalty_missed,
                 yellow_card, red_card, second_yellow, substitution,
                 var_decision, penalty_won, penalty_conceded,
                 goal_disallowed, card_other]
        raw_type:            { type: string, nullable: true,
                               description: "Source-side label preserved verbatim for debugging" }
        minute:              { type: integer }
        added_time:          { type: integer, nullable: true }
        detail:              { type: string, nullable: true }
        is_extra_time:       { type: boolean }
        attribution:         { $ref: '#/components/schemas/Attribution' }
        external_id:         { type: string, nullable: true,
                               description: "Stable source-side id (e.g. TheSportsDB idTimeline)" }
    MatchLineup:
      type: object
      description: One row per player who appeared in a match (starter or sub).
      properties:
        id:               { type: integer }
        match_id:         { type: integer }
        team_id:          { type: integer }
        player_id:        { type: integer }
        is_starter:       { type: boolean }
        shirt_number:     { type: integer, nullable: true }
        position_played:  { type: string, nullable: true,
                            description: "Granular position label (e.g. Left-Back)" }
        minutes_played:   { type: integer, nullable: true }
        rating:           { type: number, format: float, nullable: true }
    MatchTeamStats:
      type: object
      description: Per-side stats for a match. Up to two rows per match (home, away).
      properties:
        match_id:          { type: integer }
        team_id:           { type: integer }
        is_home:           { type: boolean }
        possession_pct:    { type: number, format: float, nullable: true }
        shots:             { type: integer, nullable: true }
        shots_on_target:   { type: integer, nullable: true }
        shots_off_target:  { type: integer, nullable: true }
        shots_blocked:     { type: integer, nullable: true }
        xg:                { type: number, format: float, nullable: true }
        xga:               { type: number, format: float, nullable: true }
        passes_completed:  { type: integer, nullable: true }
        passes_attempted:  { type: integer, nullable: true }
        pass_accuracy_pct: { type: number, format: float, nullable: true }
        fouls:             { type: integer, nullable: true }
        corners:           { type: integer, nullable: true }
        offsides:          { type: integer, nullable: true }
        yellow_cards:      { type: integer, nullable: true }
        red_cards:         { type: integer, nullable: true }
        goalkeeper_saves:  { type: integer, nullable: true }
        attribution:       { $ref: '#/components/schemas/Attribution' }
    MatchBroadcast:
      type: object
      description: Per-country TV / streaming partner for a match.
      properties:
        id:               { type: integer }
        match_id:         { type: integer }
        country_code:     { type: string, minLength: 2, maxLength: 2 }
        country_name:     { type: string, nullable: true }
        channel_name:     { type: string }
        channel_url:      { type: string, format: uri, nullable: true }
        broadcast_type:   { type: string, enum: [tv, streaming, radio, "tv+streaming"] }
        is_subscription:  { type: boolean }
        language:         { type: string, nullable: true }
        starts_at:        { type: string, format: date-time, nullable: true }

    # ─── Standings + season helpers ───────────────────────────────
    Standings:
      type: object
      properties:
        id:                { type: integer }
        season_id:         { type: integer }
        team_id:           { type: integer }
        position:          { type: integer }
        played:            { type: integer }
        won:               { type: integer }
        drawn:             { type: integer }
        lost:              { type: integer }
        goals_for:         { type: integer }
        goals_against:     { type: integer }
        goal_difference:   { type: integer, description: "Generated" }
        points:            { type: integer }
        points_deduction:  { type: integer }
        form:              { type: string, nullable: true }

    # ─── Player history ───────────────────────────────────────────
    PlayerContract:
      type: object
      properties:
        id:             { type: integer }
        player_id:      { type: integer }
        team_id:        { type: integer }
        start_date:     { type: string, format: date, nullable: true }
        end_date:       { type: string, format: date, nullable: true }
        contract_type:  { type: string, enum: [permanent, loan, youth, trial] }
        shirt_number:   { type: integer, nullable: true }
        is_current:     { type: boolean }
        wage:           { type: string, nullable: true }
        external_id:    { type: string, nullable: true }
    PlayerHonour:
      type: object
      properties:
        id:                  { type: integer }
        player_id:           { type: integer }
        team_id:             { type: integer, nullable: true }
        competition_id:      { type: integer, nullable: true }
        season_name:         { type: string, nullable: true }
        honour_name:         { type: string }
        honour_logo_url:     { type: string, format: uri, nullable: true }
        honour_trophy_url:   { type: string, format: uri, nullable: true }
        source:
          type: string
          deprecated: true
          description: >-
            DEPRECATED, and the one place this contract still publishes an
            acquisition label. It is retained only because 16,648 of these rows
            are compiled from Wikipedia under CC BY-SA 4.0, which makes naming
            the source a condition of use, and `player_honours` has no
            dedicated `attribution` field yet. It will be replaced by
            `attribution` once it has one; do not build on this field. Every
            other schema has already dropped it.
        external_id:         { type: string, nullable: true }
    PlayerMilestone:
      type: object
      properties:
        id:                    { type: integer }
        player_id:             { type: integer }
        team_id:               { type: integer, nullable: true }
        competition_id:        { type: integer, nullable: true }
        milestone_name:        { type: string }
        milestone_date:        { type: string, format: date, nullable: true }
        milestone_description: { type: string, nullable: true }
        external_id:           { type: string, nullable: true }

    # ─── Attribution ──────────────────────────────────────────────
    Attribution:
      type: string
      nullable: true
      description: >-
        The credit the licence behind this row obliges you to display when you
        republish it — verbatim, and nothing more.


        NULL means NO ATTRIBUTION IS OWED. That is an established finding, not
        "unknown": the row came from a CC0 or public-domain dedication, which
        waives the attribution right, or from a source that grants no licence
        and therefore imposes no credit condition. You may republish a NULL row
        with no credit at all.


        Where a credit IS present it is a condition of use, so carry it with the
        row. It is denormalised onto every row for exactly that reason: a credit
        you have to remember to join for is a credit you will forget, and a
        missing credit is a licence breach rather than an audit gap. Some rows
        carry the exact marker "Attribution requirements not established." —
        that is a conservative internal state, not a credit; treat it as "ask us
        before republishing this row", not as text to display.


        This field REPLACES the former `source` field, which was removed from
        this contract in July 2026. `source` was an internal acquisition label
        recording where we bought or fetched the row; it is not an attribution,
        its values do not correspond to what any licence requires, and it is no
        longer part of the published API. If you were reading `source` to
        satisfy an attribution obligation, read this instead — it is the field
        that actually answers that question.

    # ─── Trust, completeness and provenance ─────────────────────
    FactTierSummary:
      type: object
      properties:
        domain:  { type: string, enum: [matches, player_season_stats] }
        tier:    { type: string, enum: [corroborated, single_source, flagged] }
        n_facts: { type: integer, format: int64 }
    CompetitionCompleteness:
      type: object
      properties:
        competition_id:         { type: integer }
        competition_name:       { type: string }
        target_status:          { type: string, enum: [unresearched, provisional, verified, not_applicable] }
        expected_seasons:       { type: integer }
        present_seasons:        { type: integer }
        problem_seasons:        { type: integer }
        structural_coverage_pct: { type: number, nullable: true }
        verified_complete:      { type: boolean, description: "The only field that makes a completion claim." }
        completeness_status:    { type: string }
        evidence_url:           { type: string, format: uri, nullable: true }
        evidence_checked_at:    { type: string, format: date, nullable: true }
        measured_at:            { type: string, format: date-time }
    EntityFactVerification:
      type: object
      properties:
        subject_type:            { type: string }
        subject_id:              { type: integer, format: int64 }
        predicate:               { type: string }
        verification_status:     { type: string, enum: [single_source, multi_source_unverified, corroborated, conflict] }
        source_count_status:     { type: string }
        independent_source_count: { type: integer }
        independence_groups:     { type: array, items: { type: string } }
        unassessed_sources:       { type: array, items: { type: string } }
        candidate_values:        { type: array, items: { type: object } }
    EntityFactProvenance:
      type: object
      properties:
        fact_id:                  { type: integer, format: int64 }
        subject_type:             { type: string }
        subject_id:               { type: integer, format: int64 }
        predicate:                { type: string }
        source:                   { type: string }
        effective_source_record_id: { type: string, nullable: true }
        effective_source_url:     { type: string, format: uri, nullable: true }
        source_retrieved_at:      { type: string, format: date-time, nullable: true }
        source_content_sha256:    { type: string, nullable: true }
        provenance_status:        { type: string, enum: [run_and_record, source_record, import_run_only, source_only, unregistered_source, invalid_subject, invalid_object] }
    SourceEntityIdentity:
      type: object
      properties:
        source_id:            { type: integer }
        entity_type:          { type: string }
        source_entity_id:     { type: string }
        canonical_id:         { type: integer, format: int64 }
        verification_status:  { type: string, enum: [verified, provisional, conflict, retired] }
        verification_basis:   { type: string }
        evidence_url:         { type: string, format: uri, nullable: true }
        source_record_id:     { type: string, nullable: true }
        verified_at:          { type: string, format: date-time, nullable: true }
    IngestionGateTotal:
      type: object
      properties:
        surface:   { type: string }
        state:     { type: string }
        row_count: { type: integer, format: int64 }
    AssistDefinition:
      type: object
      description: Versioned semantics; assertion and evidence rows are not on the public surface.
      properties:
        id:                  { type: integer }
        slug:                { type: string }
        version:             { type: string }
        concept:             { type: string, enum: [reported_assist, potential_assist] }
        display_name:        { type: string }
        description:         { type: string }
        rule_spec:           { type: object, additionalProperties: true }
        publisher_source_id: { type: integer, nullable: true }
        reference_url:       { type: string, format: uri, nullable: true }
        effective_from:      { type: string, format: date, nullable: true }
        effective_to:        { type: string, format: date, nullable: true }
        is_active:           { type: boolean }

    # ─── Shared ───────────────────────────────────────────────────
    ExternalIds:
      type: object
      additionalProperties: { type: string }
      description: |
        JSONB bag of source-name → source-id. Keys observed in the wild:
        thesportsdb, wikidata, transfermarkt, fbref, espn, api_football,
        soccerway, footballdatabase, premier_league, eurosport, soccerbase.
        Use these to deep-link to source pages or cross-reference other
        importers.
      example:
        thesportsdb: "34160998"
        wikidata:    "Q21621029"
        transfermarkt: "316125"
        fbref:       "d2424d1b"
        espn:        "225426"
        api_football: "2933"
paths:
  /players:
    get:
      summary: Query canonical players
      parameters:
        - { name: id, in: query, schema: { type: string }, description: "PostgREST filter, e.g. eq.7423" }
        - { name: select, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Player rows visible to the caller
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Player" } }
  /teams:
    get:
      summary: Query non-quarantined canonical teams
      parameters:
        - { name: id, in: query, schema: { type: string } }
        - { name: select, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Team rows visible to the caller
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Team" } }
  /competitions:
    get:
      summary: Query non-quarantined canonical competitions
      parameters:
        - { name: id, in: query, schema: { type: string } }
        - { name: select, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Competition rows visible to the caller
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Competition" } }
  /seasons:
    get:
      summary: Query canonical seasons within the caller's content tier
      parameters:
        - { name: competition_id, in: query, schema: { type: string } }
        - { name: select, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Season rows visible to the caller
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Season" } }
  /matches:
    get:
      summary: Query canonical matches within the caller's content tier
      parameters:
        - { name: id, in: query, schema: { type: string } }
        - { name: season_id, in: query, schema: { type: string } }
        - { name: match_date, in: query, schema: { type: string } }
        - { name: select, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Match rows visible to the caller
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Match" } }
  /fact_tier_summary:
    get:
      summary: Publisher-aware confidence totals
      description: Corroborated requires at least two assessed publisher-independence groups.
      responses:
        "200":
          description: Exact live counts by domain and tier
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/FactTierSummary" } }
  /public_competition_completeness:
    get:
      summary: Coherent measured historical-season completeness
      parameters:
        - { name: competition_id, in: query, schema: { type: string }, description: "e.g. eq.1" }
        - { name: verified_complete, in: query, schema: { type: string }, description: "The only completion claim; e.g. eq.true" }
      responses:
        "200":
          description: One snapshot row per public competition, including unresearched rows
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/CompetitionCompleteness" } }
  /competition_research_queue:
    get:
      summary: Prioritised evidence research for every unresearched competition
      description: Candidate inception metadata is a research lead only; it does not establish a coverage contract.
      parameters:
        - { name: research_rank, in: query, schema: { type: string } }
        - { name: competition_id, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Unresearched competitions ordered by indexed-match impact
          content:
            application/json:
              schema: { type: array, items: { type: object, additionalProperties: true } }
  /coverage_gap_remediation_queue:
    get:
      summary: Missing and impaired contract years routed to remediation lanes
      description: Includes the latest exact publisher/year attempt; an unavailable outcome never removes the gap from the denominator.
      parameters:
        - { name: remediation_rank, in: query, schema: { type: string } }
        - { name: target_status, in: query, schema: { type: string } }
        - { name: remediation_lane, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: One executable queue row per missing, empty, scoreless or multi-edition contract year
          content:
            application/json:
              schema: { type: array, items: { type: object, additionalProperties: true } }
  /match_source_acquisition_summary:
    get:
      summary: Complete, actionable and deferred second-publisher denominators
      description: Exact assertion totals include same-publisher provenance, while verified match totals require a different assessed publisher group. A deferred source attempt remains in the complete single-source backlog and is not a verification claim.
      responses:
        "200":
          description: Cached exact totals for acquired assertions and remaining source work
          content:
            application/json:
              schema: { type: array, items: { type: object, additionalProperties: true } }
  /competition_classification_completeness:
    get:
      summary: Evidence-aware competition classification queue
      parameters:
        - { name: competition_id, in: query, schema: { type: string } }
        - { name: classification_status, in: query, schema: { type: string } }
      responses:
        "200":
          description: Missing values, missing evidence and source conflicts remain explicit
          content:
            application/json:
              schema: { type: array, items: { type: object, additionalProperties: true } }
  /competition_season_fixture_contract_queue:
    get:
      summary: Exact fixture-count contracts for every stored public season
      parameters:
        - { name: competition_id, in: query, schema: { type: string } }
        - { name: contract_status, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, maximum: 1000 } }
      responses:
        "200":
          description: Source-backed counts or an explicit unresearched state
          content:
            application/json:
              schema: { type: array, items: { type: object, additionalProperties: true } }
  /entity_fact_verification:
    get:
      summary: Field-level publisher-independent verification
      parameters:
        - { name: subject_type, in: query, schema: { type: string }, description: "e.g. eq.match" }
        - { name: subject_id, in: query, schema: { type: string }, description: "e.g. eq.5074" }
        - { name: predicate, in: query, schema: { type: string } }
      responses:
        "200":
          description: Candidate values with corroborated, single-source, multi-source-unverified or conflict status
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/EntityFactVerification" } }
  /assist_definitions:
    get:
      summary: Query active versioned assist semantics
      description: Publisher reports, potential assists and rule-derived candidates are not treated as equivalent facts.
      parameters:
        - { name: slug, in: query, schema: { type: string } }
        - { name: version, in: query, schema: { type: string } }
        - { name: concept, in: query, schema: { type: string } }
        - { name: select, in: query, schema: { type: string } }
      responses:
        "200":
          description: Active assist definitions visible to the caller
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/AssistDefinition" } }
  /entity_fact_provenance:
    get:
      summary: Record-level evidence for canonical assertions
      parameters:
        - { name: subject_type, in: query, schema: { type: string } }
        - { name: subject_id, in: query, schema: { type: string } }
        - { name: predicate, in: query, schema: { type: string } }
      responses:
        "200":
          description: Registered source, record/page identity, retrieval time and digest where available
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/EntityFactProvenance" } }
  /source_entity_identities:
    get:
      summary: Verified publisher-to-canonical identity mappings
      parameters:
        - { name: entity_type, in: query, schema: { type: string } }
        - { name: canonical_id, in: query, schema: { type: string } }
        - { name: verification_status, in: query, schema: { type: string } }
      responses:
        "200":
          description: Uniqueness-checked source identities
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/SourceEntityIdentity" } }
  /ingestion_gate_totals:
    get:
      summary: Safe aggregate of staged and quarantined rows
      description: Raw source payloads remain private; this endpoint makes the repair backlog visible.
      responses:
        "200":
          description: Counts by gate surface and state
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/IngestionGateTotal" } }

security:
  - ProjectKeyAuth: []
    BetaKeyAuth: []
