openapi: 3.1.0
info:
  title: Ignite REST API
  version: "1.0.0"
  summary: Upload, manage and deliver videos with Ignite.
  description: |
    OpenAPI specification for the Ignite REST API (video hosting and streaming).
    Human-readable documentation with examples: https://docs.ignite.video

    **Authentication:** Create an API key on the API Integrations page in the Ignite admin UI
    (admin role required) and send it as a Bearer token. A missing or invalid key is rejected
    with **403** and `{ "errors": [{ "message": "You are not allowed to perform this action" }] }`.
    The API does not return 401 for API keys.

    **Permissions** are granted per resource: `videos.read|create|update|delete|publish`,
    `categories.create|update|delete`, `tags.create|update|delete`. Read access to categories
    and tags is always granted. `videos.publish` is separate from create and update.

    **Publishing:** `visibility: public` is stored only when the key has `videos.publish`.
    Without it, create stores `private`, and a PATCH that sets `visibility` is accepted but
    the field is left unchanged. Neither case returns 403.

    **Category scope:** A key can optionally be limited to one or more categories. The scope
    includes those categories and their subcategories. It filters video reads and writes, and
    category reads, updates, and deletes. A scoped key cannot delete its own scope anchors.
    Creating a video with no categories assigns the scope categories (up to 10). Tags are not
    category-scoped.

    **Upload flow:** `PUT /videos/upload` creates the video (`status: NO_FILE`) and returns a
    signed URL that expires after 1 hour. `PUT` the file to that URL with the file MIME type
    as `Content-Type`. For large files, set `useMultipart: true` (recommended above 100 MB;
    the API does not enforce that threshold). Part URLs expire after 6 hours. Encoding starts
    after the upload finishes; poll `GET /videos/{videoId}`.
  contact:
    name: Ignite
    url: https://ignite.video/en/contact
servers:
  - url: https://app.ignitevideo.cloud/api
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Videos
  - name: Upload
    description: Single and multipart upload, file replacement
  - name: Thumbnails
  - name: Text tracks
    description: Subtitles and captions (WebVTT). Requires a video that has finished encoding.
  - name: Categories
  - name: Tags

