openapi: 3.1.0
info:
  title: OS.TV API
  version: "1.0.0"
  description: >
    Read model over the WWBN broadcast graph. One canonical origin per broadcaster;
    IPTV, OVP, OTA and FAST are presentations of it. The Unified BANS Read API returns
    each origin once, with identity, governance, allocation, publication and resolved
    presentations. Read-only: every endpoint is GET and never mutates the broadcast graph.
    Invariants: reviewed != governed; allocated != published != reachable.
  contact:
    name: OS.TV API
    url: https://api.os.tv/
servers:
  - url: https://api.os.tv/v1
    description: OS.TV API v1
tags:
  - name: Public
    description: No authentication required.
  - name: Registry
    description: Unified per-origin registry. Domain-administrator authentication required.
paths:
  /summary:
    get:
      tags: [Public]
      summary: Broadcast-feed integration status
      description: Integration status for the WWBN broadcast feed. Public; unrelated to the registry counts.
      operationId: getSummary
      security: []
      responses:
        "200":
          description: Integration status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IntegrationStatus" }
  /registry/summary:
    get:
      tags: [Registry]
      summary: Registry summary counts
      description: Platform-wide counts across canonical origins and catalog carriages (two separate populations).
      operationId: getRegistrySummary
      responses:
        "200":
          description: Registry summary.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RegistrySummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /registry/origins:
    get:
      tags: [Registry]
      summary: List canonical origins
      description: Paginates over canonical origins (never carriage rows). Filters compose (AND).
      operationId: listRegistryOrigins
      parameters:
        - { name: governance_state, in: query, schema: { type: string, enum: [governed, ungoverned] } }
        - { name: allocation_state, in: query, schema: { type: string, enum: [allocated, unallocated] } }
        - { name: publication_status, in: query, schema: { type: string, enum: [published, held] } }
        - { name: transport, in: query, schema: { type: string, enum: [iptv, ovp, ota, fast] } }
        - { name: has_presentations, in: query, schema: { type: boolean } }
        - { name: network_id, in: query, schema: { type: string } }
        - { name: station_id, in: query, schema: { type: string } }
        - { name: search, in: query, schema: { type: string } }
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200":
          description: A page of canonical origins.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OriginList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /registry/origins/{originId}:
    get:
      tags: [Registry]
      summary: Get one canonical origin
      operationId: getRegistryOrigin
      parameters:
        - name: originId
          in: path
          required: true
          description: "wwbn:origin:<uuid> (or the bare uuid)."
          schema: { type: string }
      responses:
        "200":
          description: The origin record.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OriginEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /registry/addresses/{address}:
    get:
      tags: [Registry]
      summary: Resolve a BANS address to its origin
      operationId: getRegistryByAddress
      parameters:
        - name: address
          in: path
          required: true
          description: "Channel-tier BANS address, e.g. 2.0.1."
          schema: { type: string }
      responses:
        "200":
          description: The origin holding that address.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OriginEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, description: "Authorization: Bearer <access_token>" }
    cookieAuth: { type: apiKey, in: cookie, name: access_token, description: "OS.TV session cookie." }
  responses:
    Unauthorized:
      description: Missing or invalid token.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Unauthorized" }
    Forbidden:
      description: Authenticated but not a domain administrator.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Forbidden" }
    NotFound:
      description: No origin for the given id or address.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/NotFound" }
  schemas:
    IntegrationStatus:
      type: object
      required: [success, connected]
      properties:
        success: { type: boolean, const: true }
        connected: { type: boolean }
        networks: { type: integer }
        iptv_networks: { type: integer }
        ovp_networks: { type: integer }
        channels: { type: integer }
        programs: { type: integer }
        reviews: { type: integer }
        pending_reviews: { type: integer }
        generated_at: { type: string, format: date-time }
    Presentation:
      type: object
      required: [carriage_id, transport]
      properties:
        carriage_id: { type: string }
        transport: { type: string, enum: [iptv, ovp, ota, fast] }
        provider: { type: [string, "null"] }
        source: { type: [string, "null"] }
        availability: { type: [string, "null"], description: "Reachability telemetry; null when unknown. Never inferred." }
    BansAllocation:
      type: object
      required: [allocated, address, network, station, channel, publication_status]
      properties:
        allocated: { type: boolean }
        address: { type: [string, "null"], description: "network.station.channel, e.g. 2.0.1" }
        network: { type: [integer, "null"] }
        station: { type: [integer, "null"] }
        channel: { type: [integer, "null"] }
        publication_status: { type: [string, "null"], enum: [published, held, null] }
        held:
          type: [object, "null"]
          properties:
            at: { type: string, format: date-time }
            reason: { type: [string, "null"] }
    Origin:
      type: object
      required: [origin, governance, bans, presentations]
      properties:
        origin:
          type: object
          required: [id, name, registry_id, network]
          properties:
            id: { type: string, description: "Per-origin key wwbn:origin:<uuid>." }
            name: { type: string }
            registry_id: { type: [string, "null"], description: "Network-tier address (shared by siblings) — not a per-origin key." }
            network:
              type: object
              properties:
                id: { type: [string, "null"] }
                name: { type: [string, "null"] }
        governance:
          type: object
          required: [state, active_edges]
          properties:
            state: { type: string, enum: [governed, ungoverned] }
            active_edges: { type: integer, minimum: 0 }
        bans: { $ref: "#/components/schemas/BansAllocation" }
        presentations:
          type: array
          items: { $ref: "#/components/schemas/Presentation" }
    OriginEnvelope:
      type: object
      required: [success, origin]
      properties:
        success: { type: boolean, const: true }
        generated_at: { type: string, format: date-time }
        origin: { $ref: "#/components/schemas/Origin" }
    OriginList:
      type: object
      required: [success, page, origins]
      properties:
        success: { type: boolean, const: true }
        generated_at: { type: string, format: date-time }
        page:
          type: object
          properties:
            page: { type: integer }
            limit: { type: integer }
            offset: { type: integer }
            returned: { type: integer }
            total: { type: integer }
        filters_applied: { type: object, additionalProperties: true }
        origins:
          type: array
          items: { $ref: "#/components/schemas/Origin" }
    RegistrySummary:
      type: object
      required: [success, canonical_origins, reviewed_origins, governed_origins, bans_allocated, published, held, transport_inventory, resolved_presentations]
      properties:
        success: { type: boolean, const: true }
        generated_at: { type: string, format: date-time }
        canonical_origins: { type: integer }
        reviewed_origins: { type: integer }
        governed_origins: { type: integer }
        eligible_unallocated: { type: integer }
        bans_allocated: { type: integer }
        published: { type: integer }
        held: { type: integer }
        catalog_carriages: { type: [integer, "null"] }
        resolved_carriages: { type: [integer, "null"] }
        unresolved_carriages: { type: [integer, "null"] }
        transport_inventory:
          type: object
          description: "Catalog carriages by transport; null where not classifiable."
          properties:
            iptv: { type: [integer, "null"] }
            ovp: { type: [integer, "null"] }
            ota: { type: [integer, "null"] }
            fast: { type: [integer, "null"] }
        resolved_presentations:
          type: object
          description: "Carriages resolved to an origin, by transport."
          properties:
            iptv: { type: integer }
            ovp: { type: integer }
            ota: { type: integer }
            fast: { type: integer }
    Unauthorized:
      type: object
      required: [error]
      properties:
        error: { type: string, const: Unauthorized }
    Forbidden:
      type: object
      required: [error]
      properties:
        error: { type: string }
        message: { type: string }
    NotFound:
      type: object
      required: [success, error]
      properties:
        success: { type: boolean, const: false }
        error: { type: string }
security:
  - bearerAuth: []
  - cookieAuth: []
