openapi: 3.1.0
info:
  title: VS Engine API
  description: >-
    Central Stats & AI Content Motor powering Insideformation client products. Sources: Kambi odds + betting patterns,
    ValueStats insights, SportRadar match data. Data policy: **never invent data** — missing values become `null` +
    `blocked: true`, frontends hide blocked widgets. See /docs/concepts/data-policy.md.
  version: 1.0.0
  contact:
    name: Insideformation — Johan Bryld
    email: johan.f.bryld@gmail.com
  license:
    name: Proprietary — all rights reserved
    url: https://insideformation.com
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        Per-client API key delivered out-of-band by Insideformation. Required on every protected endpoint; open
        endpoints (/status, /widgets) ignore it but still accept it.
  schemas:
    ErrorResponse:
      type: object
      required:
        - error
      additionalProperties: true
      properties:
        error:
          type: string
          description: Short machine-friendly error code
        message:
          type: string
          description: Human-readable error detail
    MatchCard:
      type: object
      additionalProperties: true
      properties:
        eventId:
          type: string
          description: Kambi event ID
        homeTeam:
          type: object
          properties:
            name:
              type: string
            badge:
              type: string
            form:
              type: array
              items:
                type: string
        awayTeam:
          type: object
          properties:
            name:
              type: string
            badge:
              type: string
            form:
              type: array
              items:
                type: string
        league:
          type: object
          properties:
            id:
              type: string
              description: Stable league key, e.g. "premier_league"
            name:
              type: string
              description: Human-readable league name from Kambi
        kickoff:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - scheduled
            - live
            - finished
        odds:
          type: object
          nullable: true
          description: 1X2 + O/U 2.5 + BTTS summary odds from Kambi
          additionalProperties: true
        asianHandicap:
          type: object
          nullable: true
          additionalProperties: true
        marketCount:
          type: integer
        mostBacked:
          type: object
          nullable: true
          additionalProperties: true
      title: MatchCard
    SportEventMapping:
      type: object
      required:
        - kambiEventId
        - sportEventUrn
        - homeTeamUrn
        - awayTeamUrn
        - homeTeamName
        - awayTeamName
        - kickoff
        - league
      properties:
        kambiEventId:
          type: string
          description: Numeric Kambi event id, rendered as a string to avoid JS integer truncation
          example: '1024044217'
        sportEventUrn:
          type: string
          description: Canonical SportRadar sport_event URN, of the form sr:sport_event:<id>
          example: sr:sport_event:61301151
        homeTeamUrn:
          type: string
          description: SR competitor URN for the home team
          example: sr:competitor:44
        awayTeamUrn:
          type: string
          description: SR competitor URN for the away team
          example: sr:competitor:43
        homeTeamName:
          type: string
          example: Liverpool
        awayTeamName:
          type: string
          example: Fulham
        kickoff:
          type: string
          format: date-time
          description: Scheduled kickoff in ISO 8601 UTC
          example: '2026-04-11T16:30:00Z'
        league:
          type: string
          description: Human-readable league name as supplied by Kambi
          example: Premier League
    MappingOkResponse:
      type: object
      required:
        - status
        - data
      properties:
        status:
          type: string
          enum:
            - ok
          description: Literal "ok" on success
        data:
          $ref: '#/components/schemas/def-1'
    LiveRailResponse:
      type: object
      required:
        - matches
        - meta
      properties:
        matches:
          type: array
          description: Up to `limit` currently-live matches in Tier A+B, sorted tier → clock desc → league id.
          items:
            type: object
            required:
              - eventId
              - league
              - homeTeam
              - awayTeam
              - state
              - liveScore
              - liveMinute
              - odds
              - fetchedAt
              - provenance
            properties:
              eventId:
                type: integer
                description: Kambi numeric event id
                example: 1024182402
              league:
                type: object
                required:
                  - id
                  - name
                  - slug
                properties:
                  id:
                    type: string
                    example: bundesliga
                  name:
                    type: string
                    example: Bundesliga
                  slug:
                    type: string
                    example: bundesliga
                  countryCode:
                    type: string
                    nullable: true
                    description: ISO 3166-1 alpha-2 country code, or null for international competitions
                    example: DE
              homeTeam:
                type: object
                required:
                  - name
                properties:
                  name:
                    type: string
                    example: VfL Wolfsburg
                  shortName:
                    type: string
                    nullable: true
                  badge:
                    type: string
                    nullable: true
              awayTeam:
                type: object
                required:
                  - name
                properties:
                  name:
                    type: string
                    example: Eintracht Frankfurt
                  shortName:
                    type: string
                    nullable: true
                  badge:
                    type: string
                    nullable: true
              state:
                type: string
                enum:
                  - LIVE
                description: Always "LIVE" for entries in this endpoint
              liveScore:
                type: object
                required:
                  - home
                  - away
                properties:
                  home:
                    type: integer
                    example: 0
                  away:
                    type: integer
                    example: 2
              liveMinute:
                type: object
                required:
                  - display
                properties:
                  value:
                    type: integer
                    nullable: true
                    description: Whole minutes parsed from SR clock; null for HT/FT/unparseable
                  display:
                    type: string
                    description: 'Render verbatim. Examples: "85''", "45+2''", "HT", "FT"'
                    example: 85'
              period:
                type: string
                nullable: true
                description: >-
                  Coarse period label derived from SR match_status. One of: 1H, 2H, HT, FT, ET, PEN — or null when
                  unknown.
                example: 2H
              odds:
                type: object
                required:
                  - match1x2
                properties:
                  match1x2:
                    type: object
                    required:
                      - home
                      - draw
                      - away
                    description: Kambi milliodds (decimal × 1000). e.g. 1580 = 1.58
                    properties:
                      home:
                        type: integer
                        example: 476000
                      draw:
                        type: integer
                        example: 26000
                      away:
                        type: integer
                        example: 1010
              fetchedAt:
                type: string
                format: date-time
                description: ISO 8601 — freshness of the Kambi odds snapshot for this match
              provenance:
                type: object
                required:
                  - origin
                  - algorithm
                  - generatedAt
                description: Standard VS Engine provenance block — Kambi + SR origins + algorithm
                properties:
                  origin:
                    type: array
                    items:
                      type: object
                      required:
                        - type
                        - provider
                      properties:
                        type:
                          type: string
                          enum:
                            - api
                            - file
                            - derived
                        provider:
                          type: string
                        endpoint:
                          type: string
                        urn:
                          type: string
                          nullable: true
                        field:
                          type: string
                        note:
                          type: string
                      additionalProperties: true
                  algorithm:
                    type: object
                    required:
                      - description
                      - file
                    properties:
                      description:
                        type: string
                      file:
                        type: string
                      parameters:
                        type: object
                        additionalProperties: true
                  generatedAt:
                    type: string
                    format: date-time
        meta:
          type: object
          required:
            - total
            - limit
            - sport
            - majorLeaguesOnly
            - refreshedAt
          properties:
            total:
              type: integer
              description: Number of matches returned. Can be less than `limit`; 0 when no live matches match the filter.
              example: 8
            limit:
              type: integer
              description: The effective cap applied to the response (1-20, default 10)
              example: 10
            sport:
              type: string
              description: Echo of the `sport` query param. Only `football` returns non-empty results.
              example: football
            majorLeaguesOnly:
              type: boolean
              description: Always `true` at launch — only Tier A+B leagues are considered.
              enum:
                - true
            refreshedAt:
              type: string
              format: date-time
              description: ISO 8601 — when this aggregator run happened
    MinedPattern:
      type: object
      required:
        - conditionCategory
        - conditionKey
        - conditionParams
        - outcomeKey
        - hits
        - sampleSize
        - hitRate
        - baseline
        - surpriseDelta
        - pValueRaw
        - pValueCorrected
        - surpriseScore
        - rank
        - appliesToday
      properties:
        conditionCategory:
          type: string
          enum:
            - h2h
            - form
            - trajectory
            - player
            - travel
            - season_phase
            - international_break
          description: Broad bucket — drives which rendering template the shape layer picks.
        conditionKey:
          type: string
          description: Stable machine key for i18n template lookup, e.g. "h2h_dominance" or "player_starts".
          example: h2h_dominance
        conditionParams:
          type: object
          additionalProperties: true
          description: Structured params for template substitution (team names, distances, player names, etc.).
        outcomeKey:
          type: string
          description: Stable outcome key, e.g. "home_win", "late_winner_after_80", "btts".
          example: home_win
        hits:
          type: integer
          description: Successes observed (out of sampleSize).
        sampleSize:
          type: integer
          description: Total trials.
        hitRate:
          type: number
          description: hits / sampleSize, in [0, 1].
        baseline:
          type: number
          description: Population baseline rate for that outcome, in [0, 1].
        surpriseDelta:
          type: number
          description: hitRate - baseline (signed). |delta| >= 0.20 to surface.
        pValueRaw:
          type: number
          description: One-sided binomial p-value before correction.
        pValueCorrected:
          type: number
          description: Bonferroni-adjusted p-value, capped at 1.0. Required <= 0.05.
        surpriseScore:
          type: number
          description: '|delta| * sqrt(n) * log(n). Used for ranking.'
        rank:
          type: integer
          description: 1 = most surprising; assigned post-sort.
        appliesToday:
          type: boolean
          description: True when the pattern's conditions hold for today's specific matchup (e.g. player is in starting XI).
        subjectTeam:
          type: string
          enum:
            - home
            - away
          description: Which team the pattern applies to, when team-specific.
        textTemplate:
          type: string
          description: Optional rendering hint for the shape layer. Not a finished string — i18n owns the final copy.
    CachedInsights:
      type: object
      required:
        - generatedAt
        - eventId
        - totalTestsRun
        - patternsAfterSignificance
        - patternsAfterDedup
        - patterns
      properties:
        generatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when this mining run completed.
        eventId:
          type: string
          description: Kambi numeric event id (string-encoded) for the upcoming match.
          example: '1026050829'
        totalTestsRun:
          type: integer
          description: How many candidate patterns were statistically tested in this run.
        patternsAfterSignificance:
          type: integer
          description: Survived Bonferroni-corrected p < 0.05.
        patternsAfterDedup:
          type: integer
          description: Survived dedup against ValueStats — these are the patterns surfaced.
        patterns:
          type: array
          description: Up to top-25 mined patterns ranked by surpriseScore desc.
          items:
            $ref: '#/components/schemas/def-4'
    TeamAggregates:
      type: object
      required:
        - teamId
        - teamName
        - season
        - computedAt
        - matchesAnalyzed
      properties:
        teamId:
          type: string
          example: sr:competitor:1764
        teamName:
          type: string
          example: AIK
        season:
          type: string
          nullable: true
          example: sr:season:138196
        computedAt:
          type: string
          format: date-time
        matchesAnalyzed:
          type: integer
          example: 5
        corners:
          type: object
          properties:
            for_per_match:
              type: number
              nullable: true
            against_per_match:
              type: number
              nullable: true
            home_for_per_match:
              type: number
              nullable: true
            away_for_per_match:
              type: number
              nullable: true
            home_against_per_match:
              type: number
              nullable: true
            away_against_per_match:
              type: number
              nullable: true
        cards:
          type: object
          properties:
            yellow_for_per_match:
              type: number
              nullable: true
            yellow_against_per_match:
              type: number
              nullable: true
            red_for_per_match:
              type: number
              nullable: true
            red_against_per_match:
              type: number
              nullable: true
            yellow_when_leading_per_match:
              type: number
              nullable: true
            yellow_when_trailing_per_match:
              type: number
              nullable: true
        fouls:
          type: object
          properties:
            committed_per_match:
              type: number
              nullable: true
            drawn_per_match:
              type: number
              nullable: true
        free_kicks:
          type: object
          properties:
            won_per_match:
              type: number
              nullable: true
            conceded_per_match:
              type: number
              nullable: true
        penalties:
          type: object
          properties:
            for:
              type: integer
              nullable: true
            against:
              type: integer
              nullable: true
        goals:
          type: object
          properties:
            scored_per_match:
              type: number
              nullable: true
            conceded_per_match:
              type: number
              nullable: true
            by_interval:
              type: object
              description: Goals scored / conceded per 15-min interval. All nested values null when timeline missing.
              additionalProperties: true
            first_half_pct:
              type: number
              nullable: true
            second_half_pct:
              type: number
              nullable: true
            set_piece_goals:
              type: integer
              nullable: true
            open_play_goals:
              type: integer
              nullable: true
            penalty_goals:
              type: integer
              nullable: true
            late_winner_count:
              type: integer
              nullable: true
        match_state:
          type: object
          properties:
            win_pct_when_leading_at_ht:
              type: number
              nullable: true
            comeback_win_count:
              type: integer
              nullable: true
            clean_sheet_pct:
              type: number
              nullable: true
            clean_sheet_home_pct:
              type: number
              nullable: true
            clean_sheet_away_pct:
              type: number
              nullable: true
            btts_pct:
              type: number
              nullable: true
        form_context:
          type: object
          properties:
            rest_days_before_last_match:
              type: number
              nullable: true
            matches_in_last_30_days:
              type: integer
            win_pct_with_short_rest_lt_4d:
              type: number
              nullable: true
            win_pct_with_long_rest_gt_7d:
              type: number
              nullable: true
        source:
          type: string
    PlayerAggregates:
      type: object
      required:
        - playerId
        - playerName
        - teamId
        - season
        - computedAt
        - matches_played
      properties:
        playerId:
          type: string
          example: sr:player:869030
        playerName:
          type: string
          example: Olusanya, Bénie
        teamId:
          type: string
          example: sr:competitor:1759
        season:
          type: string
          nullable: true
          example: sr:season:138196
        computedAt:
          type: string
          format: date-time
        matches_played:
          type: integer
        minutes_total:
          type: integer
          nullable: true
        starts:
          type: integer
        subbed_on:
          type: integer
        subbed_off:
          type: integer
        attacking_per_90:
          type: object
          properties:
            goals:
              type: number
              nullable: true
            assists:
              type: number
              nullable: true
            shots:
              type: number
              nullable: true
            shots_on_target:
              type: number
              nullable: true
            key_passes:
              type: number
              nullable: true
        attacking_totals:
          type: object
          properties:
            goals:
              type: integer
            assists:
              type: integer
            shots:
              type: integer
            shots_on_target:
              type: integer
        efficiency:
          type: object
          properties:
            goals_per_shot:
              type: number
              nullable: true
            shots_on_target_pct:
              type: number
              nullable: true
        defensive_per_90:
          type: object
          properties:
            tackles:
              type: number
              nullable: true
            interceptions:
              type: number
              nullable: true
            fouls_committed:
              type: number
              nullable: true
            fouls_drawn:
              type: number
              nullable: true
        discipline:
          type: object
          properties:
            yellow_cards:
              type: integer
            red_cards:
              type: integer
            yellow_per_match:
              type: number
              nullable: true
            suspension_watch:
              type: boolean
        form:
          type: object
          properties:
            goals_last_5:
              type: integer
            assists_last_5:
              type: integer
            shots_last_5:
              type: integer
            scoring_streak_matches:
              type: integer
        source:
          type: string
    def-0:
      type: object
      required:
        - error
      additionalProperties: true
      properties:
        error:
          type: string
          description: Short machine-friendly error code
        message:
          type: string
          description: Human-readable error detail
      title: ErrorResponse
    def-1:
      type: object
      required:
        - kambiEventId
        - sportEventUrn
        - homeTeamUrn
        - awayTeamUrn
        - homeTeamName
        - awayTeamName
        - kickoff
        - league
      properties:
        kambiEventId:
          type: string
          description: Numeric Kambi event id, rendered as a string to avoid JS integer truncation
          example: '1024044217'
        sportEventUrn:
          type: string
          description: Canonical SportRadar sport_event URN, of the form sr:sport_event:<id>
          example: sr:sport_event:61301151
        homeTeamUrn:
          type: string
          description: SR competitor URN for the home team
          example: sr:competitor:44
        awayTeamUrn:
          type: string
          description: SR competitor URN for the away team
          example: sr:competitor:43
        homeTeamName:
          type: string
          example: Liverpool
        awayTeamName:
          type: string
          example: Fulham
        kickoff:
          type: string
          format: date-time
          description: Scheduled kickoff in ISO 8601 UTC
          example: '2026-04-11T16:30:00Z'
        league:
          type: string
          description: Human-readable league name as supplied by Kambi
          example: Premier League
      title: SportEventMapping
    def-2:
      type: object
      required:
        - status
        - data
      properties:
        status:
          type: string
          enum:
            - ok
          description: Literal "ok" on success
        data:
          $ref: '#/components/schemas/def-1'
      title: MappingOkResponse
    def-3:
      type: object
      required:
        - matches
        - meta
      properties:
        matches:
          type: array
          description: Up to `limit` currently-live matches in Tier A+B, sorted tier → clock desc → league id.
          items:
            type: object
            required:
              - eventId
              - league
              - homeTeam
              - awayTeam
              - state
              - liveScore
              - liveMinute
              - odds
              - fetchedAt
              - provenance
            properties:
              eventId:
                type: integer
                description: Kambi numeric event id
                example: 1024182402
              league:
                type: object
                required:
                  - id
                  - name
                  - slug
                properties:
                  id:
                    type: string
                    example: bundesliga
                  name:
                    type: string
                    example: Bundesliga
                  slug:
                    type: string
                    example: bundesliga
                  countryCode:
                    type: string
                    nullable: true
                    description: ISO 3166-1 alpha-2 country code, or null for international competitions
                    example: DE
              homeTeam:
                type: object
                required:
                  - name
                properties:
                  name:
                    type: string
                    example: VfL Wolfsburg
                  shortName:
                    type: string
                    nullable: true
                  badge:
                    type: string
                    nullable: true
              awayTeam:
                type: object
                required:
                  - name
                properties:
                  name:
                    type: string
                    example: Eintracht Frankfurt
                  shortName:
                    type: string
                    nullable: true
                  badge:
                    type: string
                    nullable: true
              state:
                type: string
                enum:
                  - LIVE
                description: Always "LIVE" for entries in this endpoint
              liveScore:
                type: object
                required:
                  - home
                  - away
                properties:
                  home:
                    type: integer
                    example: 0
                  away:
                    type: integer
                    example: 2
              liveMinute:
                type: object
                required:
                  - display
                properties:
                  value:
                    type: integer
                    nullable: true
                    description: Whole minutes parsed from SR clock; null for HT/FT/unparseable
                  display:
                    type: string
                    description: 'Render verbatim. Examples: "85''", "45+2''", "HT", "FT"'
                    example: 85'
              period:
                type: string
                nullable: true
                description: >-
                  Coarse period label derived from SR match_status. One of: 1H, 2H, HT, FT, ET, PEN — or null when
                  unknown.
                example: 2H
              odds:
                type: object
                required:
                  - match1x2
                properties:
                  match1x2:
                    type: object
                    required:
                      - home
                      - draw
                      - away
                    description: Kambi milliodds (decimal × 1000). e.g. 1580 = 1.58
                    properties:
                      home:
                        type: integer
                        example: 476000
                      draw:
                        type: integer
                        example: 26000
                      away:
                        type: integer
                        example: 1010
              fetchedAt:
                type: string
                format: date-time
                description: ISO 8601 — freshness of the Kambi odds snapshot for this match
              provenance:
                type: object
                required:
                  - origin
                  - algorithm
                  - generatedAt
                description: Standard VS Engine provenance block — Kambi + SR origins + algorithm
                properties:
                  origin:
                    type: array
                    items:
                      type: object
                      required:
                        - type
                        - provider
                      properties:
                        type:
                          type: string
                          enum:
                            - api
                            - file
                            - derived
                        provider:
                          type: string
                        endpoint:
                          type: string
                        urn:
                          type: string
                          nullable: true
                        field:
                          type: string
                        note:
                          type: string
                      additionalProperties: true
                  algorithm:
                    type: object
                    required:
                      - description
                      - file
                    properties:
                      description:
                        type: string
                      file:
                        type: string
                      parameters:
                        type: object
                        additionalProperties: true
                  generatedAt:
                    type: string
                    format: date-time
        meta:
          type: object
          required:
            - total
            - limit
            - sport
            - majorLeaguesOnly
            - refreshedAt
          properties:
            total:
              type: integer
              description: Number of matches returned. Can be less than `limit`; 0 when no live matches match the filter.
              example: 8
            limit:
              type: integer
              description: The effective cap applied to the response (1-20, default 10)
              example: 10
            sport:
              type: string
              description: Echo of the `sport` query param. Only `football` returns non-empty results.
              example: football
            majorLeaguesOnly:
              type: boolean
              description: Always `true` at launch — only Tier A+B leagues are considered.
              enum:
                - true
            refreshedAt:
              type: string
              format: date-time
              description: ISO 8601 — when this aggregator run happened
      title: LiveRailResponse
    def-4:
      type: object
      required:
        - conditionCategory
        - conditionKey
        - conditionParams
        - outcomeKey
        - hits
        - sampleSize
        - hitRate
        - baseline
        - surpriseDelta
        - pValueRaw
        - pValueCorrected
        - surpriseScore
        - rank
        - appliesToday
      properties:
        conditionCategory:
          type: string
          enum:
            - h2h
            - form
            - trajectory
            - player
            - travel
            - season_phase
            - international_break
          description: Broad bucket — drives which rendering template the shape layer picks.
        conditionKey:
          type: string
          description: Stable machine key for i18n template lookup, e.g. "h2h_dominance" or "player_starts".
          example: h2h_dominance
        conditionParams:
          type: object
          additionalProperties: true
          description: Structured params for template substitution (team names, distances, player names, etc.).
        outcomeKey:
          type: string
          description: Stable outcome key, e.g. "home_win", "late_winner_after_80", "btts".
          example: home_win
        hits:
          type: integer
          description: Successes observed (out of sampleSize).
        sampleSize:
          type: integer
          description: Total trials.
        hitRate:
          type: number
          description: hits / sampleSize, in [0, 1].
        baseline:
          type: number
          description: Population baseline rate for that outcome, in [0, 1].
        surpriseDelta:
          type: number
          description: hitRate - baseline (signed). |delta| >= 0.20 to surface.
        pValueRaw:
          type: number
          description: One-sided binomial p-value before correction.
        pValueCorrected:
          type: number
          description: Bonferroni-adjusted p-value, capped at 1.0. Required <= 0.05.
        surpriseScore:
          type: number
          description: '|delta| * sqrt(n) * log(n). Used for ranking.'
        rank:
          type: integer
          description: 1 = most surprising; assigned post-sort.
        appliesToday:
          type: boolean
          description: True when the pattern's conditions hold for today's specific matchup (e.g. player is in starting XI).
        subjectTeam:
          type: string
          enum:
            - home
            - away
          description: Which team the pattern applies to, when team-specific.
        textTemplate:
          type: string
          description: Optional rendering hint for the shape layer. Not a finished string — i18n owns the final copy.
      title: MinedPattern
    def-5:
      type: object
      required:
        - generatedAt
        - eventId
        - totalTestsRun
        - patternsAfterSignificance
        - patternsAfterDedup
        - patterns
      properties:
        generatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when this mining run completed.
        eventId:
          type: string
          description: Kambi numeric event id (string-encoded) for the upcoming match.
          example: '1026050829'
        totalTestsRun:
          type: integer
          description: How many candidate patterns were statistically tested in this run.
        patternsAfterSignificance:
          type: integer
          description: Survived Bonferroni-corrected p < 0.05.
        patternsAfterDedup:
          type: integer
          description: Survived dedup against ValueStats — these are the patterns surfaced.
        patterns:
          type: array
          description: Up to top-25 mined patterns ranked by surpriseScore desc.
          items:
            $ref: '#/components/schemas/def-4'
      title: CachedInsights
    def-6:
      type: object
      required:
        - teamId
        - teamName
        - season
        - computedAt
        - matchesAnalyzed
      properties:
        teamId:
          type: string
          example: sr:competitor:1764
        teamName:
          type: string
          example: AIK
        season:
          type: string
          nullable: true
          example: sr:season:138196
        computedAt:
          type: string
          format: date-time
        matchesAnalyzed:
          type: integer
          example: 5
        corners:
          type: object
          properties:
            for_per_match:
              type: number
              nullable: true
            against_per_match:
              type: number
              nullable: true
            home_for_per_match:
              type: number
              nullable: true
            away_for_per_match:
              type: number
              nullable: true
            home_against_per_match:
              type: number
              nullable: true
            away_against_per_match:
              type: number
              nullable: true
        cards:
          type: object
          properties:
            yellow_for_per_match:
              type: number
              nullable: true
            yellow_against_per_match:
              type: number
              nullable: true
            red_for_per_match:
              type: number
              nullable: true
            red_against_per_match:
              type: number
              nullable: true
            yellow_when_leading_per_match:
              type: number
              nullable: true
            yellow_when_trailing_per_match:
              type: number
              nullable: true
        fouls:
          type: object
          properties:
            committed_per_match:
              type: number
              nullable: true
            drawn_per_match:
              type: number
              nullable: true
        free_kicks:
          type: object
          properties:
            won_per_match:
              type: number
              nullable: true
            conceded_per_match:
              type: number
              nullable: true
        penalties:
          type: object
          properties:
            for:
              type: integer
              nullable: true
            against:
              type: integer
              nullable: true
        goals:
          type: object
          properties:
            scored_per_match:
              type: number
              nullable: true
            conceded_per_match:
              type: number
              nullable: true
            by_interval:
              type: object
              description: Goals scored / conceded per 15-min interval. All nested values null when timeline missing.
              additionalProperties: true
            first_half_pct:
              type: number
              nullable: true
            second_half_pct:
              type: number
              nullable: true
            set_piece_goals:
              type: integer
              nullable: true
            open_play_goals:
              type: integer
              nullable: true
            penalty_goals:
              type: integer
              nullable: true
            late_winner_count:
              type: integer
              nullable: true
        match_state:
          type: object
          properties:
            win_pct_when_leading_at_ht:
              type: number
              nullable: true
            comeback_win_count:
              type: integer
              nullable: true
            clean_sheet_pct:
              type: number
              nullable: true
            clean_sheet_home_pct:
              type: number
              nullable: true
            clean_sheet_away_pct:
              type: number
              nullable: true
            btts_pct:
              type: number
              nullable: true
        form_context:
          type: object
          properties:
            rest_days_before_last_match:
              type: number
              nullable: true
            matches_in_last_30_days:
              type: integer
            win_pct_with_short_rest_lt_4d:
              type: number
              nullable: true
            win_pct_with_long_rest_gt_7d:
              type: number
              nullable: true
        source:
          type: string
      title: TeamAggregates
    def-7:
      type: object
      required:
        - playerId
        - playerName
        - teamId
        - season
        - computedAt
        - matches_played
      properties:
        playerId:
          type: string
          example: sr:player:869030
        playerName:
          type: string
          example: Olusanya, Bénie
        teamId:
          type: string
          example: sr:competitor:1759
        season:
          type: string
          nullable: true
          example: sr:season:138196
        computedAt:
          type: string
          format: date-time
        matches_played:
          type: integer
        minutes_total:
          type: integer
          nullable: true
        starts:
          type: integer
        subbed_on:
          type: integer
        subbed_off:
          type: integer
        attacking_per_90:
          type: object
          properties:
            goals:
              type: number
              nullable: true
            assists:
              type: number
              nullable: true
            shots:
              type: number
              nullable: true
            shots_on_target:
              type: number
              nullable: true
            key_passes:
              type: number
              nullable: true
        attacking_totals:
          type: object
          properties:
            goals:
              type: integer
            assists:
              type: integer
            shots:
              type: integer
            shots_on_target:
              type: integer
        efficiency:
          type: object
          properties:
            goals_per_shot:
              type: number
              nullable: true
            shots_on_target_pct:
              type: number
              nullable: true
        defensive_per_90:
          type: object
          properties:
            tackles:
              type: number
              nullable: true
            interceptions:
              type: number
              nullable: true
            fouls_committed:
              type: number
              nullable: true
            fouls_drawn:
              type: number
              nullable: true
        discipline:
          type: object
          properties:
            yellow_cards:
              type: integer
            red_cards:
              type: integer
            yellow_per_match:
              type: number
              nullable: true
            suspension_watch:
              type: boolean
        form:
          type: object
          properties:
            goals_last_5:
              type: integer
            assists_last_5:
              type: integer
            shots_last_5:
              type: integer
            scoring_streak_matches:
              type: integer
        source:
          type: string
      title: PlayerAggregates