paths:
  /videos:
    get:
      tags: [Videos]
      operationId: listVideos
      summary: List videos
      description: |
        Paginated list. Requires `videos.read`. API keys can read videos in `NO_FILE`.
        Default `limit` is 10. When `depth` is omitted, `categories` and `tags` are objects;
        `depth=0` returns ids. `workspace` is always the workspace id.
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Sort"
        - $ref: "#/components/parameters/Where"
        - $ref: "#/components/parameters/Depth"
      responses:
        "200":
          description: Paginated list of videos
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          $ref: "#/components/schemas/Video"
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/{videoId}:
    parameters:
      - $ref: "#/components/parameters/VideoId"
    get:
      tags: [Videos]
      operationId: getVideo
      summary: Get a video
      description: Requires `videos.read`. A missing id returns 404.
      parameters:
        - $ref: "#/components/parameters/Depth"
      responses:
        "200":
          description: The video object
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Video"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Videos]
      operationId: updateVideo
      summary: Update a video
      description: |
        Requires `videos.update`. Send only the fields to change.

        An API key can write: `title` (max 150), `description` (max 2000), `language`,
        `aiContent`, `categories`, `tags`, `customMetadata`, `chapters`,
        `autoTranscription.autoStart`, `player.useCustomDesign`, `player.selectedPreset`,
        `player.playerDesign`, and, when the workspace has password protection,
        `passwordProtection`. `visibility` is written only with `videos.publish`; otherwise
        it is ignored and the response is still 200.

        Unknown category or tag ids are dropped. If none remain, the field is `null`.
        `texttracks`, `audiotracks`, `status`, and file fields are ignored.
        A missing id returns 403, not 404.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VideoUpdate"
            example:
              title: New video title
      responses:
        "200":
          description: Updated video
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoDocMessage"
              example:
                doc:
                  id: 507f1f77bcf86cd799439011
                  title: New video title
                message: Updated successfully.
        "400":
          $ref: "#/components/responses/ValidationError"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Videos]
      operationId: deleteVideo
      summary: Delete a video
      description: |
        Requires `videos.delete`. Returns `{ doc, message }` with
        `message` = `Deleted successfully.`. A missing id returns 403, not 404.
      responses:
        "200":
          description: The deleted video
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VideoDocMessage"
              example:
                doc:
                  id: 507f1f77bcf86cd799439011
                message: Deleted successfully.
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/upload:
    put:
      tags: [Upload]
      operationId: createVideo
      summary: Create a video and request an upload URL
      description: |
        Requires `videos.create`. `title` is required, trimmed, and cut to 100 characters.
        Optional: `description` (max 2000), `visibility`, `categories` (max 10 ids),
        `tags` (max 20 ids), `language`, `autoTranscribe`, `customMetadata`, `duration`,
        `fps`, `orientation`, `useMultipart`.

        `visibility: public` without `videos.publish` is stored as `private`.
        `autoTranscribe: true` requires a supported `language` (for example `de-DE`).
        There is no `mimeType` field; set `Content-Type` on the upload `PUT` to the signed URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VideoCreate"
            examples:
              single:
                summary: Single-file upload
                value:
                  title: Your video title
                  visibility: private
              multipart:
                summary: Multipart upload
                value:
                  title: Your video title
                  useMultipart: true
                  categories: [category-id]
                  tags: [tag-id]
              autoTranscribe:
                summary: Start AI transcription after encoding
                value:
                  title: Your video title
                  language: de-DE
                  autoTranscribe: true
      responses:
        "200":
          description: Video created and upload instructions returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadInitResponse"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/upload/{videoId}:
    parameters:
      - $ref: "#/components/parameters/VideoId"
    put:
      tags: [Upload]
      operationId: replaceVideoFile
      summary: Replace the video file
      description: |
        Requires `videos.update` on that video. Sets `status` to `REPLACING` and returns a
        signed URL, or multipart identifiers when the body sets `useMultipart: true`.
        The body is optional. A missing video returns 400 `{ "error": "not_found" }`,
        not 404. An id that is not 24 hex characters returns 400 `{ "error": "invalid_video_id" }`.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                useMultipart:
                  type: boolean
      responses:
        "200":
          description: Upload instructions for the replacement file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadInitResponse"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/upload/s3/multipart/{uploadId}/prepare-parts:
    parameters:
      - $ref: "#/components/parameters/UploadId"
      - $ref: "#/components/parameters/S3Key"
    post:
      tags: [Upload]
      operationId: prepareMultipartParts
      summary: Get signed URLs for multipart parts
      description: |
        Requires `videos.update` on the video identified by `key` (`videos/{videoId}/video`).
        Returns up to 100 part URLs. Each URL expires after 6 hours (`expires` is seconds).
        Upload each part with `PUT`. Every part except the last must be at least 5 MB.
        Keep the `ETag` response header, including quotes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [startPart, endPart]
                  properties:
                    startPart:
                      type: integer
                      minimum: 1
                    endPart:
                      type: integer
                      minimum: 1
                - type: object
                  required: [partNumbers]
                  properties:
                    partNumbers:
                      type: array
                      maxItems: 100
                      items:
                        type: integer
                        minimum: 1
                        maximum: 10000
            example:
              startPart: 1
              endPart: 2
      responses:
        "200":
          description: Signed part URLs
          content:
            application/json:
              schema:
                type: object
                required: [parts, expires]
                properties:
                  parts:
                    type: array
                    items:
                      type: object
                      required: [partNumber, url]
                      properties:
                        partNumber:
                          type: integer
                        url:
                          type: string
                          format: uri
                  expires:
                    type: integer
                    description: Lifetime of each part URL, in seconds (21600).
                    example: 21600
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/upload/s3/multipart/{uploadId}/complete:
    parameters:
      - $ref: "#/components/parameters/UploadId"
      - $ref: "#/components/parameters/S3Key"
    post:
      tags: [Upload]
      operationId: completeMultipartUpload
      summary: Complete a multipart upload
      description: |
        Requires `videos.update` on the video in `key`. Pass `parts` sorted by part number,
        or `fetchPartsFromStorage: true` to let the server read the parts from storage.
        Encoding starts after a successful complete.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [parts]
                  properties:
                    parts:
                      type: array
                      items:
                        type: object
                        required: [PartNumber, ETag]
                        properties:
                          PartNumber:
                            type: integer
                          ETag:
                            type: string
                            description: Value of the part `ETag` header, including quotes.
                - type: object
                  required: [fetchPartsFromStorage]
                  properties:
                    fetchPartsFromStorage:
                      type: boolean
                      const: true
      responses:
        "200":
          description: Upload completed
          content:
            application/json:
              schema:
                type: object
                required: [success, key, etag]
                properties:
                  success:
                    type: boolean
                    const: true
                  key:
                    type: string
                  etag:
                    type: string
                    description: S3 ETag of the assembled object. Includes `-{partCount}` for multipart.
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          description: S3 could not complete the upload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EndpointError"

  /videos/{videoId}/thumbnail:
    parameters:
      - $ref: "#/components/parameters/VideoId"
    put:
      tags: [Thumbnails]
      operationId: uploadThumbnail
      summary: Upload a custom thumbnail
      description: |
        Requires `videos.update`. The video must already be encoded (`status: COMPLETE`).
        Send a JPEG or PNG as multipart field `file`. The image is resized to variants up to
        the video's resolution. The response is the video object itself, not `{ doc, message }`.
        A video that is still encoding, or a missing video, returns 400.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
            encoding:
              file:
                contentType: image/jpeg, image/png
      responses:
        "200":
          description: The video, with updated `src.thumbnails`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Video"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Thumbnails]
      operationId: resetThumbnail
      summary: Reset the thumbnail
      description: |
        Requires `videos.update` and an encoded video. Restores the generated thumbnail.
        Returns the video object. A missing video returns 400.
      responses:
        "200":
          description: The video, with restored `src.thumbnails`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Video"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/{videoId}/texttracks:
    parameters:
      - $ref: "#/components/parameters/VideoId"
    put:
      tags: [Text tracks]
      operationId: createTextTrack
      summary: Create a text track
      description: |
        Requires `videos.update` and an encoded video. `language` must be a supported locale
        such as `en-US` or `de-DE` (`en` is rejected). `type` is `subtitles` or `captions`.
        Send WebVTT as JSON `content` or as multipart field `file`.

        The 200 body is the track. Its `content` property is a deprecated placeholder, not the
        uploaded VTT. Use `GET /videos/{videoId}/texttracks/{texttrackId}` for the real file.
        A missing or not-yet-encoded video returns 400, not 404.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TextTrackInput"
            example:
              language: en-US
              type: captions
              content: "WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nHello"
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/TextTrackFileInput"
      responses:
        "200":
          description: The created text track
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TextTrack"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /videos/{videoId}/texttracks/{texttrackId}:
    parameters:
      - $ref: "#/components/parameters/VideoId"
      - name: texttrackId
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Text tracks]
      operationId: getTextTrack
      summary: Get a text track, including VTT content
      description: |
        Requires `videos.read`. `content` is the WebVTT text. A missing track returns 400,
        not 404.
      responses:
        "200":
          description: The text track, with VTT `content`
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TextTrack"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"
    patch:
      tags: [Text tracks]
      operationId: updateTextTrack
      summary: Update a text track
      description: |
        Requires `videos.update`. `language`, `type`, and `content` are all optional.
        A metadata-only body (for example `{ "type": "subtitles" }`) is valid.
        The response `content` is the deprecated placeholder. Use GET for the VTT.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TextTrackPatch"
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/TextTrackFilePatch"
      responses:
        "200":
          description: The updated text track
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TextTrack"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Text tracks]
      operationId: deleteTextTrack
      summary: Delete a text track
      description: |
        Requires `videos.update` and an encoded video. Returns the video object
        (not `{ doc, message }`) with the track removed. A missing track returns 400.
      responses:
        "200":
          description: The video, without the deleted track
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Video"
        "400":
          $ref: "#/components/responses/EndpointError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /categories:
    get:
      tags: [Categories]
      operationId: listCategories
      summary: List categories
      description: |
        Read access is always granted. A category-scoped key only sees its scope and
        subcategories. Default sort is `title`. Default `limit` is 10.
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Sort"
        - $ref: "#/components/parameters/Where"
        - $ref: "#/components/parameters/Depth"
      responses:
        "200":
          description: Paginated list of categories
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          $ref: "#/components/schemas/Category"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Categories]
      operationId: createCategory
      summary: Create a category
      description: |
        Requires `categories.create`. `title` is required (max 100). `slug` is generated from
        `title` when omitted. `description` max 2000. `parent` is a category id.
        Returns 201 `{ doc, message }` with `message` = `Category successfully created.`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CategoryInput"
            example:
              title: Your category title
              description: Optional description
      responses:
        "201":
          description: The created category
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryDocMessage"
              example:
                doc:
                  id: 507f1f77bcf86cd799439012
                  title: Your category title
                  slug: your-category-title
                message: Category successfully created.
        "400":
          $ref: "#/components/responses/ValidationError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /categories/{categoryId}:
    parameters:
      - $ref: "#/components/parameters/CategoryId"
    get:
      tags: [Categories]
      operationId: getCategory
      summary: Get a category
      description: Returns the category. A missing id returns 404.
      parameters:
        - $ref: "#/components/parameters/Depth"
      responses:
        "200":
          description: The category
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Category"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Categories]
      operationId: updateCategory
      summary: Update a category
      description: |
        Requires `categories.update`. Writable fields are `title`, `slug`, `description`,
        and `parent`. Returns `{ doc, message }` with `message` = `Updated successfully.`
        A missing id returns 403, not 404.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CategoryInput"
      responses:
        "200":
          description: Updated category
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryDocMessage"
        "400":
          $ref: "#/components/responses/ValidationError"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Categories]
      operationId: deleteCategory
      summary: Delete a category
      description: |
        Requires `categories.delete`. Returns `{ doc, message }` with
        `message` = `Deleted successfully.` A scoped key cannot delete a category that is
        one of its scope anchors. A missing id returns 403, not 404.
      responses:
        "200":
          description: Deleted category
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CategoryDocMessage"
        "403":
          $ref: "#/components/responses/Forbidden"

  /tags:
    get:
      tags: [Tags]
      operationId: listTags
      summary: List tags
      description: Read access is always granted. Tags are not affected by category scope.
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Sort"
        - $ref: "#/components/parameters/Where"
      responses:
        "200":
          description: Paginated list of tags
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      docs:
                        type: array
                        items:
                          $ref: "#/components/schemas/Tag"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Tags]
      operationId: createTag
      summary: Create a tag
      description: |
        Requires `tags.create`. `title` is required. `slug` is generated from `title` when
        omitted. Returns 201 `{ doc, message }` with `message` = `Tag successfully created.`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TagInput"
            example:
              title: Your tag title
      responses:
        "201":
          description: The created tag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagDocMessage"
        "400":
          $ref: "#/components/responses/ValidationError"
        "403":
          $ref: "#/components/responses/Forbidden"

  /tags/{tagId}:
    parameters:
      - $ref: "#/components/parameters/TagId"
    get:
      tags: [Tags]
      operationId: getTag
      summary: Get a tag
      responses:
        "200":
          description: The tag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Tag"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Tags]
      operationId: updateTag
      summary: Update a tag
      description: |
        Requires `tags.update`. Writable fields are `title`, `slug`, and `description`.
        Returns `{ doc, message }`. A missing id returns 403, not 404.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TagInput"
      responses:
        "200":
          description: Updated tag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagDocMessage"
        "400":
          $ref: "#/components/responses/ValidationError"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Tags]
      operationId: deleteTag
      summary: Delete a tag
      description: |
        Requires `tags.delete`. Returns `{ doc, message }` with
        `message` = `Deleted successfully.` A missing id returns 403, not 404.
      responses:
        "200":
          description: Deleted tag
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagDocMessage"
        "403":
          $ref: "#/components/responses/Forbidden"

