openapi: 3.1.0 info: title: Videos API summary: A base Videos API every service can start from, instead of reinventing one. description: |- Videos have been done. Every product that handles video rebuilds the same upload flow, the same transcoding wait, and the same set of playback URLs — slightly differently each time. This is a **base** Videos API for the [API Commons](https://apicommons.org): a starting point you copy into your own service rather than a hosted API. It is the sibling of the [Images base](https://apicommons.org/base/images/) and deliberately shares its shape — two-step upload, requested renditions, merge-patch metadata — with the one difference that actually matters: **video is not ready when the bytes finish uploading.** Transcoding takes real time, so `status` moves through `processing`, and the base ships webhooks so a client is told when a video becomes playable instead of polling. Captions are a first-class sub-resource for the same reason `alt` is required on an image: an API that makes accessibility an afterthought produces inaccessible products downstream. Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details, identical to every other API Commons base, and checked in CI by the [Problem Details ruleset](https://github.com/api-commons/spectral-problem-details-ruleset). Partial updates use [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396) JSON Merge Patch. version: 1.0.0 contact: name: API Evangelist url: https://apievangelist.com email: info@apievangelist.com license: name: Apache-2.0 identifier: Apache-2.0 externalDocs: description: The Videos base on API Commons url: https://apicommons.org/base/videos/ tags: - name: Videos description: Uploading, describing, transcoding, and deleting videos. - name: Captions description: Caption and subtitle tracks for a video. security: - bearerAuth: [] paths: /videos: get: summary: List videos description: List videos the caller can see, newest first. operationId: listVideos tags: [Videos] parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: status in: query description: Filter by processing status. required: false schema: $ref: '#/components/schemas/VideoStatus' responses: '200': description: A page of videos. headers: RateLimit: $ref: '#/components/headers/RateLimit' content: application/json: schema: $ref: '#/components/schemas/VideoList' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' post: summary: Create a video description: >- Create the video record and receive a short-lived `upload.url` to PUT the bytes to. The video stays in `pending`, then `processing`, and only becomes `ready` when transcoding completes. operationId: createVideo tags: [Videos] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VideoCreate' responses: '201': description: The video record was created and is awaiting bytes. headers: Location: description: The URL of the newly created video. schema: type: string format: uri-reference content: application/json: schema: $ref: '#/components/schemas/VideoUpload' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '415': $ref: '#/components/responses/UnsupportedMediaType' '422': $ref: '#/components/responses/UnprocessableContent' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' /videos/{videoId}: parameters: - $ref: '#/components/parameters/VideoId' get: summary: Get a video description: >- Retrieve a video's metadata. Check `status` before linking to `renditions` — a video that is still `processing` has none. operationId: getVideo tags: [Videos] responses: '200': description: The video. headers: ETag: $ref: '#/components/headers/ETag' content: application/json: schema: $ref: '#/components/schemas/Video' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' patch: summary: Update a video description: >- Partially update video metadata with an RFC 7396 JSON Merge Patch. A member set to null is REMOVED. The source bytes are immutable — replace a video by creating a new one. operationId: updateVideo tags: [Videos] parameters: - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/merge-patch+json: schema: $ref: '#/components/schemas/VideoPatch' responses: '200': description: The updated video. headers: ETag: $ref: '#/components/headers/ETag' content: application/json: schema: $ref: '#/components/schemas/Video' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '412': $ref: '#/components/responses/PreconditionFailed' '415': $ref: '#/components/responses/UnsupportedMediaType' '422': $ref: '#/components/responses/UnprocessableContent' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' delete: summary: Delete a video description: >- Delete the video, its renditions, and its caption tracks. Playback URLs already handed out may remain in downstream caches until they expire. operationId: deleteVideo tags: [Videos] responses: '204': description: The video, its renditions, and its captions were deleted. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' /videos/{videoId}/captions: parameters: - $ref: '#/components/parameters/VideoId' get: summary: List caption tracks description: List every caption or subtitle track attached to this video. operationId: listVideoCaptions tags: [Captions] responses: '200': description: The caption tracks for this video. content: application/json: schema: $ref: '#/components/schemas/CaptionList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' post: summary: Add a caption track description: Attach a WebVTT or SRT caption track in a given language. operationId: createVideoCaption tags: [Captions] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CaptionCreate' responses: '201': description: The caption track was added. content: application/json: schema: $ref: '#/components/schemas/Caption' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableContent' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' webhooks: videoReady: post: summary: Video became playable description: >- Sent when transcoding completes and the video moves to `ready`. This is why the base ships webhooks at all — video is not usable when the upload finishes, and polling a status field is the thing every implementation ends up regretting. operationId: onVideoReady tags: [Videos] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VideoEvent' responses: '204': description: Acknowledged. Any 2xx is treated as delivered. videoFailed: post: summary: Video processing failed description: >- Sent when transcoding fails. The payload carries a problem detail explaining why, using the same RFC 9457 shape as every error response in this API. operationId: onVideoFailed tags: [Videos] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VideoFailureEvent' responses: '204': description: Acknowledged. Any 2xx is treated as delivered. components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: A bearer token obtained from your identity provider. parameters: VideoId: name: videoId in: path required: true description: The video's unique identifier. schema: type: string example: vid_01HZX Limit: name: limit in: query required: false description: Maximum number of results to return. schema: type: integer minimum: 1 maximum: 200 default: 25 Cursor: name: cursor in: query required: false description: Opaque cursor from a previous response's `next` value. schema: type: string IfMatch: name: If-Match in: header required: false description: >- The ETag from a prior read. Conditions the write on the video not having changed since, per RFC 9110 Section 13.1.1. schema: type: string schemas: VideoStatus: type: string description: >- `pending` means the record exists but the bytes have not arrived. `processing` means transcoding is running — there are no renditions yet. `ready` means it is playable. `failed` means processing did not complete. enum: [pending, processing, ready, failed] Video: type: object description: A video and its metadata. required: [id, status, created] properties: id: type: string example: vid_01HZX status: $ref: '#/components/schemas/VideoStatus' title: type: string example: How the Kin Score works description: type: string contentType: type: string description: The media type of the stored source. example: video/mp4 durationSeconds: type: number description: Duration of the source, in seconds. Absent until processing completes. width: type: integer height: type: integer bytes: type: integer description: Size of the stored source, in bytes. renditions: type: array description: Playable renditions. Empty until `status` is `ready`. items: $ref: '#/components/schemas/VideoRendition' tags: type: array items: type: string created: type: string format: date-time modified: type: string format: date-time VideoCreate: type: object description: The fields accepted when creating a video record. required: [contentType] properties: contentType: type: string description: The media type of the bytes you are about to upload. example: video/mp4 title: type: string description: type: string tags: type: array items: type: string VideoUpload: type: object description: A newly created video record plus where to send the bytes. required: [video, upload] properties: video: $ref: '#/components/schemas/Video' upload: type: object description: Where and how to deliver the bytes. required: [url, method, expires] properties: url: type: string format: uri description: Short-lived URL to PUT the bytes to. method: type: string enum: [PUT] expires: type: string format: date-time description: After this, request a new upload URL. VideoPatch: type: object description: >- An RFC 7396 merge patch over video metadata. Send only what changes. A member set to null is REMOVED, so do not serialize absent optional fields as null. properties: title: type: [string, 'null'] description: type: [string, 'null'] tags: type: [array, 'null'] items: type: string VideoRendition: type: object description: A playable rendition of a video. required: [url, contentType] properties: url: type: string format: uri description: Playback URL for this rendition. contentType: type: string example: application/vnd.apple.mpegurl width: type: integer height: type: integer bitrateKbps: type: integer Caption: type: object description: A caption or subtitle track. required: [id, language, url, format] properties: id: type: string example: cap_01HZX language: type: string description: BCP 47 language tag. example: en-US label: type: string description: Human-readable name shown in a player's track menu. example: English format: type: string enum: [vtt, srt] url: type: string format: uri autoGenerated: type: boolean description: >- Whether the track was machine-generated. Worth surfacing — auto-generated captions are not an accessibility guarantee. CaptionCreate: type: object description: The fields accepted when adding a caption track. required: [language, format, url] properties: language: type: string example: en-US label: type: string format: type: string enum: [vtt, srt] url: type: string format: uri description: Where the caption file can be fetched from. autoGenerated: type: boolean CaptionList: type: object required: [data] properties: data: type: array items: $ref: '#/components/schemas/Caption' VideoEvent: type: object description: Webhook payload for a video lifecycle event. required: [event, occurred, video] properties: event: type: string enum: [video.ready] occurred: type: string format: date-time video: $ref: '#/components/schemas/Video' VideoFailureEvent: type: object description: >- Webhook payload for a failed video. `problem` is the same RFC 9457 shape used by every error response in this API, so a client has one error format to parse. required: [event, occurred, videoId, problem] properties: event: type: string enum: [video.failed] occurred: type: string format: date-time videoId: type: string example: vid_01HZX problem: $ref: '#/components/schemas/Problem' VideoList: type: object description: A page of videos. required: [data] properties: data: type: array items: $ref: '#/components/schemas/Video' next: type: [string, 'null'] description: Cursor for the next page, or null when this is the last page. # --------------------------------------------------------------------------- # RFC 9457 Problem Details. # # Identical in every API Commons base, and lifted from # https://github.com/api-commons/problem-details-for-http-apis so that every # base errors the same way. Conformance is checked in CI by # https://github.com/api-commons/spectral-problem-details-ruleset # # The `urn:ietf:rfc:7807` XML namespace below is CORRECT and deliberate — RFC # 9457 Appendix B retained it for backward compatibility. Do not "fix" it. # --------------------------------------------------------------------------- Problem: type: object description: >- A problem detail object, per RFC 9457 Section 3.1. Every member is optional. `additionalProperties` is deliberately left unset so problem types can carry extension members, as Section 3.2 requires. xml: name: problem namespace: urn:ietf:rfc:7807 properties: type: type: string format: uri description: A URI reference identifying the problem type. Defaults to about:blank. example: https://example.com/probs/out-of-credit title: type: string description: A short, human-readable summary of the problem type. example: You do not have enough credit. status: type: integer description: >- The HTTP status code generated by the origin server. Advisory — the real status code is the one on the response. example: 403 detail: type: string description: >- A human-readable explanation specific to this occurrence, focused on helping the client correct the problem rather than on debugging. example: Your current balance is 30, but that costs 50. instance: type: string format: uri-reference description: A URI reference identifying this specific occurrence of the problem. example: /account/12345/msgs/abc headers: ETag: description: >- An opaque validator for the current state of the resource. Send it back in `If-Match` on a write to avoid clobbering a concurrent update. RFC 9110 Section 8.8.3. schema: type: string example: '"a1b2c3"' RateLimit: description: >- Remaining quota and the reset interval for the current policy, per the IETF RateLimit header fields draft. schema: type: string example: 'limit=100, remaining=94, reset=50' WWW-Authenticate: description: >- The authentication scheme and parameters applicable to the target resource. RFC 9110 Section 11.6.1 requires a 401 to carry at least one challenge; a problem detail explaining the 401 does not substitute for it. schema: type: string example: 'Bearer realm="example", error="invalid_token"' Retry-After: description: >- How long the client should wait before retrying, in seconds or as an HTTP-date. RFC 9110 Section 10.2.3. schema: type: string example: '120' responses: BadRequest: description: Bad Request content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/bad-request title: Bad Request status: 400 detail: The request body is invalid or a required field is missing. instance: /requests/01HZ Unauthorized: description: Unauthorized headers: WWW-Authenticate: $ref: '#/components/headers/WWW-Authenticate' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/unauthorized title: Unauthorized status: 401 detail: The access token is missing, malformed, or expired. instance: /requests/01HZ Forbidden: description: Forbidden content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/forbidden title: Forbidden status: 403 detail: The credential is valid but lacks the scope for this operation. instance: /requests/01HZ NotFound: description: Not Found content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/not-found title: Not Found status: 404 detail: No resource exists with that identifier. instance: /requests/01HZ Conflict: description: Conflict content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/conflict title: Conflict status: 409 detail: The resource has changed since you last read it. instance: /requests/01HZ UnprocessableContent: description: Unprocessable Content content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/unprocessable-content title: Unprocessable Content status: 422 detail: The request was well-formed but failed a business rule. instance: /requests/01HZ PreconditionFailed: description: Precondition Failed content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/precondition-failed title: Precondition Failed status: 412 detail: The resource changed since you read it. Re-read it and retry. instance: /requests/01HZ UnsupportedMediaType: description: Unsupported Media Type content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/unsupported-media-type title: Unsupported Media Type status: 415 detail: This operation expects application/merge-patch+json. instance: /requests/01HZ TooManyRequests: description: Too Many Requests headers: Retry-After: $ref: '#/components/headers/Retry-After' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/too-many-requests title: Too Many Requests status: 429 detail: You have exceeded the rate limit for this endpoint. instance: /requests/01HZ InternalServerError: description: Internal Server Error content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://example.com/errors/internal-server-error title: Internal Server Error status: 500 detail: The request could not be completed. Retry, then contact support. instance: /requests/01HZ