openapi: 3.0.3
info:
  title: AnyTV Provider API
  version: "1.0.0"
  description: >
    Preview your customers' devices, activate AnyTV Pro on them with your credits, and deliver,
    update and remove their playlists from your own system. Server to server only. Authenticate
    with an API key created in the provider console. Every route can also answer 400
    https_required, 401 invalid_api_key, 403 ip_not_allowed, 413 body_too_large,
    415 unsupported_media_type, 429 rate_limited, 500 and 503 api_unavailable. A 5xx body from the
    network may not be JSON.
servers:
  - url: https://providers-api.anytvapp.com
security:
  - apiKey: []

tags:
  - name: Account
  - name: Devices
  - name: Activations
  - name: Playlists

paths:
  /v1/me:
    get:
      tags: [Account]
      summary: Check your key
      description: Needs no scope. The provider, the key, your caps and your capabilities right now.
      operationId: getMe
      responses:
        "200": { description: The caller., content: { application/json: { schema: { $ref: "#/components/schemas/Me" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/credits:
    get:
      tags: [Account]
      summary: Credit balance
      description: Scope credits:read.
      operationId: getCredits
      responses:
        "200": { description: The balance in units (10 units = 1 credit)., content: { application/json: { schema: { $ref: "#/components/schemas/Balance" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/credits/ledger:
    get:
      tags: [Account]
      summary: Credit ledger
      description: Scope credits:read. Every move of your credits, newest first.
      operationId: listLedger
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: A page of entries., content: { application/json: { schema: { $ref: "#/components/schemas/LedgerPage" } } } }
        "400": { $ref: "#/components/responses/BadList" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/devices/preview:
    post:
      tags: [Devices]
      summary: Preview a device
      description: Scope devices:read. Resolves the code on the TV to your device_ref and says what you can do with the device.
      operationId: previewDevice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string, example: "ABCD-EFGH", description: "Dashes, spaces and lower case are accepted." }
      responses:
        "200": { description: The preview., content: { application/json: { schema: { $ref: "#/components/schemas/Preview" } } } }
        "400": { description: invalid_target., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/DeviceNotFound" }

  /v1/devices/{device_ref}:
    get:
      tags: [Devices]
      summary: Retrieve a device
      description: Scope devices:read. The same preview, by the reference you saved.
      operationId: getDevice
      parameters:
        - { name: device_ref, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The preview., content: { application/json: { schema: { $ref: "#/components/schemas/Preview" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/DeviceNotFound" }

  /v1/activations:
    post:
      tags: [Activations]
      summary: Activate Pro
      description: >
        Scope activations:write. Idempotency-Key required. Unlocks AnyTV Pro on the device for the term with your credits.
        201 when active, 200 on a replay, 202 processing while billing confirms.
      operationId: createActivation
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyRequired"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [term]
              properties:
                code: { type: string, description: "The code on the TV. Send this or device_ref." }
                device_ref: { type: string }
                term: { type: string, enum: [P1Y, P2Y] }
                external_ref: { type: string, minLength: 1, maxLength: 128, description: "Your own reference. Not personal data." }
                confirm_already_pro: { type: boolean, description: "true to activate a device whose customer already pays for Pro." }
      responses:
        "201": { description: Active., content: { application/json: { schema: { $ref: "#/components/schemas/Activation" } } } }
        "200": { description: Replayed (Idempotent-Replayed true)., content: { application/json: { schema: { $ref: "#/components/schemas/Activation" } } } }
        "202": { description: Processing., content: { application/json: { schema: { $ref: "#/components/schemas/Processing" } } } }
        "400": { description: invalid_target, invalid_term, invalid_external_ref, idempotency_key_required, invalid_idempotency_key., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "402": { description: insufficient_credits., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { $ref: "#/components/responses/DeviceNotFound" }
        "409": { description: already_activated_by_you, device_has_live_activation, already_pro_confirmation_required, device_blocked_provider, activation_failed, request_in_progress., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "422": { description: platform_not_available, idempotency_key_reused., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "429": { $ref: "#/components/responses/Capped" }
        "503": { $ref: "#/components/responses/Paused" }
    get:
      tags: [Activations]
      summary: List activations
      description: Scope activations:read. Newest first.
      operationId: listActivations
      parameters:
        - { name: status, in: query, schema: { $ref: "#/components/schemas/ActivationStatus" } }
        - { name: expiring_before, in: query, schema: { type: string, format: date-time } }
        - { name: external_ref, in: query, schema: { type: string } }
        - { name: device_ref, in: query, schema: { type: string } }
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: A page., content: { application/json: { schema: { $ref: "#/components/schemas/ActivationPage" } } } }
        "400": { $ref: "#/components/responses/BadList" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/activations/{id}:
    get:
      tags: [Activations]
      summary: Retrieve an activation
      description: Scope activations:read.
      operationId: getActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }]
      responses:
        "200": { description: The activation., content: { application/json: { schema: { $ref: "#/components/schemas/Activation" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/ActivationNotFound" }
    delete:
      tags: [Activations]
      summary: Revoke (alias)
      description: Scope activations:write. Idempotency-Key required. The same as POST /v1/activations/{id}/revoke.
      operationId: deleteActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }, { $ref: "#/components/parameters/IdempotencyKeyRequired" }]
      responses:
        "200": { description: Revoked., content: { application/json: { schema: { $ref: "#/components/schemas/Activation" } } } }
        "202": { description: Processing., content: { application/json: { schema: { $ref: "#/components/schemas/Processing" } } } }
        "404": { $ref: "#/components/responses/ActivationNotFound" }
        "409": { description: activation_not_active., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

  /v1/activations/{id}/extend:
    post:
      tags: [Activations]
      summary: Extend
      description: Scope activations:write. Idempotency-Key required. Adds a term after the current one.
      operationId: extendActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }, { $ref: "#/components/parameters/IdempotencyKeyRequired" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [term], properties: { term: { type: string, enum: [P1Y, P2Y] } } }
      responses:
        "200": { description: Extended., content: { application/json: { schema: { $ref: "#/components/schemas/Activation" } } } }
        "202": { description: Processing., content: { application/json: { schema: { $ref: "#/components/schemas/Processing" } } } }
        "402": { description: insufficient_credits., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { $ref: "#/components/responses/ActivationNotFound" }
        "409": { description: activation_not_active, extension_in_progress., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "503": { $ref: "#/components/responses/Paused" }

  /v1/activations/{id}/transfer:
    post:
      tags: [Activations]
      summary: Move to a new device
      description: Scope activations:write. Idempotency-Key required. The remaining term moves to a new activation on the new device.
      operationId: transferActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }, { $ref: "#/components/parameters/IdempotencyKeyRequired" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string, description: "The new device's code. Send this or device_ref." }
                device_ref: { type: string }
                keep_on_old_device: { type: boolean, default: false }
      responses:
        "200": { description: Moved; the new activation with previous_activation_id., content: { application/json: { schema: { allOf: [{ $ref: "#/components/schemas/Activation" }, { type: object, properties: { previous_activation_id: { type: string } } }] } } } }
        "202": { description: Processing., content: { application/json: { schema: { $ref: "#/components/schemas/Processing" } } } }
        "400": { description: invalid_target., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { description: activation_not_found, device_not_found., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "409": { description: transfer_limit_reached (with resets_at), same_device, activation_not_transferable, device_has_live_activation, device_blocked_provider., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

  /v1/activations/{id}/undo:
    post:
      tags: [Activations]
      summary: Undo
      description: Scope activations:write. Idempotency-Key required. Within 72 hours; the credits come back.
      operationId: undoActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }, { $ref: "#/components/parameters/IdempotencyKeyRequired" }]
      responses:
        "200": { description: Undone, with refunded_units., content: { application/json: { schema: { allOf: [{ $ref: "#/components/schemas/Activation" }, { type: object, properties: { refunded_units: { type: integer } } }] } } } }
        "202": { description: Processing., content: { application/json: { schema: { $ref: "#/components/schemas/Processing" } } } }
        "404": { $ref: "#/components/responses/ActivationNotFound" }
        "409": { description: undo_window_closed, activation_transferred, device_already_undone, undo_cap_reached, activation_not_active., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

  /v1/activations/{id}/revoke:
    post:
      tags: [Activations]
      summary: Revoke
      description: Scope activations:write. Idempotency-Key required. Ends Pro early; no credits come back.
      operationId: revokeActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }, { $ref: "#/components/parameters/IdempotencyKeyRequired" }]
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object, properties: { remove_deliveries: { type: boolean, default: true } } }
      responses:
        "200": { description: Revoked, with removals_queued., content: { application/json: { schema: { allOf: [{ $ref: "#/components/schemas/Activation" }, { type: object, properties: { removals_queued: { type: integer } } }] } } } }
        "202": { description: Processing., content: { application/json: { schema: { $ref: "#/components/schemas/Processing" } } } }
        "404": { $ref: "#/components/responses/ActivationNotFound" }
        "409": { description: activation_not_active., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

  /v1/activations/{id}/deliveries:
    post:
      tags: [Playlists]
      summary: Send for an activation
      description: Scope deliveries:write. Idempotency-Key accepted. A playlist linked to an active activation of yours.
      operationId: sendDeliveryForActivation
      parameters: [{ $ref: "#/components/parameters/ActivationId" }, { $ref: "#/components/parameters/IdempotencyKeyOptional" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [playlist, name]
              properties:
                playlist: { $ref: "#/components/schemas/PlaylistInput" }
                name: { type: string, minLength: 1, maxLength: 120 }
                profile_hint: { type: string, maxLength: 60 }
      responses:
        "201": { description: Pending., content: { application/json: { schema: { $ref: "#/components/schemas/Delivery" } } } }
        "400": { description: invalid_playlist, invalid_name., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { $ref: "#/components/responses/ActivationNotFound" }
        "409": { description: activation_not_active, device_blocked_provider, playlist_limit_reached, host_on_hold., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "429": { $ref: "#/components/responses/Capped" }
        "503": { $ref: "#/components/responses/Paused" }

  /v1/deliveries:
    post:
      tags: [Playlists]
      summary: Send a playlist
      description: Scope deliveries:write. Idempotency-Key accepted. Free; no activation needed.
      operationId: sendDelivery
      parameters: [{ $ref: "#/components/parameters/IdempotencyKeyOptional" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [playlist, name]
              properties:
                code: { type: string, description: "The code on the TV. Send this or device_ref." }
                device_ref: { type: string }
                playlist: { $ref: "#/components/schemas/PlaylistInput" }
                name: { type: string, minLength: 1, maxLength: 120 }
                profile_hint: { type: string, maxLength: 60 }
      responses:
        "201": { description: Pending., content: { application/json: { schema: { $ref: "#/components/schemas/Delivery" } } } }
        "400": { description: invalid_target, invalid_playlist, invalid_name., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { $ref: "#/components/responses/DeviceNotFound" }
        "409": { description: device_blocked_provider, playlist_limit_reached, host_on_hold., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "429": { $ref: "#/components/responses/Capped" }
        "503": { $ref: "#/components/responses/Paused" }
    get:
      tags: [Playlists]
      summary: List playlists
      description: Scope deliveries:read.
      operationId: listDeliveries
      parameters:
        - { name: device_ref, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DeliveryStatus" } }
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: A page., content: { application/json: { schema: { $ref: "#/components/schemas/DeliveryPage" } } } }
        "400": { $ref: "#/components/responses/BadList" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/deliveries/bulk-host-update:
    post:
      tags: [Playlists]
      summary: Move playlists to a new host
      description: Scope deliveries:write. Preview with dry_run, then send the count back as expected_count. One run a minute, 50 playlists a run.
      operationId: bulkHostUpdate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from_host, to_host]
              properties:
                from_host: { type: string, example: "tv.fresh.example:8080" }
                to_host: { type: string, example: "tv2.fresh.example:8080" }
                dry_run: { type: boolean, default: false }
                expected_count: { type: integer, description: "Required when dry_run is false: the count from the dry run." }
      responses:
        "200": { description: Counted, or nothing moved., content: { application/json: { schema: { $ref: "#/components/schemas/BulkHostUpdate" } } } }
        "202": { description: Updates queued., content: { application/json: { schema: { $ref: "#/components/schemas/BulkHostUpdate" } } } }
        "400": { description: invalid_from_host, invalid_to_host, same_host., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { description: nothing_matched., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "409": { description: count_mismatch., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "429": { $ref: "#/components/responses/Capped" }
        "503": { $ref: "#/components/responses/Paused" }

  /v1/deliveries/{id}:
    get:
      tags: [Playlists]
      summary: Retrieve a playlist
      description: Scope deliveries:read.
      operationId: getDelivery
      parameters: [{ $ref: "#/components/parameters/DeliveryId" }]
      responses:
        "200": { description: The delivery., content: { application/json: { schema: { $ref: "#/components/schemas/Delivery" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/DeliveryNotFound" }
    put:
      tags: [Playlists]
      summary: Update a playlist
      description: Scope deliveries:write. Idempotency-Key accepted. Replaces the playlist in place; the answer is the new delivery.
      operationId: updateDelivery
      parameters: [{ $ref: "#/components/parameters/DeliveryId" }, { $ref: "#/components/parameters/IdempotencyKeyOptional" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                playlist: { type: object, description: "Only the fields that change; the type cannot change." }
                name: { type: string, minLength: 1, maxLength: 120 }
                epg_url: { type: string, nullable: true }
      responses:
        "200": { description: The replacement delivery., content: { application/json: { schema: { $ref: "#/components/schemas/Delivery" } } } }
        "400": { description: invalid_playlist, invalid_name., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { $ref: "#/components/responses/Standing" }
        "404": { $ref: "#/components/responses/DeliveryNotFound" }
        "409": { description: delivery_not_live, replacement_pending, type_change_needs_remove_and_add., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    delete:
      tags: [Playlists]
      summary: Remove a playlist
      description: Scope deliveries:write. Idempotency-Key accepted. Works in every account state.
      operationId: removeDelivery
      parameters: [{ $ref: "#/components/parameters/DeliveryId" }, { $ref: "#/components/parameters/IdempotencyKeyOptional" }]
      responses:
        "200": { description: Cancelled or removal pending., content: { application/json: { schema: { $ref: "#/components/schemas/Delivery" } } } }
        "404": { $ref: "#/components/responses/DeliveryNotFound" }
        "409": { description: already_removed., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "An API key from the provider console: atv_live_<12 hex>_<64 hex>."

  parameters:
    ActivationId: { name: id, in: path, required: true, schema: { type: string } }
    DeliveryId: { name: id, in: path, required: true, schema: { type: string } }
    Cursor: { name: cursor, in: query, schema: { type: string }, description: "next_cursor from the previous page." }
    Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, pattern: "^[A-Za-z0-9_\\-:.]{8,128}$" }
      description: "Unique per operation, for example order-1042-activate. Remembered 24 hours."
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, pattern: "^[A-Za-z0-9_\\-:.]{8,128}$" }

  responses:
    Unauthorized: { description: invalid_api_key., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Forbidden: { description: insufficient_scope or ip_not_allowed., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Standing: { description: provider_not_active, approval_required, kyc_required, not_allowlisted, not_operator, email_unverified, terms_required, attestation_required, each with reason., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    DeviceNotFound: { description: device_not_found., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    ActivationNotFound: { description: activation_not_found., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    DeliveryNotFound: { description: delivery_not_found., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    BadList: { description: invalid_limit or invalid_cursor., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Capped: { description: rate_limited, new_device_cap_reached or activation_cap_reached, with retry_after., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Paused: { description: activations_disabled, deliveries_disabled or billing_unavailable., content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, description: "Stable. Branch on this." }
        message: { type: string, description: "For people. May be reworded." }
        retry_after: { type: integer, description: "Seconds to wait, also in the Retry-After header." }
        reason: { type: string, description: "Your standing's blocked reason, when that is the cause." }
        resets_at: { type: string, format: date-time, description: "On transfer_limit_reached." }

    Me:
      type: object
      properties:
        provider: { type: object, properties: { id: { type: string }, display_name: { type: string }, tier: { type: string, enum: [operator, reseller] }, status: { type: string } } }
        key: { type: object, properties: { id: { type: string }, name: { type: string }, scopes: { type: array, items: { $ref: "#/components/schemas/Scope" } }, expires_at: { type: string, format: date-time, nullable: true } } }
        limits: { type: object, properties: { new_devices_per_day: { type: integer }, activations_per_day: { type: integer }, live_deliveries_per_device: { type: integer } } }
        switches: { type: object, properties: { activations: { type: boolean } } }
        capabilities: { type: object, additionalProperties: { $ref: "#/components/schemas/CapabilityVerdict" } }

    Scope:
      type: string
      enum: [credits:read, devices:read, activations:read, activations:write, deliveries:read, deliveries:write]

    CapabilityVerdict:
      type: object
      required: [allowed]
      properties:
        allowed: { type: boolean }
        reason: { type: string, enum: [account_closed, suspended, frozen, email_unverified, terms_required, attestation_required, approval_pending, kyc_required, kyc_pending, kyc_rejected, not_allowlisted, program_paused, daily_delivery_cap, daily_activation_cap, insufficient_credits, not_operator] }

    Balance:
      type: object
      properties:
        available_units: { type: integer }
        reserved_units: { type: integer }
        credit_limit_units: { type: integer }
        available_credits: { type: string, example: "184.0" }

    LedgerPage:
      type: object
      properties:
        entries:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              kind: { type: string, enum: [purchase, reserve, commit, release, refund_undo, adjustment, forfeit, purchase_refund, transfer_in, transfer_out] }
              units: { type: integer, description: "Signed change of the available units." }
              reserved_units: { type: integer, description: "Signed change of the reserved units." }
              activation_id: { type: string, nullable: true }
              purchase_id: { type: string, nullable: true }
              reason: { type: string, nullable: true }
              created_at: { type: string, format: date-time }
        next_cursor: { type: string, nullable: true }

    Preview:
      type: object
      properties:
        device_ref: { type: string }
        device:
          type: object
          properties:
            platform: { type: string }
            model: { type: string, nullable: true }
            app_version: { type: string, nullable: true }
            store: { type: string, nullable: true }
            install_source: { type: string, nullable: true }
            store_country: { type: string, nullable: true }
        pro: { type: object, properties: { from_customer: { type: boolean } } }
        activation:
          type: object
          properties:
            live: { type: boolean }
            by_you: { type: boolean }
            id: { type: string }
            expires_at: { type: string, format: date-time, nullable: true }
        blocked_by_customer: { type: boolean }
        eligible:
          type: object
          properties:
            activate: { type: boolean }
            reason: { type: string, enum: [already_activated_by_you, device_has_live_activation, device_blocked_provider, platform_not_available] }
            requires_confirmation: { type: boolean }

    ActivationStatus:
      type: string
      enum: [reserved, active, failed, transferring, revoke_pending, revoked, undone, released, transferred, expired]

    Activation:
      type: object
      properties:
        id: { type: string }
        status: { $ref: "#/components/schemas/ActivationStatus" }
        device_ref: { type: string }
        starts_at: { type: string, format: date-time, nullable: true }
        expires_at: { type: string, format: date-time, nullable: true }
        external_ref: { type: string, nullable: true }
        confirmed_already_pro: { type: boolean }
        transferred_from: { type: string, nullable: true }
        transferred_to: { type: string, nullable: true }
        revoke_reason: { type: string, nullable: true }
        undo_deadline: { type: string, format: date-time, nullable: true }
        terms:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              kind: { type: string, enum: [initial, extension] }
              term: { type: string, enum: [P1Y, P2Y] }
              units: { type: integer }
              state: { type: string }
              expires_at: { type: string, format: date-time, nullable: true }
              committed_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        ended_at: { type: string, format: date-time, nullable: true }

    ActivationPage:
      type: object
      properties:
        activations: { type: array, items: { $ref: "#/components/schemas/Activation" } }
        next_cursor: { type: string, nullable: true }

    Processing:
      type: object
      properties:
        status: { type: string, enum: [processing] }
        activation: { $ref: "#/components/schemas/Activation" }

    PlaylistInput:
      oneOf:
        - type: object
          required: [type, url]
          properties:
            type: { type: string, enum: [m3u] }
            url: { type: string, maxLength: 2048, description: "http or https, no credentials in the URL." }
            epgUrl: { type: string, maxLength: 2048 }
        - type: object
          required: [type, server, username, password]
          properties:
            type: { type: string, enum: [xtream] }
            server: { type: string, maxLength: 2048, example: "http://tv.fresh.example:8080" }
            username: { type: string, maxLength: 128 }
            password: { type: string, maxLength: 128 }
            epgUrl: { type: string, maxLength: 2048 }

    DeliveryStatus:
      type: string
      enum: [pending, delivered, removal_pending, removed, removed_by_user, cancelled, blocked, expired, superseded, failed]

    Delivery:
      type: object
      properties:
        id: { type: string }
        source: { type: string, enum: [provider, customer] }
        device_ref: { type: string, nullable: true }
        activation_id: { type: string, nullable: true }
        replaces_delivery_id: { type: string, nullable: true }
        type: { type: string, enum: [m3u, xtream] }
        name: { type: string }
        host: { type: string, nullable: true }
        status: { $ref: "#/components/schemas/DeliveryStatus" }
        status_reason: { type: string, nullable: true }
        failure_class: { type: string, nullable: true, enum: [auth, unreachable, parse, limit, unsupported, other, null] }
        required_capability: { type: integer }
        last_ack: { type: string, nullable: true }
        last_ack_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        delivered_at: { type: string, format: date-time, nullable: true }
        removed_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time }

    DeliveryPage:
      type: object
      properties:
        deliveries: { type: array, items: { $ref: "#/components/schemas/Delivery" } }
        next_cursor: { type: string, nullable: true }

    BulkHostUpdate:
      type: object
      properties:
        matched: { type: integer }
        updated: { type: integer }
        skipped: { type: integer }
        remaining: { type: integer }
