openapi: 3.1.0
info:
  title: ILY Sign Admin API
  version: 1.0.0
  summary: Public admin API for locations, screens, groups, playlists, and assets.
  description: |
    The ILY Sign Admin API exposes a small, production-backed `/v1` surface for
    automation and integrations. Authenticate with a named API key created in
    Console Settings -> Developers.

    API access requires an active trial or paid plan. Free plans cannot call
    `/v1`, even if a key exists.
servers:
  - url: https://api.ilysign.com/v1
    description: Production
  - url: https://api.dev.ilysign.com/v1
    description: Development
security:
  - ApiKeyHeader: []
tags:
  - name: Locations
  - name: Screens
  - name: Groups
  - name: Playlists
  - name: Assets
paths:
  /locations:
    get:
      tags: [Locations]
      operationId: listLocations
      summary: List locations
      description: Returns active locations visible to the API key.
      responses:
        '200':
          description: Locations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Location'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /screens:
    get:
      tags: [Screens]
      operationId: listScreens
      summary: List screens
      description: Returns screens visible to the API key, including publish status.
      responses:
        '200':
          description: Screens
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Screen'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /screens/claim:
    post:
      tags: [Screens]
      operationId: claimScreen
      summary: Claim a screen
      description: Claims a pending pairing code into the authenticated organization.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClaimScreenRequest'
      responses:
        '200':
          description: Claimed screen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimScreenResponse'
        '400':
          $ref: '#/components/responses/BadRequestError'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /groups:
    get:
      tags: [Groups]
      operationId: listGroups
      summary: List groups
      description: Returns screen groups visible to the API key.
      responses:
        '200':
          description: Groups
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Group'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /playlists:
    get:
      tags: [Playlists]
      operationId: listPlaylists
      summary: List playlists
      responses:
        '200':
          description: Playlists
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Playlist'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /playlists/{id}/publish:
    post:
      tags: [Playlists]
      operationId: publishPlaylist
      summary: Publish a playlist
      description: Increments the playlist generation and meters transfer usage.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Publish result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublishPlaylistResponse'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
        '404':
          $ref: '#/components/responses/NotFoundError'
  /assets:
    get:
      tags: [Assets]
      operationId: listAssets
      summary: List assets
      description: Returns the organization asset library with temporary signed view URLs.
      responses:
        '200':
          description: Assets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Asset'
        '401':
          $ref: '#/components/responses/UnauthorizedError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: Authorization
      description: |
        Send `Authorization: Bearer ilys_key_...`.
  responses:
    BadRequestError:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnauthorizedError:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ForbiddenError:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFoundError:
      description: Not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      additionalProperties: false
      required: [error, code]
      properties:
        error:
          type: string
        code:
          type: string
          examples: [bad_request, unauthorized, forbidden, not_found, internal]
    Location:
      type: object
      additionalProperties: false
      required: [id, name, archived, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        archived:
          type: boolean
        created_at:
          type: string
          format: date-time
        playlist_id:
          type: [string, 'null']
          format: uuid
    Screen:
      type: object
      additionalProperties: false
      required:
        [id, name, status, applied_generation, catalog_generation, created_at, publish_status]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        location_id:
          type: [string, 'null']
          format: uuid
        playlist_id:
          type: [string, 'null']
          format: uuid
        status:
          type: string
          examples: [active, revoked]
        last_seen_at:
          type: [string, 'null']
          format: date-time
        last_sync_at:
          type: [string, 'null']
          format: date-time
        applied_generation:
          type: integer
          minimum: 0
        catalog_generation:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
        publish_status:
          type: string
          examples: [live, waiting, offline, revoked]
    ClaimScreenRequest:
      type: object
      additionalProperties: false
      required: [code]
      properties:
        code:
          type: string
          description: Pending screen pairing code.
        name:
          type: [string, 'null']
          description: Optional display name to assign during claim.
    ClaimScreenResponse:
      type: object
      additionalProperties: false
      required: [screen]
      properties:
        screen:
          $ref: '#/components/schemas/ClaimedScreen'
    ClaimedScreen:
      type: object
      additionalProperties: false
      required: [id, name, status, applied_generation, catalog_generation, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        location_id:
          type: [string, 'null']
          format: uuid
        playlist_id:
          type: [string, 'null']
          format: uuid
        status:
          type: string
        last_seen_at:
          type: [string, 'null']
          format: date-time
        last_sync_at:
          type: [string, 'null']
          format: date-time
        applied_generation:
          type: integer
          minimum: 0
        catalog_generation:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
    Group:
      type: object
      additionalProperties: false
      required: [id, name, member_count, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        playlist_id:
          type: [string, 'null']
          format: uuid
        created_at:
          type: string
          format: date-time
        member_count:
          type: integer
          minimum: 0
    Playlist:
      type: object
      additionalProperties: false
      required: [id, name, slide_interval_secs, transition, generation, items, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slide_interval_secs:
          type: integer
          minimum: 1
        transition:
          type: string
        generation:
          type: integer
          minimum: 0
        items:
          type: array
          items:
            $ref: '#/components/schemas/PlaylistItem'
        created_at:
          type: string
          format: date-time
        published_at:
          type: [string, 'null']
          format: date-time
    PlaylistItem:
      type: object
      additionalProperties: false
      required: [asset_id]
      properties:
        asset_id:
          type: string
          format: uuid
        duration_ms:
          type: [integer, 'null']
          minimum: 0
    PublishPlaylistResponse:
      type: object
      additionalProperties: false
      required: [playlist, transfer_bytes_metered]
      properties:
        playlist:
          $ref: '#/components/schemas/Playlist'
        transfer_bytes_metered:
          type: integer
          minimum: 0
    Asset:
      type: object
      additionalProperties: false
      required:
        [id, name, mime, size_bytes, content_hash, s3_key, is_demo, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        mime:
          type: string
        size_bytes:
          type: integer
          minimum: 0
        content_hash:
          type: string
        s3_key:
          type: string
        width:
          type: [integer, 'null']
          minimum: 0
        height:
          type: [integer, 'null']
          minimum: 0
        duration_ms:
          type: [integer, 'null']
          minimum: 0
        is_demo:
          type: boolean
        created_at:
          type: string
          format: date-time
        url:
          type: [string, 'null']
          format: uri
