{
  "openapi": "3.1.0",
  "info": {
    "title": "OctaVideo Server API",
    "version": "1.0.0-beta",
    "summary": "Upload, manage and securely play videos from your own server.",
    "description": "The server-to-server API of OctaVideo. Every request is authenticated with a library private key in the `AccessKey` header and acts on that one library.\n\nSuccessful responses wrap their payload in `data`; list responses add `meta` with pagination. Errors use a single envelope with a stable machine `code` and a `request_id` to quote to support.\n\nThe API is in private beta: the contract may still change before general availability.",
    "contact": {
      "name": "OctaVideo",
      "url": "https://docs.octavideo.tech/en/"
    }
  },
  "servers": [
    {
      "url": "https://api.octavideo.tech/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "AccessKey": []
    }
  ],
  "tags": [
    {
      "name": "Videos",
      "description": "Upload, import and manage the videos of a library."
    },
    {
      "name": "Collections",
      "description": "Group videos of a library into collections."
    },
    {
      "name": "Playback",
      "description": "Issue one-time passwords (OTPs) that let a viewer play a video."
    }
  ],
  "paths": {
    "/videos/upload": {
      "post": {
        "tags": [
          "Videos"
        ],
        "operationId": "uploadVideo",
        "summary": "Upload a video",
        "description": "Uploads a video file as `multipart/form-data` and queues it for processing.\n\nSend the `title` part **before** the `file` part: the file is streamed straight into storage, so parts after it are ignored. If `title` is missing or blank, the file name without its extension is used.\n\nThe file can be up to 10 GiB and the request may run for up to 2 hours. The new video starts in the `queued` status; poll [Get a video](#operation/getVideo) until `encoding_status` is `ready`.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Video title. Must come before `file`. Defaults to the file name without its extension."
                  },
                  "file": {
                    "type": "string",
                    "contentMediaType": "application/octet-stream",
                    "description": "The video file, up to 10 GiB."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The video was created and queued for processing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoEnvelope"
                },
                "example": {
                  "data": {
                    "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                    "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
                    "title": "Lesson 1: Introduction",
                    "description": "",
                    "encoding_status": "queued",
                    "progress": 0,
                    "duration_seconds": 0,
                    "size_bytes": 412337152,
                    "chapters": [],
                    "created_at": "2026-10-25T09:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          }
        ]
      }
    },
    "/videos/import": {
      "post": {
        "tags": [
          "Videos"
        ],
        "operationId": "importVideo",
        "summary": "Import a video",
        "description": "Imports a video from Google Drive, OneDrive or a public URL. The import runs in the background: the response is `202 Accepted` with a `job_id` and the new video in the `queued` status.\n\nFor `url`, `source_ref` must be an absolute `http` or `https` URL.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportRequest"
              },
              "example": {
                "source": "url",
                "source_ref": "https://example.com/lessons/intro.mp4",
                "title": "Lesson 1: Introduction"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The import was accepted and the video is queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ImportResult"
                    }
                  }
                },
                "example": {
                  "data": {
                    "job_id": "a41f0c2e-9b7d-4e3a-8c1f-5d2b6e9a0f13",
                    "video": {
                      "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                      "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
                      "title": "Lesson 1: Introduction",
                      "description": "",
                      "encoding_status": "queued",
                      "progress": 0,
                      "duration_seconds": 0,
                      "size_bytes": 0,
                      "chapters": [],
                      "created_at": "2026-10-25T09:30:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          }
        ]
      }
    },
    "/videos": {
      "get": {
        "tags": [
          "Videos"
        ],
        "operationId": "listVideos",
        "summary": "List videos",
        "description": "Lists the videos of the library, one page at a time, newest first.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "name": "collection_id",
            "in": "query",
            "required": false,
            "description": "Only return videos in this collection.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of videos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Video"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                      "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
                      "title": "Lesson 1: Introduction",
                      "description": "",
                      "encoding_status": "ready",
                      "progress": 100,
                      "duration_seconds": 754.2,
                      "size_bytes": 412337152,
                      "chapters": [],
                      "created_at": "2026-10-25T09:30:00Z"
                    }
                  ],
                  "meta": {
                    "page": 1,
                    "per_page": 25,
                    "total": 1
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/videos/{videoId}": {
      "get": {
        "tags": [
          "Videos"
        ],
        "operationId": "getVideo",
        "summary": "Get a video",
        "description": "Returns one video, including its processing status. Poll this endpoint after an upload or import: `encoding_status` moves from `queued` to `processing` to `ready` (or `failed`), and `progress` goes from 0 to 100.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "responses": {
          "200": {
            "description": "The video.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoEnvelope"
                },
                "example": {
                  "data": {
                    "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                    "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
                    "title": "Lesson 1: Introduction",
                    "description": "",
                    "encoding_status": "processing",
                    "progress": 42,
                    "duration_seconds": 754.2,
                    "size_bytes": 412337152,
                    "chapters": [],
                    "created_at": "2026-10-25T09:30:00Z",
                    "stage": "transcode"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/videos/{videoId}/details": {
      "put": {
        "tags": [
          "Videos"
        ],
        "operationId": "updateVideoDetails",
        "summary": "Update video details",
        "description": "Replaces the title and description of a video.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DetailsRequest"
              },
              "example": {
                "title": "Lesson 1: Introduction",
                "description": "What we cover in this course."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated video.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoEnvelope"
                },
                "example": {
                  "data": {
                    "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                    "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
                    "title": "Lesson 1: Introduction",
                    "description": "What we cover in this course.",
                    "encoding_status": "ready",
                    "progress": 100,
                    "duration_seconds": 754.2,
                    "size_bytes": 412337152,
                    "chapters": [],
                    "created_at": "2026-10-25T09:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/videos/{videoId}/transcribe": {
      "post": {
        "tags": [
          "Videos"
        ],
        "operationId": "transcribeVideo",
        "summary": "Transcribe a video",
        "description": "Starts a transcription of the video's speech. The body is optional: leave out `language` to auto-detect it. Transcription runs in the background, so the response is `202 Accepted` with a `job_id`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TranscribeRequest"
              },
              "example": {
                "language": "ar"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The transcription job was accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Job"
                    }
                  }
                },
                "example": {
                  "data": {
                    "job_id": "c92e1b7a-3f4d-4a8e-9b0c-1d2e3f4a5b6c"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/videos/{videoId}/chapters": {
      "put": {
        "tags": [
          "Videos"
        ],
        "operationId": "setVideoChapters",
        "summary": "Set video chapters",
        "description": "Replaces all chapters of a video. Send up to 200 chapters whose `start_seconds` strictly increase. An empty list removes every chapter.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChaptersRequest"
              },
              "example": {
                "chapters": [
                  {
                    "title": "Welcome",
                    "start_seconds": 0
                  },
                  {
                    "title": "Course outline",
                    "start_seconds": 95.5
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated video.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoEnvelope"
                },
                "example": {
                  "data": {
                    "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                    "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
                    "title": "Lesson 1: Introduction",
                    "description": "",
                    "encoding_status": "ready",
                    "progress": 100,
                    "duration_seconds": 754.2,
                    "size_bytes": 412337152,
                    "chapters": [
                      {
                        "title": "Welcome",
                        "start_seconds": 0
                      },
                      {
                        "title": "Course outline",
                        "start_seconds": 95.5
                      }
                    ],
                    "created_at": "2026-10-25T09:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/videos/{videoId}/thumbnails": {
      "get": {
        "tags": [
          "Videos"
        ],
        "operationId": "listVideoThumbnails",
        "summary": "List video thumbnails",
        "description": "Returns the URLs of the thumbnails generated for a video. The list is empty until processing reaches the thumbnails stage.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "responses": {
          "200": {
            "description": "The thumbnail URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Thumbnails"
                    }
                  }
                },
                "example": {
                  "data": {
                    "thumbnails": [
                      "https://media.example.com/libraries/0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e/videos/6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34/thumbnails/001.jpg",
                      "https://media.example.com/libraries/0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e/videos/6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34/thumbnails/002.jpg"
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/collections": {
      "post": {
        "tags": [
          "Collections"
        ],
        "operationId": "createCollection",
        "summary": "Create a collection",
        "description": "Creates an empty collection in the library.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CollectionRequest"
              },
              "example": {
                "name": "Arabic grammar, level 1"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new collection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CollectionEnvelope"
                },
                "example": {
                  "data": {
                    "id": "3c7a9e21-5b4d-4f8e-a1c2-7d9e0f1a2b3c",
                    "name": "Arabic grammar, level 1",
                    "video_ids": [],
                    "created_at": "2026-10-25T09:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          }
        ]
      }
    },
    "/collections/{collectionId}/videos": {
      "post": {
        "tags": [
          "Collections"
        ],
        "operationId": "addCollectionVideos",
        "summary": "Add videos to a collection",
        "description": "Adds 1 to 100 videos of the library to a collection. Video IDs must be unique within the request.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          },
          {
            "name": "collectionId",
            "in": "path",
            "required": true,
            "description": "ID of the collection.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "3c7a9e21-5b4d-4f8e-a1c2-7d9e0f1a2b3c"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddVideosRequest"
              },
              "example": {
                "video_ids": [
                  "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated collection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CollectionEnvelope"
                },
                "example": {
                  "data": {
                    "id": "3c7a9e21-5b4d-4f8e-a1c2-7d9e0f1a2b3c",
                    "name": "Arabic grammar, level 1",
                    "video_ids": [
                      "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34"
                    ],
                    "created_at": "2026-10-25T09:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/playback/otp": {
      "post": {
        "tags": [
          "Playback"
        ],
        "operationId": "issuePlaybackOtp",
        "summary": "Issue a playback OTP",
        "description": "Issues a one-time password (OTP) that lets one viewer play one video, and returns the `embed_url` to load in an iframe.\n\nCall this from your server each time a signed-in viewer opens a video page, and never expose your library key to the browser. An OTP can be used once and expires after `ttl_seconds` (5 minutes by default).\n\nAlways pass `user_id`: it makes one person count as one unique viewer across devices.\n\nThe video must be `ready`, otherwise the response is `409` with the code `video_not_ready`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OTPRequest"
              },
              "example": {
                "video_id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
                "user_id": "student-4821",
                "ttl_seconds": 300
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The OTP and the embed URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OTP"
                    }
                  }
                },
                "example": {
                  "data": {
                    "otp": "Zx8kQ2mVb7nR4tY1uW9eA3sD6fG0hJ5kL2pO8iU7yT4",
                    "expires_at": "2026-10-25T09:35:00Z",
                    "embed_url": "https://player.octavideo.tech/embed/0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e/6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34?otp=Zx8kQ2mVb7nR4tY1uW9eA3sD6fG0hJ5kL2pO8iU7yT4"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The video is not ready for playback yet (`video_not_ready`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "example": {
                  "error": {
                    "code": "video_not_ready",
                    "message": "video is not ready for playback",
                    "request_id": "9f2c4e7a1b3d5f6e8a0c2e4f6a8b0d1c"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/AccessKey"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "AccessKey": {
        "type": "apiKey",
        "in": "header",
        "name": "AccessKey",
        "description": "The private key of a library, starting with `ovk_`. Copy it from **API & embeds** in the console, keep it on your server, and rotate it there if it leaks."
      }
    },
    "parameters": {
      "AccessKey": {
        "name": "AccessKey",
        "in": "header",
        "required": true,
        "description": "The library's private key. Same value as the `AccessKey` security scheme; listed here so code samples include it.",
        "schema": {
          "type": "string"
        },
        "example": "ovk_YOUR_LIBRARY_PRIVATE_KEY"
      },
      "VideoId": {
        "name": "videoId",
        "in": "path",
        "required": true,
        "description": "ID of the video.",
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "example": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34"
      },
      "Page": {
        "name": "page",
        "in": "query",
        "required": false,
        "description": "Page number, starting at 1.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "in": "query",
        "required": false,
        "description": "Items per page.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is invalid. `validation_failed` lists each invalid field in `details.fields`; `bad_request` means the body could not be read (malformed JSON, unknown fields, a missing multipart body).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "validation_failed",
                "message": "validation failed",
                "request_id": "9f2c4e7a1b3d5f6e8a0c2e4f6a8b0d1c",
                "details": {
                  "fields": [
                    {
                      "field": "video_id",
                      "message": "must be a UUID"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The `AccessKey` header is missing or the key is not valid (`unauthorized`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "unauthorized",
                "request_id": "9f2c4e7a1b3d5f6e8a0c2e4f6a8b0d1c"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist in this library (`not_found`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "not found",
                "request_id": "9f2c4e7a1b3d5f6e8a0c2e4f6a8b0d1c"
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request body is over the limit: 1 MiB for JSON, 10 GiB for uploads (`payload_too_large`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "error": {
                "code": "payload_too_large",
                "message": "request body too large",
                "request_id": "9f2c4e7a1b3d5f6e8a0c2e4f6a8b0d1c"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Video": {
        "type": "object",
        "required": [
          "id",
          "library_id",
          "title",
          "description",
          "encoding_status",
          "progress",
          "duration_seconds",
          "size_bytes",
          "chapters",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "ID of the video."
          },
          "library_id": {
            "type": "string",
            "format": "uuid",
            "description": "ID of the library the video belongs to."
          },
          "title": {
            "type": "string",
            "maxLength": 255
          },
          "description": {
            "type": "string",
            "maxLength": 5000
          },
          "encoding_status": {
            "$ref": "#/components/schemas/EncodingStatus"
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Overall processing progress in percent."
          },
          "stage": {
            "$ref": "#/components/schemas/Stage"
          },
          "failure_reason": {
            "type": "string",
            "description": "Why processing failed. Only present when `encoding_status` is `failed`. Known values: `invalid_media`, `max_attempts_exceeded`, `processing_error`.",
            "examples": [
              "invalid_media"
            ]
          },
          "duration_seconds": {
            "type": "number",
            "minimum": 0,
            "description": "Duration of the video. `0` until it is known."
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 0,
            "description": "Size of the source file."
          },
          "chapters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Chapter"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the video was created, in UTC (RFC 3339)."
          }
        },
        "example": {
          "id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
          "library_id": "0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e",
          "title": "Lesson 1: Introduction",
          "description": "",
          "encoding_status": "processing",
          "progress": 42,
          "stage": "transcode",
          "duration_seconds": 754.2,
          "size_bytes": 412337152,
          "chapters": [],
          "created_at": "2026-10-25T09:30:00Z"
        }
      },
      "EncodingStatus": {
        "type": "string",
        "enum": [
          "queued",
          "processing",
          "ready",
          "failed"
        ],
        "description": "Processing state of the video. Only `ready` videos can be played."
      },
      "Stage": {
        "type": "string",
        "enum": [
          "fetch",
          "transcode",
          "thumbnails",
          "package",
          "encrypt",
          "publish",
          "meter",
          "finalize"
        ],
        "description": "The processing stage the video is in. Present while `encoding_status` is `processing`."
      },
      "Chapter": {
        "type": "object",
        "required": [
          "title",
          "start_seconds"
        ],
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 100
          },
          "start_seconds": {
            "type": "number",
            "minimum": 0,
            "description": "Where the chapter starts, in seconds from the beginning of the video."
          }
        }
      },
      "VideoEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Video"
          }
        }
      },
      "ImportRequest": {
        "type": "object",
        "required": [
          "source",
          "source_ref"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "google_drive",
              "onedrive",
              "url"
            ],
            "description": "Where to import the video from."
          },
          "source_ref": {
            "type": "string",
            "maxLength": 2048,
            "description": "The file reference at the source. For `url`, an absolute `http` or `https` URL."
          },
          "title": {
            "type": "string",
            "maxLength": 255,
            "description": "Video title. Defaults to `Imported video`."
          }
        },
        "example": {
          "source": "url",
          "source_ref": "https://example.com/lessons/intro.mp4",
          "title": "Lesson 1: Introduction"
        }
      },
      "ImportResult": {
        "type": "object",
        "required": [
          "job_id",
          "video"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "ID of the background import job."
          },
          "video": {
            "$ref": "#/components/schemas/Video"
          }
        }
      },
      "DetailsRequest": {
        "type": "object",
        "required": [
          "title"
        ],
        "additionalProperties": false,
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
          },
          "description": {
            "type": "string",
            "maxLength": 5000
          }
        },
        "example": {
          "title": "Lesson 1: Introduction",
          "description": "What we cover in this course."
        }
      },
      "TranscribeRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "language": {
            "type": "string",
            "description": "BCP 47 language tag of the speech, such as `ar` or `en-US`. Leave it out to auto-detect.",
            "examples": [
              "ar"
            ]
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "job_id"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "description": "ID of the background job."
          }
        }
      },
      "ChaptersRequest": {
        "type": "object",
        "required": [
          "chapters"
        ],
        "additionalProperties": false,
        "properties": {
          "chapters": {
            "type": "array",
            "maxItems": 200,
            "description": "All chapters of the video, with strictly increasing `start_seconds`. An empty list removes every chapter.",
            "items": {
              "$ref": "#/components/schemas/Chapter"
            }
          }
        },
        "example": {
          "chapters": [
            {
              "title": "Welcome",
              "start_seconds": 0
            },
            {
              "title": "Course outline",
              "start_seconds": 95.5
            }
          ]
        }
      },
      "Thumbnails": {
        "type": "object",
        "required": [
          "thumbnails"
        ],
        "properties": {
          "thumbnails": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "Collection": {
        "type": "object",
        "required": [
          "id",
          "name",
          "video_ids"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "maxLength": 100
          },
          "video_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CollectionEnvelope": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Collection"
          }
        }
      },
      "CollectionRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          }
        },
        "example": {
          "name": "Arabic grammar, level 1"
        }
      },
      "AddVideosRequest": {
        "type": "object",
        "required": [
          "video_ids"
        ],
        "additionalProperties": false,
        "properties": {
          "video_ids": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "OTPRequest": {
        "type": "object",
        "required": [
          "video_id"
        ],
        "additionalProperties": false,
        "properties": {
          "video_id": {
            "type": "string",
            "format": "uuid",
            "description": "The video to play. It must be `ready`."
          },
          "user_id": {
            "type": "string",
            "maxLength": 128,
            "description": "Your own ID for the viewer: printable ASCII, no spaces. Used to count unique viewers across devices.",
            "examples": [
              "student-4821"
            ]
          },
          "ttl_seconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "default": 300,
            "description": "How long the OTP stays valid before it is used."
          }
        },
        "example": {
          "video_id": "6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34",
          "user_id": "student-4821",
          "ttl_seconds": 300
        }
      },
      "OTP": {
        "type": "object",
        "required": [
          "otp",
          "expires_at",
          "embed_url"
        ],
        "properties": {
          "otp": {
            "type": "string",
            "minLength": 43,
            "maxLength": 43,
            "description": "The one-time password."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the OTP expires if it is not used, in UTC."
          },
          "embed_url": {
            "type": "string",
            "format": "uri",
            "description": "The player URL to load in an iframe. It already contains the OTP."
          }
        },
        "example": {
          "otp": "Zx8kQ2mVb7nR4tY1uW9eA3sD6fG0hJ5kL2pO8iU7yT4",
          "expires_at": "2026-10-25T09:35:00Z",
          "embed_url": "https://player.octavideo.tech/embed/0b8e5d3a-1c2f-4e6a-8b7d-9f0a1b2c3d4e/6f1c2b9e-4a7d-4f3e-9b8a-2d5c7e1f0a34?otp=Zx8kQ2mVb7nR4tY1uW9eA3sD6fG0hJ5kL2pO8iU7yT4"
        }
      },
      "PageMeta": {
        "type": "object",
        "required": [
          "page",
          "per_page",
          "total"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "per_page": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Total number of items across all pages."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message",
          "request_id"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine code. Branch on this, not on `message`.",
            "examples": [
              "validation_failed"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable description. It can change at any time."
          },
          "request_id": {
            "type": "string",
            "description": "ID of the request, also sent in the `X-Request-ID` header. Quote it when you contact support."
          },
          "details": {
            "type": "object",
            "required": [
              "fields"
            ],
            "properties": {
              "fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "field",
                    "message"
                  ],
                  "properties": {
                    "field": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/Error"
          }
        }
      }
    }
  }
}