paths:
  /feed/{clientId}:
    get:
      summary: GET /dataengine/feed/:clientId
      tags:
        - lobby
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /feed/{clientId}/poll:
    get:
      summary: GET /dataengine/feed/:clientId/poll
      tags:
        - lobby
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /feed/{clientId}/match/{eventId}:
    get:
      summary: GET /dataengine/feed/:clientId/match/:eventId
      tags:
        - match-detail
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
        - schema:
            type: string
          in: path
          name: eventId
          required: true
      responses:
        '200':
          description: Default Response
  /feed/{clientId}/refresh:
    post:
      summary: POST /dataengine/feed/:clientId/refresh
      tags:
        - lobby
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /articles/generate:
    post:
      summary: POST /dataengine/articles/generate
      tags:
        - content
      responses:
        '200':
          description: Default Response
  /video/generate:
    post:
      summary: POST /dataengine/video/generate
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /video/{videoId}:
    get:
      summary: GET /dataengine/video/:videoId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: videoId
          required: true
      responses:
        '200':
          description: Default Response
  /video/by-match/{eventId}:
    get:
      summary: GET /dataengine/video/by-match/:eventId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: eventId
          required: true
      responses:
        '200':
          description: Default Response
  /pipeline/run:
    post:
      summary: POST /dataengine/pipeline/run
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /recipes:
    get:
      summary: GET /dataengine/recipes
      tags:
        - content
      responses:
        '200':
          description: Default Response
  /recipes/reload:
    post:
      summary: POST /dataengine/recipes/reload
      tags:
        - content
      responses:
        '200':
          description: Default Response
  /auto-articles/status:
    get:
      summary: GET /dataengine/auto-articles/status
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /auto-articles/run:
    post:
      summary: POST /dataengine/auto-articles/run
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /auto-videos/status:
    get:
      summary: GET /dataengine/auto-videos/status
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /auto-videos/run:
    post:
      summary: POST /dataengine/auto-videos/run
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /pipeline/match/{eventId}:
    get:
      summary: Per-event pipeline attempt history
      tags:
        - admin
      description: >-
        Returns the last 50 article/video generation attempts for a single match event, each annotated with timestamp,
        recipe, status, optional error type+message, and cost or generated content id when available. Useful for
        diagnosing silent failures (Claude credit issues, HeyGen outages, ElevenLabs/Imagen errors) without reading
        container stderr.
      parameters:
        - schema:
            type: string
          in: path
          name: eventId
          required: true
          description: Kambi event id
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                type: object
                required:
                  - eventId
                  - attempts
                properties:
                  eventId:
                    type: string
                  clientId:
                    type: string
                    nullable: true
                  attempts:
                    type: array
                    items:
                      type: object
                      required:
                        - timestamp
                        - pipeline
                        - recipe
                        - status
                      properties:
                        timestamp:
                          type: string
                          format: date-time
                        pipeline:
                          type: string
                          enum:
                            - auto-articles
                            - auto-videos
                        recipe:
                          type: string
                        status:
                          type: string
                          enum:
                            - success
                            - failed
                            - skipped
                        clientId:
                          type: string
                        error:
                          type: object
                          properties:
                            type:
                              type: string
                            message:
                              type: string
                        costEur:
                          type: number
                        generatedArticleId:
                          type: string
                        generatedVideoId:
                          type: string
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /status:
    get:
      summary: GET /dataengine/status
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /cost:
    get:
      summary: GET /dataengine/cost
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /cost/log:
    get:
      summary: GET /dataengine/cost/log
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /widgets:
    get:
      summary: GET /dataengine/widgets
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /journalists:
    get:
      summary: GET /dataengine/journalists
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /alerts:
    get:
      summary: GET /dataengine/alerts
      tags:
        - admin
      responses:
        '200':
          description: Default Response
  /health/leagues:
    get:
      summary: Per-league health snapshot (OK / empty / blocked)
      tags:
        - admin
      description: >-
        Reports which leagues are live, empty or blocked, with the age of the most recent match file per league. Open
        endpoint — no API key required. Added in the 2026-04-12 audit remediation (item N2).
      responses:
        '200':
          description: Default Response
  /standalone/{leagueId}:
    get:
      summary: GET /dataengine/standalone/:leagueId
      tags:
        - league
      parameters:
        - schema:
            type: string
          in: path
          name: leagueId
          required: true
      responses:
        '200':
          description: Default Response
  /standalone/{leagueId}/refresh:
    post:
      summary: POST /dataengine/standalone/:leagueId/refresh
      tags:
        - league
      parameters:
        - schema:
            type: string
          in: path
          name: leagueId
          required: true
      responses:
        '200':
          description: Default Response
  /leaders/{leagueId}:
    get:
      summary: GET /dataengine/leaders/:leagueId
      tags:
        - league
      parameters:
        - schema:
            type: string
          in: path
          name: leagueId
          required: true
      responses:
        '200':
          description: Default Response
  /goal-profiles:
    get:
      summary: GET /dataengine/goal-profiles
      tags:
        - league
      responses:
        '200':
          description: Default Response
  /team-stats-aggregates:
    get:
      summary: GET /dataengine/team-stats-aggregates
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /player-profiles:
    get:
      summary: GET /dataengine/player-profiles
      tags:
        - league
      responses:
        '200':
          description: Default Response
  /liga-bast/{leagueId}:
    get:
      summary: GET /dataengine/liga-bast/:leagueId
      tags:
        - league
      parameters:
        - schema:
            type: string
          in: path
          name: leagueId
          required: true
      responses:
        '200':
          description: Default Response
  /insights/{eventId}:
    get:
      summary: Read cached pre-match mined insights for one fixture
      tags:
        - insights
      description: >-
        Returns the top-25 statistically-validated patterns mined for the given Kambi event. Patterns are mined every 6
        hours pre-match by the cron at minutes :45 of hours 1, 7, 13 and 19 UTC, and cached on disk under
        `computed/insights/{eventId}`. The cached payload is served with a 60s Cache-Control max-age + ETag — clients
        should honor `If-None-Match` to avoid redundant transfers between mining ticks.


        This dataset SUPPLEMENTS the editorial ValueStats insight feed — overlapping criteria are filtered server-side
        so consumers never see duplicates. Every pattern surfaced has survived: (1) sample size n >= 10, (2) signed |Δ|
        >= 20pp vs population baseline, and (3) Bonferroni-corrected one-sided binomial p < 0.05 across all tests run
        for this event.


        Set `?run=1` to force read-through mining if no cache exists yet — useful for editorial preview tooling. Returns
        404 when the event has never been mined and `run` is unset.
      parameters:
        - schema:
            type: string
            enum:
              - '1'
          in: query
          name: run
          required: false
          description: When set to `1`, mine on-demand if no cache exists.
        - schema:
            type: string
          example: '1026050829'
          in: path
          name: eventId
          required: true
          description: Numeric Kambi event id (string-encoded to avoid JS integer truncation).
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-5'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /insights/{eventId}/run:
    post:
      summary: Force re-mine insights for one fixture
      tags:
        - insights
      description: >-
        Idempotent re-mining of pre-match patterns for a single Kambi event. Reads the cached EnrichedMatch + historical
        SR summaries off disk, runs all four mining phases (scoring/dedup, h2h+form+trajectory, player splits, external
        signals), Bonferroni-corrects, dedups against ValueStats, and overwrites `computed/insights/{eventId}` with the
        fresh top-25.


        Returns the run summary — counts at each filter stage plus the top-3 patterns by surpriseScore. Use this for
        editorial preview after pushing new ValueStats data, or to manually warm a fixture that the 6h cron missed.


        Master-key only (POST endpoints require X-API-Key per the global auth hook). Returns 404 when no EnrichedMatch
        is on disk for the event, 500 if mining throws (error message in response body — never fabricated).
      parameters:
        - schema:
            type: string
          example: '1026050829'
          in: path
          name: eventId
          required: true
          description: Numeric Kambi event id (string-encoded).
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                  - eventId
                  - totalTestsRun
                  - patternsAfterSignificance
                  - patternsAfterDedup
                  - surfaced
                  - top3
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  eventId:
                    type: string
                    example: '1026050829'
                  totalTestsRun:
                    type: integer
                  patternsAfterSignificance:
                    type: integer
                  patternsAfterDedup:
                    type: integer
                  surfaced:
                    type: integer
                    description: Number of patterns returned (post-dedup, capped at top-25).
                  top3:
                    type: array
                    description: Top 3 patterns by surpriseScore — quick preview without fetching the full set.
                    items:
                      $ref: '#/components/schemas/def-4'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
        '500':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /insights/run-all:
    post:
      summary: Force re-mine insights for every upcoming pre-match fixture
      tags:
        - insights
      description: >-
        Iterates every EnrichedMatch on disk where `status === "scheduled"` and `kickoff` is in the future, and re-mines
        each one. Idempotent — safe to call repeatedly. Skips matches whose status, kickoff, or team ids fail
        validation; per-event mining errors are caught and reported (capped at the first 10) without aborting the run.


        This is the same operation the 6-hourly cron (`45 1,7,13,19 * * *` UTC) performs — call this endpoint manually
        only for editorial bulk-warm or after a data backfill. Compute is local-only (no AI calls), so this endpoint is
        NOT subject to the `AI_DAILY_BUDGET_EUR` cost guard. Typical run-time is ~30s for ~300 matches against the
        existing on-disk historical cache.


        Master-key only.
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                  - scheduled
                  - mined
                  - skipped
                  - failed
                  - errors
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  scheduled:
                    type: integer
                    description: How many upcoming pre-match fixtures were attempted.
                  mined:
                    type: integer
                    description: Successfully re-mined and cached.
                  skipped:
                    type: integer
                    description: Skipped because not scheduled, kickoff in past, or missing team ids.
                  failed:
                    type: integer
                    description: Mining threw — see `errors` for details (capped at 10).
                  errors:
                    type: array
                    description: First 10 errors with eventId + message.
                    items:
                      type: object
                      required:
                        - eventId
                        - error
                      properties:
                        eventId:
                          type: string
                        error:
                          type: string
  /catalog:
    get:
      summary: Self-service API catalog (machine-readable JSON)
      tags:
        - catalog
      description: >-
        Returns a single document describing every endpoint, every widget, every dataset, the auth model, rate limits,
        and a recent-changes log. Designed for downstream consumers that need to discover capabilities without reading
        the source.


        The endpoint list is harvested from the live Fastify route table at request time, so it can never drift from the
        running API. Memoized for 30 seconds.


        For a browser-friendly version see `GET /dataengine/docs/catalog.html`. For the full OpenAPI 3.1 spec see
        `/dataengine/docs/openapi.yaml` or the interactive Scalar UI at `/dataengine/docs/`.
      responses:
        '200':
          description: Catalog document. See the response example for shape.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: Catalog document. See the response example for shape.
  /docs/catalog.html:
    get:
      summary: Self-service API catalog (browser-friendly HTML)
      tags:
        - catalog
      description: >-
        Same content as `GET /dataengine/catalog` but rendered as a self-contained HTML page with inline CSS — no
        external dependencies. Table-of-contents at top, sections for authentication, endpoints (grouped by tag),
        widgets, datasets, rate limits, and changelog.
      responses:
        '200':
          description: HTML document.
          content:
            text/html:
              schema:
                type: string
                description: HTML document.
  /admin/packages:
    get:
      summary: List approval queue packages
      tags:
        - admin-packages
      description: >-
        Returns a compact list view of packages, sorted by kickoff descending. Default filter is
        `status=pending_approval, clientId=hemmaklubben, limit=50`.
      parameters:
        - schema:
            type: string
            enum:
              - pending_approval
              - approved
              - scheduled
              - live
              - rejected
              - dropped
              - unpublished
              - all
          in: query
          name: status
          required: false
        - schema:
            type: string
          in: query
          name: clientId
          required: false
        - schema:
            type: string
            format: date-time
          in: query
          name: from
          required: false
        - schema:
            type: string
            format: date-time
          in: query
          name: to
          required: false
        - schema:
            type: string
          in: query
          name: limit
          required: false
      responses:
        '200':
          description: Default Response
  /admin/packages/{id}:
    get:
      summary: Fetch full package with version history
      tags:
        - admin-packages
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /admin/packages/{id}/version/{versionId}:
    get:
      summary: Fetch a specific historical version of a package
      tags:
        - admin-packages
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
        - schema:
            type: string
          in: path
          name: versionId
          required: true
      responses:
        '200':
          description: Default Response
  /admin/packages/{id}/approve:
    post:
      summary: Approve a pending package
      tags:
        - admin-packages
      description: >-
        Body: `{ "goLive": "now" }` to publish immediately, or `{ "goLive": "<ISO>" }` to schedule. Returns 409 if the
        package is not in `pending_approval`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - goLive
              properties:
                goLive:
                  type: string
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /admin/packages/{id}/reject:
    post:
      summary: Reject the current version with feedback and trigger regeneration
      tags:
        - admin-packages
      description: >-
        Marks the current version as rejected, stores the feedback comment, and enqueues a regeneration via the relevant
        article/video pipeline. Subsequent generation lands as v(n+1) on the same package; status stays
        `pending_approval` throughout. v1 limitation: scope=`video.script` still triggers a full re-render of audio +
        video.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scope
                - comment
              properties:
                scope:
                  type: string
                  enum:
                    - all
                    - article
                    - article.heroImage
                    - video
                    - video.script
                    - video.videoTrack
                    - video.audioTrack
                    - video.subtitles
                    - bannerHeadlines
                comment:
                  type: string
                  minLength: 1
                  maxLength: 2000
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /admin/packages/{id}/edit:
    patch:
      summary: Apply an in-place text edit to the current version
      tags:
        - admin-packages
      description: >-
        Field is a dot-path inside `currentVersion.elements`, e.g. "article.headline". Numeric path components index
        arrays (e.g. "article.body.0.text"). The edit is applied immediately to the package element snapshot AND
        mirrored to the canonical article/video file when the field maps cleanly. An entry is appended to `edits[]`. No
        new version is created and no regen is triggered.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - field
                - value
              properties:
                field:
                  type: string
                value:
                  type: string
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /admin/packages/{id}/unpublish:
    post:
      summary: Unpublish a live package
      tags:
        - admin-packages
      description: Moves status from `live` to `unpublished`. Subsequent feed rebuilds drop it from the lobby.
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /dam/{clientId}/catalog:
    get:
      summary: GET /dataengine/dam/:clientId/catalog
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /dam/{clientId}/select:
    get:
      summary: GET /dataengine/dam/:clientId/select
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /fs/matches:
    get:
      summary: GET /dataengine/fs/matches
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /fs/match/{eventId}:
    get:
      summary: GET /dataengine/fs/match/:eventId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: eventId
          required: true
      responses:
        '200':
          description: Default Response
  /fs/team/{teamId}:
    get:
      summary: GET /dataengine/fs/team/:teamId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: teamId
          required: true
      responses:
        '200':
          description: Default Response
  /fs/player/{playerId}:
    get:
      summary: GET /dataengine/fs/player/:playerId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: playerId
          required: true
      responses:
        '200':
          description: Default Response
  /fs/standings/{leagueId}:
    get:
      summary: GET /dataengine/fs/standings/:leagueId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: leagueId
          required: true
      responses:
        '200':
          description: Default Response
  /fs/leaders/{leagueId}:
    get:
      summary: GET /dataengine/fs/leaders/:leagueId
      tags:
        - league
      parameters:
        - schema:
            type: string
          in: path
          name: leagueId
          required: true
      responses:
        '200':
          description: Default Response
  /brand/{clientId}:
    get:
      summary: GET /dataengine/brand/:clientId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /brand/{clientId}/invalidate:
    post:
      summary: POST /dataengine/brand/:clientId/invalidate
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: clientId
          required: true
      responses:
        '200':
          description: Default Response
  /mappings/sr-to-kambi:
    get:
      summary: Resolve a SportRadar sport_event URN to its Kambi event id
      tags:
        - mappings
      description: >-
        Takes a SportRadar `sr:sport_event:<id>` URN and returns the canonical mapping record (Kambi numeric event id +
        team URNs + kickoff + league) if an enriched match exists with that URN persisted.


        Returns 404 when the URN is not present on any enriched record. This happens when SR has not yet published the
        fixture in either team's summary cache, or when the enriched match pre-dates the sportEventUrn persistence
        rollout and has not been re-enriched. The engine **never fabricates a mapping** — a 404 is the honest answer per
        the no-invented-data policy.
      parameters:
        - schema:
            type: string
            pattern: ^sr:sport_event:\d+$
          example: sr:sport_event:61301151
          in: query
          name: urn
          required: true
          description: SportRadar sport_event URN, e.g. `sr:sport_event:61301151`
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-2'
        '400':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /mappings/kambi-to-sr:
    get:
      summary: Resolve a Kambi event id to its SportRadar sport_event URN
      tags:
        - mappings
      description: >-
        Takes a numeric Kambi event id and returns the canonical mapping record (SR sport_event URN + team URNs +
        kickoff + league) if the enriched match has its `sportEventUrn` field populated.


        Returns 404 when no SR URN has been resolved for this Kambi event. Root causes: the match is not yet enriched,
        SR has not published the fixture in either team's summary cache (common for in-play matches — SR's summaries
        endpoint drops fixtures while they are in-play and republishes after full-time), or the match belongs to a
        league we do not ingest from SR (non-soccer leagues always 404). **Never fabricates a mapping.**
      parameters:
        - schema:
            type: string
            pattern: ^\d+$
          example: '1024044217'
          in: query
          name: eventId
          required: true
          description: Numeric Kambi event id (passed as string to avoid JS integer truncation)
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-2'
        '400':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /odds/{eventId}:
    get:
      summary: GET /dataengine/odds/:eventId
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: eventId
          required: true
      responses:
        '200':
          description: Default Response
  /odds/{eventId}/movement:
    get:
      summary: GET /dataengine/odds/:eventId/movement
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: eventId
          required: true
      responses:
        '200':
          description: Default Response
  /junibet/feed:
    get:
      summary: GET /dataengine/junibet/feed
      tags:
        - junibet-features
      responses:
        '200':
          description: Default Response
  /junibet/popular:
    get:
      summary: GET /dataengine/junibet/popular
      tags:
        - junibet-features
      responses:
        '200':
          description: Default Response
  /junibet/catalog:
    get:
      summary: GET /dataengine/junibet/catalog
      tags:
        - junibet-features
      responses:
        '200':
          description: Default Response
  /junibet/accumulators:
    get:
      summary: GET /dataengine/junibet/accumulators
      tags:
        - junibet-features
      responses:
        '200':
          description: Default Response
  /junibet/market-spotlight:
    get:
      summary: GET /dataengine/junibet/market-spotlight
      tags:
        - junibet-features
      responses:
        '200':
          description: Default Response
  /junibet/ticker:
    get:
      summary: GET /dataengine/junibet/ticker
      tags:
        - junibet-features
      responses:
        '200':
          description: Default Response
  /junibet/live-rail:
    get:
      summary: Live Now rail — major-league in-play matches for the Junibet lobby
      tags:
        - junibet-features
      description: >-
        Returns up to 10 currently-live football matches from Tier A (Premier League, La Liga, Serie A, Bundesliga,
        Ligue 1) and Tier B (Champions, Europa, Conference League). Each entry carries the live score, SR clock, live
        1X2 milliodds, and a full provenance block with both Kambi and SportRadar origins.


        Every request re-aggregates from the raw Kambi (≤150s freshness budget) and raw SR live summary (≤30s freshness
        budget) caches, so response latency is O(matches) with no deep enrichment. Matches that fail any freshness or
        completeness check are silently dropped — the endpoint never ships a partial card with missing score or odds.


        Polled at 5 s cadence by the frontend. Returns HTTP 200 with an empty matches array when no Tier A/B matches are
        live — never 404.
      parameters:
        - schema:
            type: string
          example: '10'
          in: query
          name: limit
          required: false
          description: Max number of matches (1-20, default 10)
        - schema:
            type: string
          example: football
          in: query
          name: sport
          required: false
          description: Sport filter. Only `football` is implemented at launch; anything else returns an empty array.
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-3'
  /client/{id}/sportsbook/feed:
    get:
      summary: GET /dataengine/client/:id/sportsbook/feed
      tags:
        - lobby
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /client/{id}/sportsbook/popular:
    get:
      summary: GET /dataengine/client/:id/sportsbook/popular
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /client/{id}/sportsbook/catalog:
    get:
      summary: GET /dataengine/client/:id/sportsbook/catalog
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /client/{id}/sportsbook/accumulators:
    get:
      summary: GET /dataengine/client/:id/sportsbook/accumulators
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /client/{id}/sportsbook/market-spotlight:
    get:
      summary: GET /dataengine/client/:id/sportsbook/market-spotlight
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /client/{id}/sportsbook/ticker:
    get:
      summary: GET /dataengine/client/:id/sportsbook/ticker
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /client/{id}/sportsbook/live-rail:
    get:
      summary: GET /dataengine/client/:id/sportsbook/live-rail
      tags:
        - discovery
      parameters:
        - schema:
            type: string
          in: path
          name: id
          required: true
      responses:
        '200':
          description: Default Response
  /live/{eventId}/summary:
    get:
      summary: Fetch the latest SportRadar live summary for an in-play match
      tags:
        - live
      description: >-
        Returns the most recent `sport_event_status` (match_status, period, clock, home/away score, period_scores) and
        `statistics.totals` (possession, shots, corners, fouls, cards) that the live poller has harvested from
        SportRadar for this fixture, plus optional `ball_locations` and `match_situation` fields when SR publishes them.


        The poller runs every 15 seconds against any enriched match with `sportEventUrn` set whose kickoff is in
        `[now-10min, now+180min]`. Responses are served straight from disk (ETag + 30-second `Cache-Control`), so
        expected freshness is 15s behind the live wire.


        Returns 404 when the poller has never seen this match — usually means either the enriched record has no SR URN
        (SR has not published the fixture in either team's summary cache), the match is outside the poller window, or
        the eventId does not exist.
      parameters:
        - schema:
            type: string
          example: '1026050511'
          in: path
          name: eventId
          required: true
          description: Numeric Kambi event id of the match
      responses:
        '200':
          description: Latest SR match summary payload wrapped with the poller metadata
          content:
            application/json:
              schema:
                description: Latest SR match summary payload wrapped with the poller metadata
                type: object
                additionalProperties: true
                properties:
                  fetchedAt:
                    type: string
                    format: date-time
                    description: When the live poller last pulled this payload from SR
                  urn:
                    type: string
                    description: SR sport_event URN this summary belongs to
                    example: sr:sport_event:67090592
                  eventId:
                    type: string
                    description: Kambi event id echoed from the request
                  sport_event:
                    type: object
                    additionalProperties: true
                    description: Raw SR `sport_event` block (id, start_time, competitors, venue)
                  sport_event_status:
                    type: object
                    additionalProperties: true
                    description: Raw SR `sport_event_status` (status, match_status, home_score, away_score, clock, period_scores)
                  statistics:
                    type: object
                    additionalProperties: true
                    description: Raw SR `statistics.totals` with per-team possession/shots/corners/etc
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /live/{eventId}/timeline:
    get:
      summary: Fetch the latest SportRadar timeline (event-by-event log) for an in-play match
      tags:
        - live
      description: >-
        Returns the ordered list of match events harvested from SportRadar: `match_started`, `period_start`,
        `goal_scored` (with scorers + assists), `yellow_card`, `red_card`, `substitution`, `video_assistant_referee`
        (VAR), `injury_time_shown`, `period_score`, `match_ended`, etc. Each entry carries an ISO `time`, a SR `type`
        code, and optional `commentary`, `match_clock`, `period`, and player references.


        Refreshed on the same 15-second live-poller cadence as `/summary`. Returns 404 with the same semantics as
        `/summary` — the poller has never seen this match.
      parameters:
        - schema:
            type: string
          example: '1026050511'
          in: path
          name: eventId
          required: true
          description: Numeric Kambi event id of the match
      responses:
        '200':
          description: Latest SR timeline payload wrapped with the poller metadata
          content:
            application/json:
              schema:
                description: Latest SR timeline payload wrapped with the poller metadata
                type: object
                additionalProperties: true
                properties:
                  fetchedAt:
                    type: string
                    format: date-time
                  urn:
                    type: string
                    example: sr:sport_event:67090592
                  eventId:
                    type: string
                  sport_event:
                    type: object
                    additionalProperties: true
                  sport_event_status:
                    type: object
                    additionalProperties: true
                  timeline:
                    type: array
                    description: Ordered list of match events (goals, cards, subs, VAR, period changes)
                    items:
                      type: object
                      additionalProperties: true
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /live/{eventId}/lineups:
    get:
      summary: Fetch starting lineups + formations for a match (cached once per fixture)
      tags:
        - live
      description: >-
        Returns the SportRadar lineups payload — starting XI, formations (e.g. `"4-2-3-1"`), coaches, and substitutes
        for both teams. Unlike `/summary` and `/timeline` this is fetched **once** on the first poll where SR has
        published it (typically 15-60 minutes before kickoff) and cached for the duration of the match — the data does
        not change during play.


        Returns 404 when SR has not yet published lineups, which is normal until close to kickoff. Clients should poll
        periodically and accept 404 gracefully.
      parameters:
        - schema:
            type: string
          example: '1024044217'
          in: path
          name: eventId
          required: true
          description: Numeric Kambi event id of the match
      responses:
        '200':
          description: SR lineups payload wrapped with the poller metadata
          content:
            application/json:
              schema:
                description: SR lineups payload wrapped with the poller metadata
                type: object
                additionalProperties: true
                properties:
                  fetchedAt:
                    type: string
                    format: date-time
                  urn:
                    type: string
                    example: sr:sport_event:61301151
                  eventId:
                    type: string
                  lineups:
                    type: object
                    additionalProperties: true
                    description: 'Raw SR lineups: per-team starting XI, formation, coach, substitutes'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /cms/market-config:
    get:
      summary: GET /dataengine/cms/market-config
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
    post:
      summary: POST /dataengine/cms/market-config
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
  /team-aggregates/{teamId}:
    get:
      summary: Per-team rich aggregates for AI insight prompts
      tags:
        - aggregates
      description: >-
        Returns a comprehensive season-scoped statistical profile for one team — corner, card, foul, free-kick, goal,
        match-state and form-context aggregates derived from every closed SR match summary in the current season. Powers
        Junibet AI insight prompts ("AIK averages 6.2 corners home / 5.3 away, 7.1 vs top-half opposition").


        CACHING: 6h TTL on disk under `computed/team-aggregates/{teamId}.json`. The route serves the cached payload with
        a 60s Cache-Control + ETag; pass `?refresh=1` to force re-compute. Standings ingest piggybacks a background
        recompute so the cache stays warm.


        NULL HANDLING: Fields that depend on timeline data (goal-by-interval, set-piece / open-play / penalty splits,
        late winners, comeback wins, penalties for/against) are `null` whenever the cached SR summaries lack a
        `timeline` array — currently the common case. The frontend / prompt layer must hide null fields rather than
        rendering 0.
      parameters:
        - schema:
            type: string
            enum:
              - '1'
          in: query
          name: refresh
          required: false
          description: When `1`, force a fresh compute even if the disk cache is < 6h old.
        - schema:
            type: string
          example: sr:competitor:1764
          in: path
          name: teamId
          required: true
          description: SR competitor URN, e.g. sr:competitor:1764 (AIK).
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-6'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
        '503':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /player-aggregates/{playerId}:
    get:
      summary: Per-player rich aggregates for AI insight prompts
      tags:
        - aggregates
      description: >-
        Returns season-scoped per-player aggregates: attacking totals + per-90 metrics, efficiency ratios, discipline +
        suspension-watch flag, and form (goals/assists/shots in last 5 matches + scoring streak). Computed from every
        closed SR match summary that lists the player in `statistics.totals.competitors[].players[]`.


        CACHING: 6h TTL on disk under `computed/player-aggregates/{playerId}.json`. The route serves with 60s
        Cache-Control + ETag; `?refresh=1` forces re-compute.


        NULL HANDLING: Per-90 metrics depend on `minutes_played`, which SR does NOT ship in the per-player team-summary
        block for most leagues. When minutes are missing every per-90 field is `null`. `key_passes`, `tackles`,
        `interceptions` are also always `null` because SR does not surface them in this dataset.
      parameters:
        - schema:
            type: string
            enum:
              - '1'
          in: query
          name: refresh
          required: false
          description: When `1`, force a fresh compute even if the disk cache is < 6h old.
        - schema:
            type: string
          example: sr:player:869030
          in: path
          name: playerId
          required: true
          description: SR player URN, e.g. sr:player:869030.
      responses:
        '200':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-7'
        '404':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
        '503':
          description: Default Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/def-0'
  /:
    get:
      summary: GET /
      tags:
        - discovery
      responses:
        '200':
          description: Default Response
servers:
  - url: https://api.insideformation.com/dataengine
    description: Production
  - url: http://localhost:3100/dataengine
    description: Local development
security:
  - ApiKeyAuth: []
tags:
  - name: lobby
    description: Client-facing lobby feed endpoints. One call returns everything needed to render a front page.
  - name: match-detail
    description: Per-match widget feeds, odds catalogues, and live summaries.
  - name: league
    description: 'League-scoped widgets: leader boards, standings, liga-bäst rankings, standalone highlights.'
  - name: junibet-features
    description: Junibet-only feature endpoints that hybridize ValueStats confidence with Kambi betting patterns.
  - name: live
    description: >-
      In-play match data: live summaries, timelines, lineups (SportRadar-backed). Polled every 15 seconds while a
      fixture is in the window [kickoff-10min, kickoff+180min]. Responses are raw SportRadar payloads;
      match_status=not_started until the whistle.
  - name: mappings
    description: >-
      Canonical SR↔Kambi identifier bridge. Every enriched football match has its SportRadar sport_event URN persisted
      at ingest time via deterministic URN+kickoff matching (never fuzzy names). These endpoints let you convert between
      Kambi numeric event ids and SR sr:sport_event:* URNs in either direction.
  - name: admin
    description: 'Operational endpoints: pipeline triggers, cache refreshes, health, cost, alerts.'
  - name: discovery
    description: Introspection endpoints for widgets, journalists, recipes, catalogs.
  - name: content
    description: AI article generation and lifecycle management.
  - name: insights
    description: >-
      Pre-match data-mined insights — surprising-but-statistically-significant patterns supplementing ValueStats.
      Patterns surface things like "AIK has only won 1 of 20 matches against Malmö" or "team X with player Y starting
      wins 78% vs 23% without". Mined every 6h pre-match, cached per event.
  - name: catalog
    description: >-
      Self-service API catalog. One call returns every endpoint, every widget, every dataset, plus auth model and
      refresh cadences. Designed for downstream consumers (mobile apps, web frontends, other backends) that need to
      discover what the engine exposes without reading the source.
  - name: admin-packages
    description: >-
      Per-match approval queue for clients that opt into editorial moderation (Hemmaklubben). Generated articles +
      videos land in a Package in `pending_approval` status; editors approve / reject / edit / unpublish through these
      endpoints. Only packages that reach `live` (or `scheduled` whose `scheduledFor` is in the past) become visible in
      the lobby feed.
  - name: aggregates
    description: >-
      Per-team and per-player rich aggregates derived from cached SR match summaries. Powers Junibet AI insight prompts
      so a bettor evaluating a market gets concrete season-level patterns ("AIK averages 6.2 corners home / 5.3 away,
      7.1 vs top-half opposition"; "Player X has 3 goals in last 5 with 1.4 shots-on-target per match"). 6h on-disk TTL
      per-id with `?refresh=1` override; standings ingest piggybacks a background recompute.