webhooks:
  videoEvent:
    post:
      summary: Video lifecycle event
      description: |
        Webhook subscriptions are configured in the Ignite admin UI, not via this API.
        Ignite sends a `POST` whose body is the video object without `player`.
        Event types: `video.created`, `video.updated` (metadata, encoding progress, and
        completion), `video.deleted` (the object as it existed before deletion).

        Verify `X-Webhook-Signature` (`t=<unix ms>,v1=<hex>`):
        `HMAC-SHA256(t + "." + raw_body, signing_secret)`.
      parameters:
        - name: X-Webhook-Event
          in: header
          required: true
          schema:
            type: string
            enum: [video.created, video.updated, video.deleted]
        - name: X-Webhook-Signature
          in: header
          required: true
          schema:
            type: string
            example: t=1705316400000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Video"
      responses:
        "200":
          description: Return any 2xx status to acknowledge receipt.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key from the Ignite admin UI (API Integrations).

  parameters:
    VideoId:
      name: videoId
      in: path
      required: true
      schema:
        type: string
        pattern: "^[a-fA-F0-9]{24}$"
      example: 507f1f77bcf86cd799439011
    CategoryId:
      name: categoryId
      in: path
      required: true
      schema:
        type: string
        pattern: "^[a-fA-F0-9]{24}$"
    TagId:
      name: tagId
      in: path
      required: true
      schema:
        type: string
        pattern: "^[a-fA-F0-9]{24}$"
    UploadId:
      name: uploadId
      in: path
      required: true
      description: "`multipartUpload.uploadId` from `PUT /videos/upload`"
      schema:
        type: string
    S3Key:
      name: key
      in: query
      required: true
      description: "`multipartUpload.key` from `PUT /videos/upload` (URL-encoded). Must match `videos/{videoId}/video`."
      schema:
        type: string
    Page:
      name: page
      in: query
      description: Page number. Default 1.
      schema:
        type: integer
        minimum: 1
    Limit:
      name: limit
      in: query
      description: Page size. Default 10.
      schema:
        type: integer
        minimum: 1
    Sort:
      name: sort
      in: query
      description: Field to sort by. Prefix with `-` for descending order, e.g. `-createdAt`.
      schema:
        type: string
    Depth:
      name: depth
      in: query
      description: |
        Relationship population. Omit it to receive `categories` and `tags` as objects.
        `0` returns ids only. `workspace` stays an id either way.
      schema:
        type: integer
        minimum: 0
    Where:
      name: where
      in: query
      description: |
        Filter in bracket notation, e.g. `where[status][equals]=COMPLETE`.
        See https://docs.ignite.video/api-reference/querying for operators.
      style: deepObject
      explode: true
      schema:
        type: object
        additionalProperties: true

  responses:
    Forbidden:
      description: |
        Missing or invalid API key, missing permission, or outside category scope.
        For collection update and delete, a missing id also returns this status.
        Body is `{ "errors": [{ "message": "You are not allowed to perform this action" }] }`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PayloadError"
    NotFound:
      description: |
        GET of a missing document. Body is `{ "errors": [{ "message": "Not Found" }] }`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PayloadError"
    ValidationError:
      description: |
        A field failed validation. Body is `{ "errors": [{ "message": "<detail>" }] }`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PayloadError"
    EndpointError:
      description: |
        Custom endpoint error. Body is `{ "error": "<code or sentence>", "message"?: "<detail>" }`.
        Permission failures use 403.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/EndpointError"

  schemas:
    PayloadError:
      type: object
      required: [errors]
      properties:
        errors:
          type: array
          items:
            type: object
            required: [message]
            properties:
              message:
                type: string
    EndpointError:
      type: object
      required: [error]
      properties:
        error:
          type: string
        message:
          type: string
        success:
          type: boolean
    PaginatedResponse:
      type: object
      required: [docs, totalDocs, limit, totalPages, page, pagingCounter, hasPrevPage, hasNextPage]
      properties:
        docs:
          type: array
          items: {}
        totalDocs:
          type: integer
        limit:
          type: integer
        totalPages:
          type: integer
        page:
          type: integer
        pagingCounter:
          type: integer
        hasPrevPage:
          type: boolean
        hasNextPage:
          type: boolean
        prevPage:
          type: [integer, "null"]
        nextPage:
          type: [integer, "null"]
    VideoStatus:
      type: string
      enum: [NO_FILE, PROGRESSING, REPLACING, COMPLETE, ERROR]
      description: |
        NO_FILE: object exists, no file uploaded yet. PROGRESSING: encoding in progress.
        REPLACING: replacement file is being uploaded. COMPLETE: playback sources are in `src`.
        ERROR: encoding failed. See `errorCode` and `errorMessage`.
    Visibility:
      type: string
      enum: [public, private]
    CustomMetadata:
      type: object
      maxProperties: 50
      propertyNames:
        pattern: "^[a-zA-Z0-9_-]+$"
        maxLength: 40
      description: |
        Flat object, at most 50 keys. Keys match `^[a-zA-Z0-9_-]+$` and are at most 40
        characters. Values are strings (max 500), finite numbers, or booleans. Nested objects
        and arrays are rejected. A later write merges onto the stored object. Sending `""`
        or `null` for a key deletes that key. Omitted keys are kept.
      additionalProperties:
        oneOf:
          - type: string
            maxLength: 500
          - type: number
          - type: boolean
          - type: "null"
            description: Deletes the key on write.
    VideoCreate:
      type: object
      required: [title]
      properties:
        title:
          type: string
          description: Trimmed and cut to 100 characters.
        description:
          type: string
          maxLength: 2000
        visibility:
          $ref: "#/components/schemas/Visibility"
        categories:
          type: array
          maxItems: 10
          description: Category ids. Unknown ids are dropped.
          items:
            type: string
        tags:
          type: array
          maxItems: 20
          description: Tag ids. Unknown ids are dropped.
          items:
            type: string
        language:
          type: string
          description: Spoken-language tag, for example `de-DE` or `en-US`.
          example: de-DE
        autoTranscribe:
          type: boolean
          description: Start AI transcription after encoding. Requires a supported `language`.
        customMetadata:
          $ref: "#/components/schemas/CustomMetadata"
        duration:
          type: number
          description: Optional hint in seconds. Encoding overwrites it.
        fps:
          type: number
          description: Optional hint. Encoding overwrites it.
        orientation:
          type: string
          enum: [landscape, portrait]
          description: Optional hint. Encoding overwrites it.
        useMultipart:
          type: boolean
          description: Return multipart upload identifiers instead of a single signed URL.
    VideoUpdate:
      type: object
      description: Send only the fields to change. Other properties are ignored.
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 150
        description:
          type: string
          maxLength: 2000
        visibility:
          allOf:
            - $ref: "#/components/schemas/Visibility"
          description: Applied only when the key has `videos.publish`.
        categories:
          type: array
          description: Replaces the category list. Unknown ids are dropped. An empty result is stored as null.
          items:
            type: string
        tags:
          type: array
          description: Replaces the tag list. Unknown ids are dropped. An empty result is stored as null.
          items:
            type: string
        language:
          type: string
          description: Spoken-language tag, for example `en-US`. The API currently stores other strings as well.
        aiContent:
          type: string
          enum: [none, generated, modified]
        customMetadata:
          $ref: "#/components/schemas/CustomMetadata"
        chapters:
          type: array
          items:
            $ref: "#/components/schemas/ChapterInput"
        autoTranscription:
          type: object
          properties:
            autoStart:
              type: boolean
              description: Start AI transcription after the next encode when true.
        player:
          type: object
          properties:
            useCustomDesign:
              type: boolean
            selectedPreset:
              type: [string, "null"]
              description: Player preset id.
            playerDesign:
              type: object
              additionalProperties: true
        passwordProtection:
          type: object
          description: Written only when the workspace has video password protection.
          properties:
            enabled:
              type: boolean
            password:
              type: string
              minLength: 8
    UploadInitResponse:
      type: object
      required: [videoId]
      properties:
        videoId:
          type: string
        title:
          type: string
          description: Present on create. The stored title, after trim and the 100-character cut.
        signedUrl:
          type: string
          format: uri
          description: Present for a single-file upload. Expires after 1 hour. PUT the bytes with the file MIME type as Content-Type.
        multipartUpload:
          type: object
          description: Present when `useMultipart` is true.
          required: [uploadId, key]
          properties:
            uploadId:
              type: string
            key:
              type: string
              example: videos/507f1f77bcf86cd799439011/video
    VideoDocMessage:
      type: object
      required: [doc, message]
      properties:
        doc:
          $ref: "#/components/schemas/Video"
        message:
          type: string
          examples:
            - Updated successfully.
            - Deleted successfully.
    ThumbnailVariant:
      type: object
      properties:
        name:
          type: string
          description: Resolution label. Generated sizes are 2160p, 1440p, 1080p, 720p, 480p, 360p, and 240p, capped at the source.
          enum: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"]
        width:
          type: integer
        height:
          type: integer
        formats:
          type: object
          properties:
            jpeg:
              $ref: "#/components/schemas/ImageFile"
            webp:
              $ref: "#/components/schemas/ImageFile"
    ImageFile:
      type: object
      properties:
        url:
          type: string
          format: uri
        fileSize:
          type: integer
    VideoSource:
      type: object
      properties:
        name:
          type: string
          enum: ["2160p", "1440p", "1080p", "720p", "540p", "360p", "240p"]
        url:
          type: string
          format: uri
        width:
          type: integer
        height:
          type: integer
    Chapter:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
          maxLength: 100
        timestamp:
          type: string
          description: "HH:MM:SS"
          pattern: "^\\d{2}:\\d{2}:\\d{2}$"
    ChapterInput:
      type: object
      required: [title, timestamp]
      properties:
        title:
          type: string
          maxLength: 100
        timestamp:
          type: string
          pattern: "^\\d{2}:\\d{2}:\\d{2}$"
    TaxonomyRef:
      type: object
      description: Category or tag as nested on a video at the default depth. `depth=0` returns the id string instead.
      properties:
        id:
          type: string
        title:
          type: string
        slug:
          type: string
        description:
          type: string
    Video:
      type: object
      properties:
        id:
          type: string
        status:
          $ref: "#/components/schemas/VideoStatus"
        statusProgress:
          type: number
          description: Encoding progress in percent.
        errorCode:
          type: [string, "null"]
          description: |
            Set when `status` is `ERROR`. Examples include DURATION_LIMIT_EXCEEDED,
            UNKNOWN_ERROR, and MEDIACONVERT_1040 (unsupported format). New codes can appear.
        errorMessage:
          type: [string, "null"]
        visibility:
          $ref: "#/components/schemas/Visibility"
        v:
          type: number
          description: File version. Starts at 0 and increments when a replacement finishes encoding.
        contentUpdatedAt:
          type: string
          format: date-time
          description: Bumped on content writes. Not writable.
        title:
          type: string
        description:
          type: string
        duration:
          type: number
          description: Duration in seconds. Filled when encoding completes.
        fps:
          type: number
        orientation:
          type: string
          enum: [landscape, portrait]
        language:
          type: string
        aiContent:
          type: string
          enum: [none, generated, modified]
          description: AI content label (EU AI Act Art. 50).
        categories:
          description: Objects at the default depth, id strings at `depth=0`, or null when empty.
          oneOf:
            - type: array
              items:
                oneOf:
                  - $ref: "#/components/schemas/TaxonomyRef"
                  - type: string
            - type: "null"
        tags:
          description: Objects at the default depth, id strings at `depth=0`, or null when empty.
          oneOf:
            - type: array
              items:
                oneOf:
                  - $ref: "#/components/schemas/TaxonomyRef"
                  - type: string
            - type: "null"
        customMetadata:
          $ref: "#/components/schemas/CustomMetadata"
        src:
          type: object
          description: Empty until encoding completes.
          properties:
            filename:
              type: string
            thumbnailUrl:
              type: string
              format: uri
              deprecated: true
              description: 1080p JPEG of the active thumbnail. Use `src.thumbnails`.
            thumbnails:
              type: array
              description: Active thumbnail variants, highest resolution first.
              items:
                $ref: "#/components/schemas/ThumbnailVariant"
            abr:
              type: object
              description: Adaptive bitrate HLS master playlist.
              properties:
                resolution:
                  type: string
                  example: auto
                description:
                  type: string
                url:
                  type: string
                  format: uri
                maxWidth:
                  type: integer
                maxHeight:
                  type: integer
            hls:
              type: array
              items:
                $ref: "#/components/schemas/VideoSource"
            mp4:
              type: array
              items:
                $ref: "#/components/schemas/VideoSource"
            audio:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                    description: For example `high-quality` or `low-quality`.
                  url:
                    type: string
                    format: uri
        texttracks:
          type: array
          items:
            $ref: "#/components/schemas/TextTrackSummary"
        chapters:
          type: array
          items:
            $ref: "#/components/schemas/Chapter"
        audiotracks:
          type: array
          items:
            type: object
            additionalProperties: true
        transcriptions:
          type: array
          description: Derived transcript view. Not writable.
          items:
            type: object
            properties:
              language:
                type: string
              autoGenerated:
                type: boolean
              text:
                type: string
              segments:
                type: array
                items:
                  type: object
                  properties:
                    startTime:
                      type: number
                    endTime:
                      type: number
                    text:
                      type: string
        autoTranscription:
          type: object
          properties:
            autoStart:
              type: boolean
            lastUpdatedAt:
              type: string
              format: date-time
            data:
              description: Related transcription records, populated according to `depth`.
        player:
          type: object
          description: Included on API-key reads. Omitted from webhook payloads.
          properties:
            selectedPreset:
              description: Preset object, or absent when none is selected.
            useCustomDesign:
              type: boolean
            playerDesign:
              type: object
              additionalProperties: true
            shareUrl:
              type: string
              format: uri
              description: Embed player URL. Use as an iframe `src` or share link.
        passwordProtection:
          type: object
          properties:
            enabled:
              type: boolean
            password:
              type: string
              description: Present for callers who can update the video when the feature is enabled.
        previewVideoUrl:
          type: string
          format: uri
          description: Signed preview URL, added on read when renditions exist.
        previewThumbnailUrl:
          type: string
          format: uri
        previewThumbnailUrlSmall:
          type: string
          format: uri
        workspace:
          type: string
          description: Workspace id.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TextTrackType:
      type: string
      enum: [subtitles, captions]
    TextTrackSummary:
      type: object
      properties:
        id:
          type: string
        language:
          type: string
          example: en-US
        filename:
          type: string
        type:
          $ref: "#/components/schemas/TextTrackType"
        url:
          type: string
          format: uri
        autoGenerated:
          type: boolean
        autoTranslated:
          type: boolean
        edited:
          type: boolean
        updatedAt:
          type: string
          format: date-time
        content:
          type: string
          description: |
            On a video object, and on create or update responses, this is a URL-encoded
            deprecated placeholder. It is not the uploaded WebVTT. GET the track for the file.
    TextTrack:
      allOf:
        - $ref: "#/components/schemas/TextTrackSummary"
        - type: object
          properties:
            content:
              type: string
              description: |
                On GET, the WebVTT document. On create and update responses, the deprecated placeholder.
    TextTrackInput:
      type: object
      required: [language, type, content]
      properties:
        language:
          type: string
          description: Supported locale, for example `en-US` or `de-DE`. `en` is rejected.
          example: en-US
        type:
          $ref: "#/components/schemas/TextTrackType"
        content:
          type: string
          description: WebVTT document.
    TextTrackPatch:
      type: object
      description: All fields are optional. A metadata-only change does not require `content`.
      properties:
        language:
          type: string
          example: de-DE
        type:
          $ref: "#/components/schemas/TextTrackType"
        content:
          type: string
          description: Replacement WebVTT document.
    TextTrackFileInput:
      type: object
      required: [language, type, file]
      properties:
        language:
          type: string
          example: en-US
        type:
          $ref: "#/components/schemas/TextTrackType"
        file:
          type: string
          format: binary
          description: WebVTT file.
    TextTrackFilePatch:
      type: object
      properties:
        language:
          type: string
        type:
          $ref: "#/components/schemas/TextTrackType"
        file:
          type: string
          format: binary
    Breadcrumb:
      type: object
      properties:
        id:
          type: string
        doc:
          type: string
          description: Category id this crumb points at. Sometimes absent on the create response.
        url:
          type: string
        label:
          type: string
    CategoryInput:
      type: object
      properties:
        title:
          type: string
          maxLength: 100
        slug:
          type: string
          description: Generated from `title` when omitted.
        description:
          type: string
          maxLength: 2000
        parent:
          type: string
          description: Parent category id.
    Category:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        slug:
          type: string
        description:
          type: string
        parent:
          description: Parent category object at the default depth, or absent for a root. `depth=0` returns the id.
        path:
          type: array
          items:
            $ref: "#/components/schemas/Breadcrumb"
        pathLabel:
          type: string
          description: Breadcrumb titles joined with ` / `.
        videoCount:
          type: integer
        videoCountWithSubcategories:
          type: integer
        workspace:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CategoryDocMessage:
      type: object
      required: [doc, message]
      properties:
        doc:
          $ref: "#/components/schemas/Category"
        message:
          type: string
    TagInput:
      type: object
      properties:
        title:
          type: string
        slug:
          type: string
          description: Generated from `title` when omitted.
        description:
          type: string
    Tag:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        slug:
          type: string
        description:
          type: string
        workspace:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TagDocMessage:
      type: object
      required: [doc, message]
      properties:
        doc:
          $ref: "#/components/schemas/Tag"
        message:
          type: string
