openapi: 3.1.0
info:
  title: SmartAVStream External API
  version: 1.1.0
  summary: Tenant-bound live-event scheduling and encoder Device control.
  description: |
    The supported SmartAVStream `/api/v1` machine contract for external live
    Events and fixed encoder Devices.

    ApiClient and Device credentials are separate. The authenticated credential
    determines Tenant authority; request data cannot select a Tenant. Normal
    Device polling never returns a stream key.

    Automatic schedule cascading and independent scheduled provider shutdown at
    the 30-minute overrun boundary are approved but are not deployed in this
    contract release.
  contact:
    name: SmartAVStream support
  license:
    name: Proprietary
servers:
  - url: https://api-staging.smartavstream.com
    description: Staging
  - url: https://api.smartavstream.com
    description: Production
tags:
  - name: Live Events
    description: Tenant-bound server-to-server scheduling operations.
  - name: Encoder Device
    description: Self-scoped bootstrap, heartbeat and control polling.
security: []
paths:
  /api/v1/live-events:
    get:
      operationId: findLiveEventByExternalReference
      summary: Find a live Event by the caller's stable external reference
      tags: [Live Events]
      security:
        - apiClientBearer: []
      parameters:
        - name: externalReference
          in: query
          required: true
          description: Tenant-unique caller reference.
          schema:
            $ref: '#/components/schemas/ExternalReference'
      responses:
        '200':
          description: The authoritative Event.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveEventEnvelope'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/ApiClientAuthenticationFailed'
        '403':
          $ref: '#/components/responses/ApiClientScopeRequired'
        '404':
          $ref: '#/components/responses/LiveEventNotFound'
        '429':
          $ref: '#/components/responses/ApiClientRateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      operationId: createLiveEvent
      summary: Create a draft scheduled live Event
      description: |
        Creates scheduled-live or live-to-VOD Content and may atomically assign
        an existing ready Channel owned by the authenticated Tenant. Repeating
        an identical request with the same Idempotency-Key returns the original
        result.
      tags: [Live Events]
      security:
        - apiClientBearer: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LiveEventCreate'
            examples:
              scheduledLive:
                value:
                  title: Quarterly briefing
                  externalReference: office-event-4831
                  contentType: live_to_vod
                  scheduledStartAt: '2026-10-09T22:00:00Z'
                  scheduledEndAt: '2026-10-09T23:00:00Z'
                  encodingProfile: standard-landscape
                  channelId: 11111111-1111-4111-8111-111111111111
                  preBufferSeconds: 300
                  postBufferSeconds: 300
      responses:
        '201':
          description: Event created.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveEventEnvelope'
        '200':
          description: Original result replayed for the same key and payload.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveEventEnvelope'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/ApiClientAuthenticationFailed'
        '403':
          $ref: '#/components/responses/ApiClientScopeRequired'
        '409':
          $ref: '#/components/responses/LiveEventConflict'
        '429':
          $ref: '#/components/responses/ApiClientRateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/live-events/{eventId}:
    parameters:
      - $ref: '#/components/parameters/EventId'
    get:
      operationId: getLiveEvent
      summary: Read one live Event
      tags: [Live Events]
      security:
        - apiClientBearer: []
      responses:
        '200':
          description: The authoritative Event.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveEventEnvelope'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/ApiClientAuthenticationFailed'
        '403':
          $ref: '#/components/responses/ApiClientScopeRequired'
        '404':
          $ref: '#/components/responses/LiveEventNotFound'
        '429':
          $ref: '#/components/responses/ApiClientRateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      operationId: updateLiveEvent
      summary: Edit supported live Event fields
      description: Requires both an idempotency key and the latest numeric revision.
      tags: [Live Events]
      security:
        - apiClientBearer: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/IfMatch'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LiveEventUpdate'
            example:
              scheduledEndAt: '2026-10-09T23:30:00Z'
      responses:
        '200':
          description: Event updated or the identical mutation replayed.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveEventEnvelope'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/ApiClientAuthenticationFailed'
        '403':
          $ref: '#/components/responses/ApiClientScopeRequired'
        '404':
          $ref: '#/components/responses/LiveEventNotFound'
        '409':
          $ref: '#/components/responses/LiveEventConflict'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/ApiClientRateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/encoder/bootstrap:
    post:
      operationId: bootstrapEncoderDevice
      summary: Retrieve the bound Channel connection details once per credential version
      description: |
        Returns the ingest server and stream key only for initial provisioning or
        an authorised credential-version change. Treat the response as a secret;
        normal polling never contains connection credentials.
      tags: [Encoder Device]
      security:
        - deviceBearer: []
      responses:
        '200':
          description: Connection details claimed for this Device credential version.
          headers:
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
            Pragma:
              $ref: '#/components/headers/NoCachePragma'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EncoderBootstrapEnvelope'
        '401':
          $ref: '#/components/responses/DeviceAuthenticationFailed'
        '409':
          $ref: '#/components/responses/EncoderBootstrapNotRequired'
        '429':
          $ref: '#/components/responses/DeviceRateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/ProviderFailed'
  /api/v1/encoder/poll:
    post:
      operationId: pollEncoderDevice
      summary: Report heartbeat/state and receive the current instruction
      description: |
        Call normally every 60 seconds. A successful response always contains
        an explicit idle, start, continue or stop instruction. If transport fails
        or the endpoint returns DEVICE_CONTROL_UNAVAILABLE, preserve the current
        encoder state and do not infer a new instruction.
      tags: [Encoder Device]
      security:
        - deviceBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EncoderPollRequest'
            examples:
              idle:
                value:
                  reportedState: idle
                  appliedInstructionRevision: null
              streaming:
                value:
                  reportedState: streaming
                  appliedInstructionRevision: event-version:start
      responses:
        '200':
          description: Authoritative current instruction and next poll interval.
          headers:
            Cache-Control:
              $ref: '#/components/headers/PrivateNoStore'
            Pragma:
              $ref: '#/components/headers/NoCachePragma'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EncoderPollEnvelope'
        '400':
          $ref: '#/components/responses/ValidationFailed'
        '401':
          $ref: '#/components/responses/DeviceAuthenticationFailed'
        '429':
          $ref: '#/components/responses/DeviceRateLimited'
        '503':
          $ref: '#/components/responses/DeviceControlUnavailable'
components:
  securitySchemes:
    apiClientBearer:
      type: http
      scheme: bearer
      bearerFormat: sav_api_<credential-id>_<secret>
      description: Tenant-bound office/scheduling-system credential.
    deviceBearer:
      type: http
      scheme: bearer
      bearerFormat: sav_device_<credential-id>_<secret>
      description: Self-scoped credential bound to one Device and Channel.
  parameters:
    EventId:
      name: eventId
      in: path
      required: true
      description: Opaque SmartAVStream Event/Content identifier.
      schema:
        type: string
        format: uuid
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        8–200 visible ASCII characters. Reuse for a retry of the identical
        logical mutation; never reuse for a different request.
      schema:
        type: string
        minLength: 8
        maxLength: 200
        pattern: '^[!-~]+$'
    IfMatch:
      name: If-Match
      in: header
      required: true
      description: Latest numeric revision, normally copied from the Event ETag.
      schema:
        type: string
        examples: ['"3"']
  headers:
    ETag:
      description: Quoted numeric Event revision.
      schema:
        type: string
        examples: ['"1"']
    IdempotencyReplayed:
      description: Present with `true` when the stored result was replayed.
      required: false
      schema:
        type: string
        enum: ['true']
    PrivateNoStore:
      description: Response must not be stored by shared or private caches.
      schema:
        type: string
        examples: [private, no-store]
    NoCachePragma:
      description: Compatibility no-cache directive.
      schema:
        type: string
        enum: [no-cache]
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
  schemas:
    RequestId:
      type: string
      format: uuid
      description: Correlation identifier safe to quote to support.
    ExternalReference:
      type: string
      minLength: 1
      maxLength: 200
    OffsetDateTime:
      type: string
      format: date-time
      description: ISO 8601 timestamp with an explicit UTC offset.
    LiveContentType:
      type: string
      enum: [scheduled_live, live_to_vod]
    LiveEventCreate:
      type: object
      additionalProperties: false
      required:
        - title
        - contentType
        - scheduledStartAt
        - scheduledEndAt
        - encodingProfile
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        externalReference:
          $ref: '#/components/schemas/ExternalReference'
        contentType:
          $ref: '#/components/schemas/LiveContentType'
        scheduledStartAt:
          $ref: '#/components/schemas/OffsetDateTime'
        scheduledEndAt:
          $ref: '#/components/schemas/OffsetDateTime'
        encodingProfile:
          type: string
          minLength: 1
          maxLength: 100
        channelId:
          type: string
          format: uuid
        preBufferSeconds:
          type: integer
          minimum: 0
          maximum: 86400
          default: 0
        postBufferSeconds:
          type: integer
          minimum: 0
          maximum: 86400
          default: 0
    LiveEventUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        externalReference:
          oneOf:
            - $ref: '#/components/schemas/ExternalReference'
            - type: 'null'
        contentType:
          $ref: '#/components/schemas/LiveContentType'
        scheduledStartAt:
          $ref: '#/components/schemas/OffsetDateTime'
        scheduledEndAt:
          $ref: '#/components/schemas/OffsetDateTime'
        encodingProfile:
          type: string
          minLength: 1
          maxLength: 100
        channelId:
          oneOf:
            - type: string
              format: uuid
            - type: 'null'
        preBufferSeconds:
          type: integer
          minimum: 0
          maximum: 86400
        postBufferSeconds:
          type: integer
          minimum: 0
          maximum: 86400
    LiveEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - externalReference
        - title
        - contentType
        - publicationStatus
        - scheduledStartAt
        - scheduledEndAt
        - encodingProfile
        - revision
        - channel
        - watchUrl
      properties:
        id:
          type: string
          format: uuid
        externalReference:
          oneOf:
            - $ref: '#/components/schemas/ExternalReference'
            - type: 'null'
        title:
          type: string
        contentType:
          $ref: '#/components/schemas/LiveContentType'
        publicationStatus:
          type: string
          enum: [draft, published]
        scheduledStartAt:
          type: string
          format: date-time
        scheduledEndAt:
          type: string
          format: date-time
        encodingProfile:
          type: string
        revision:
          type: integer
          minimum: 1
        channel:
          oneOf:
            - $ref: '#/components/schemas/LiveEventChannel'
            - type: 'null'
        watchUrl:
          oneOf:
            - type: string
              format: uri
            - type: 'null'
    LiveEventChannel:
      type: object
      additionalProperties: false
      required: [id, streamSessionId, preBufferSeconds, postBufferSeconds]
      properties:
        id:
          type: string
          format: uuid
        streamSessionId:
          type: string
          format: uuid
        preBufferSeconds:
          type: integer
          minimum: 0
        postBufferSeconds:
          type: integer
          minimum: 0
    LiveEventEnvelope:
      type: object
      additionalProperties: false
      required: [data, requestId]
      properties:
        data:
          $ref: '#/components/schemas/LiveEvent'
        requestId:
          $ref: '#/components/schemas/RequestId'
    EncoderBootstrap:
      type: object
      additionalProperties: false
      required: [channelId, ingestServer, streamKey, channelCredentialVersion]
      properties:
        channelId:
          type: string
          format: uuid
        ingestServer:
          type: string
          format: uri
          pattern: '^rtmps://'
        streamKey:
          type: string
          readOnly: true
          description: Returned only for the claimable credential version. Never log it.
        channelCredentialVersion:
          type: integer
          minimum: 1
    EncoderBootstrapEnvelope:
      type: object
      additionalProperties: false
      required: [data, requestId]
      properties:
        data:
          $ref: '#/components/schemas/EncoderBootstrap'
        requestId:
          $ref: '#/components/schemas/RequestId'
    EncoderPollRequest:
      type: object
      additionalProperties: false
      required: [reportedState]
      properties:
        reportedState:
          type: string
          enum: [idle, streaming]
        appliedInstructionRevision:
          oneOf:
            - type: string
              minLength: 1
              maxLength: 200
            - type: 'null'
    EncoderInstruction:
      type: string
      enum: [idle, start, continue, stop]
    EncoderInstructionReason:
      type: string
      enum:
        - scheduled_window
        - manual_start
        - no_scheduled_event
        - before_scheduled_window
        - event_finished_or_unpublished
        - previous_event_must_stop
    EncoderEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - contentId
        - title
        - scheduledStartAt
        - scheduledEndAt
        - instructionStartAt
        - instructionStopAt
        - activation
        - revisionSeed
      properties:
        id:
          type: string
          format: uuid
          description: StreamSession/Event occurrence identifier.
        contentId:
          type: string
          format: uuid
        title:
          type: string
        scheduledStartAt:
          type: string
          format: date-time
        scheduledEndAt:
          type: string
          format: date-time
        instructionStartAt:
          type: string
          format: date-time
        instructionStopAt:
          type: string
          format: date-time
        activation:
          type: string
          enum: [scheduled, manual_start]
        revisionSeed:
          type: string
          description: Opaque server value; do not parse.
    EncoderNextEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - contentId
        - title
        - scheduledStartAt
        - scheduledEndAt
        - instructionStartAt
        - revisionSeed
      properties:
        id:
          type: string
          format: uuid
        contentId:
          type: string
          format: uuid
        title:
          type: string
        scheduledStartAt:
          type: string
          format: date-time
        scheduledEndAt:
          type: string
          format: date-time
        instructionStartAt:
          type: string
          format: date-time
        revisionSeed:
          type: string
          description: Opaque server value; do not parse.
    EncoderPoll:
      type: object
      additionalProperties: false
      required:
        - instruction
        - instructionRevision
        - reason
        - pollAfterSeconds
        - serverTime
        - reportedState
        - channel
        - event
        - nextEvent
      properties:
        instruction:
          $ref: '#/components/schemas/EncoderInstruction'
        instructionRevision:
          type: string
          description: Opaque deduplication boundary; do not parse.
        reason:
          $ref: '#/components/schemas/EncoderInstructionReason'
        pollAfterSeconds:
          type: integer
          const: 60
        serverTime:
          type: string
          format: date-time
        reportedState:
          type: string
          enum: [idle, streaming]
        channel:
          type: object
          additionalProperties: false
          required: [id]
          properties:
            id:
              type: string
              format: uuid
        event:
          oneOf:
            - $ref: '#/components/schemas/EncoderEvent'
            - type: 'null'
        nextEvent:
          oneOf:
            - $ref: '#/components/schemas/EncoderNextEvent'
            - type: 'null'
    EncoderPollEnvelope:
      type: object
      additionalProperties: false
      required: [data, requestId]
      properties:
        data:
          $ref: '#/components/schemas/EncoderPoll'
        requestId:
          $ref: '#/components/schemas/RequestId'
    ErrorDetail:
      type: object
      additionalProperties: false
      required: [code, message, requestId]
      properties:
        code:
          type: string
        message:
          type: string
        requestId:
          $ref: '#/components/schemas/RequestId'
        fields:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
    DeviceUnavailableEnvelope:
      allOf:
        - $ref: '#/components/schemas/ErrorEnvelope'
        - type: object
          required: [fallback, retryAfterSeconds]
          properties:
            fallback:
              type: string
              const: preserve_current_state
            retryAfterSeconds:
              type: integer
              const: 60
  responses:
    ValidationFailed:
      description: Invalid JSON, query, header or request body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: VALIDATION_FAILED
              message: The request body is invalid
              requestId: 33333333-3333-4333-8333-333333333333
              fields:
                scheduledEndAt:
                  - Expected end must be after the scheduled start
    ApiClientAuthenticationFailed:
      description: ApiClient credential missing, invalid or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: API_CLIENT_AUTHENTICATION_FAILED
              message: The API client credential is invalid or revoked
              requestId: 33333333-3333-4333-8333-333333333333
    ApiClientScopeRequired:
      description: The credential lacks the required events:read or events:write scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: API_CLIENT_SCOPE_REQUIRED
              message: The API client requires the events:write scope
              requestId: 33333333-3333-4333-8333-333333333333
    DeviceAuthenticationFailed:
      description: Device credential missing, invalid or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: DEVICE_AUTHENTICATION_FAILED
              message: The Device credential is missing, invalid or revoked
              requestId: 33333333-3333-4333-8333-333333333333
    LiveEventNotFound:
      description: No accessible Event matched within the authenticated Tenant.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: LIVE_EVENT_NOT_FOUND
              message: The live event was not found
              requestId: 33333333-3333-4333-8333-333333333333
    LiveEventConflict:
      description: Idempotency, scheduling, assignment or revision conflict.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: LIVE_EVENT_CONFLICT
              message: Idempotency-Key was already used with a different request
              requestId: 33333333-3333-4333-8333-333333333333
    PreconditionRequired:
      description: If-Match is missing or does not contain a positive numeric revision.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: PRECONDITION_REQUIRED
              message: If-Match must contain the current numeric revision
              requestId: 33333333-3333-4333-8333-333333333333
    ApiClientRateLimited:
      description: ApiClient request limit exceeded.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: API_CLIENT_RATE_LIMITED
              message: The API client rate limit has been exceeded
              requestId: 33333333-3333-4333-8333-333333333333
    DeviceRateLimited:
      description: Device is polling too frequently.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: DEVICE_RATE_LIMITED
              message: The Device is polling too frequently
              requestId: 33333333-3333-4333-8333-333333333333
    EncoderBootstrapNotRequired:
      description: No unclaimed connection-details version is available.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: ENCODER_BOOTSTRAP_NOT_REQUIRED
              message: Connection details are not available for this Device credential version
              requestId: 33333333-3333-4333-8333-333333333333
    DeviceControlUnavailable:
      description: Preserve the current local state and retry after 60 seconds.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        Cache-Control:
          $ref: '#/components/headers/PrivateNoStore'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/DeviceUnavailableEnvelope'
          example:
            error:
              code: DEVICE_CONTROL_UNAVAILABLE
              message: No new instruction is available; preserve the current encoder state and retry
              requestId: 44444444-4444-4444-8444-444444444444
            fallback: preserve_current_state
            retryAfterSeconds: 60
    ProviderFailed:
      description: Media provider could not reveal the connection details.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: LIVE_CHANNEL_CREDENTIAL_PROVIDER_FAILED
              message: The live Channel credential provider operation failed
              requestId: 33333333-3333-4333-8333-333333333333
    InternalError:
      description: Unexpected temporary server failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: INTERNAL_ERROR
              message: An unexpected error occurred
              requestId: 33333333-3333-4333-8333-333333333333
