# Cerul API Documentation: https://cerul.ai/docs Base URL: https://api.cerul.ai (no /v1). # Quickstart Replace YOUR_API_KEY with a Key created in Console, then run this request to list your videos. No upload is needed to verify access. ```bash curl --fail-with-body "https://api.cerul.ai/videos?limit=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` The response stores videos in data, newest first. An empty list is normal in a new workspace. With a ready video, continue to [Search](/docs.md?locale=en&page=search); otherwise [upload a video](/docs.md?locale=en&page=upload). ## Request parameters | Field | Type · Required | Description | | --- | --- | --- | | Authorization | string · Required | Bearer YOUR_API_KEY | | limit | integer · Optional | ≥ 1; ≤ 500; default: 100 | | cursor | string · Optional | minLength: 1; maxLength: 512 | | q | string · Optional | minLength: 1; maxLength: 200 | | status | string · Optional | pattern: ^(uploading/uploaded/processing/ready/failed)(,(uploading/uploaded/processing/ready/failed))*$ | Required scopes: videos:read. ## Reading the response data[].id identifies the video. filename is its original name; status reports processing progress. ready means searchable; playable means the web-playable copy is ready, which can happen first. indexing reports speech, screen_text and visual separately (status indexed, absent or failed, with count and reason), so a ready video may still lack one type. Lists return at most limit videos (default 100, maximum 500). While next_cursor is not null, pass it back as cursor to read the next page. q finds a video by filename or exact ID; status filters by comma-separated states. ## Complete example [Download complete example](/docs-quickstart.sh) Run the snippets in the same Bash session with curl (7.76+), jq, Python 3 and ffprobe installed. Set talk.mp4 to your video. Processing and search consume credits; preserve the run ID when retrying unchanged requests. Use https://api.cerul.ai without a /v1 prefix and send Authorization: Bearer . Verify your account email before uploading, indexing, searching or exporting. Keys stay on your server; a Workspace shares one cloud wallet across Keys and authenticated app/CLI calls. ## Retries and errors Send an Idempotency-Key of 8–200 characters where required, including search. Keep the same key and input after an ambiguous response; use a new key for a different operation/input. Poll pending jobs according to Retry-After and set a deadline. A client timeout does not cancel a server job. • 401 · invalid_credentials: Check or replace the expired/revoked credential. • 403 · permission_denied / insufficient_entitlement: Check scopes, verified email, entitlements and wallet balance. • 429 · rate_limited: Back off using retry_after_seconds when present; check concurrency. --- # Upload video Place your video at talk.mp4 and replace YOUR_API_KEY. This request creates a video and reserves upload URLs using its real byte size; it does not upload the bytes yet. ```bash REQUEST_KEY="upload-$(uuidgen)" FILE_SIZE=$(wc -c < ./talk.mp4 | tr -d ' ') curl --fail-with-body "https://api.cerul.ai/videos" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $REQUEST_KEY" \ -d "{\"filename\":\"talk.mp4\",\"media_type\":\"video/mp4\",\"byte_size\":$FILE_SIZE}" ``` ## Request parameters | Field | Type · Required | Description | | --- | --- | --- | | Authorization | string · Required | Bearer YOUR_API_KEY | | filename | string · Required | minLength: 1; maxLength: 255 | | media_type | string · Required | minLength: 3; maxLength: 127; pattern: ^[Vv][Ii][Dd][Ee][Oo]/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$ | | byte_size | integer · Required | ≥ 1; ≤ 5368709120 | | Idempotency-Key | string · Required | minLength: 8; maxLength: 200 | Required scopes: videos:write. ## Upload the file bytes Save data.id. For each entry in data.upload.parts, PUT the exact sequential byte range to its temporary URL using data.upload.required_headers. Do not send the API Key to the upload URL. Maximum file size is 5 GiB; URL ingestion is not supported. [Download complete example](/docs-quickstart.sh) Run the snippets in the same Bash session with curl (7.76+), jq, Python 3 and ffprobe installed. Set talk.mp4 to your video. Processing and search consume credits; preserve the run ID when retrying unchanged requests. ## Confirm the upload After all parts succeed, POST /videos/{video_id}/complete with Bearer, Content-Type: application/json and a new Idempotency-Key. Submit the real duration and SHA-256. Completion starts indexing by default; do not submit a duplicate index request. | Field | Type · Required | Description | | --- | --- | --- | | Authorization | string · Required | Bearer YOUR_API_KEY | | duration_seconds | number · Required for indexing | Source duration, used for quota admission before transcription. — > 0 | | content_sha256 | string · Required for indexing | Declares source identity for indexing. The Worker verifies it against the uploaded bytes and successful unique-material billing is deduplicated by this hash; a distinct video asset may still be processed independently. — pattern: ^[a-fA-F0-9]{64}$ | | index | boolean · Optional | false: upload only; true: start indexing — default: true | | processing | object · Optional | | | processing.screen_text | object · Optional | | | processing.screen_text.enabled | boolean · Optional | Index text visible on screen, sampled independently of speech. Defaults to false. — default: false | | processing.screen_text.interval_seconds | integer · Optional | On-screen text sampling interval. Defaults to 10 seconds. — ≥ 1; ≤ 3600; default: 10 | | video_id | string · Required | A Video is the developer-facing name for a media Asset, and shares its identifier. — pattern: ^asset_[A-Za-z0-9_-]+$ | | Idempotency-Key | string · Required | minLength: 8; maxLength: 200 | Required scopes: videos:write. Starting indexing at completion additionally requires jobs:write, videos:read and files:write. Set index:false only to retain an unindexed upload; later call POST /videos/{video_id}/index (jobs:write, videos:read, collection-items:write, files:write). ## Wait for indexing Poll GET /videos/{video_id} with a deadline: ready is searchable; failed means inspect error; processing means wait. A client timeout does not cancel the server job. On-screen text is off by default. Set processing.screen_text.enabled to true (SCREEN_TEXT_ENABLED=true in the example) before completion to index it at a 10-second interval. A ready video is searchable; read data.indexing for the speech, screen_text and visual results, each indexed, absent or failed with a reason. A video without audio has no transcript. --- # Search Replace YOUR_API_KEY and YOUR_VIDEO_ID to search a ready video; omit filter to search every video in the workspace. Keep REQUEST_KEY unchanged when retrying the same request. ```bash REQUEST_KEY="search-$(uuidgen)" curl --fail-with-body "https://api.cerul.ai/search" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $REQUEST_KEY" \ -d '{ "query": "When did they discuss scaling laws?", "filter": {"video_ids": ["YOUR_VIDEO_ID"]}, "limit": 5 }' ``` ## Request parameters | Field | Type · Required | Description | | --- | --- | --- | | Authorization | string · Required | Bearer YOUR_API_KEY | | query | string · Required | minLength: 1; maxLength: 1000 | | filter | object · Optional | Omit to search every video in the workspace. | | filter.video_ids | array · Optional | uniqueItems | | limit | integer · Optional | ≥ 1; ≤ 100; default: 20 | | include | array · Optional | Optional extra blocks. `debug` adds model, version and ranking details to each clip. — uniqueItems | | Idempotency-Key | string · Required | minLength: 8; maxLength: 200 | Required scopes: videos:read. ## Read the result Each data item is a clip. id identifies the clip (pass it as clip_id when exporting) and video_id its video. start_seconds and end_seconds locate it. type is speech, screen_text or visual; text is the verbatim transcript or on-screen text and is null for a visual match. related lists other types that matched in the same stretch; context.before and context.after hold the verbatim lines around it. score is the raw similarity within the clip's own type, from 0 to 1. It is not comparable across types and is not a correctness probability; results are ordered by fused ranking, so order and score can disagree. thumbnail_url and playback.url are signed links valid for one hour (null until the web-playable copy is ready); playback.url ends with a #t=start,end fragment that browsers honour. Store console_url instead: it is a permanent Console link to that moment and requires signing in to the workspace. video carries the filename, duration and summary; summary is generated when summary_generated is true. Keep request_id, usage and warnings. Display the source video, time range, type and text, and let users replay the clip before relying on it. Export a bounded range with [exports](/docs.md?locale=en&page=exports). ## No matches An empty data array is not a request failure; warnings then includes no_confident_match. Check readiness, filter and indexing before refining the query. A valid completed search may be charged without matches; a request with nothing indexed is not charged. GET /videos/{video_id}/transcript returns speech segments with start_seconds and end_seconds; no audio may return 422 no_audio_track. --- # Export and manage Replace the Key and video ID to export seconds 30 to 45 as an MP4 file. Confirm the source contains this range. The response is an export in progress, not a ready download. ```bash REQUEST_KEY="export-$(uuidgen)" curl --fail-with-body "https://api.cerul.ai/exports" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $REQUEST_KEY" \ -d '{"video_id":"YOUR_VIDEO_ID","start_seconds":30,"end_seconds":45}' ``` ## Request parameters | Field | Type · Required | Description | | --- | --- | --- | | Authorization | string · Required | Bearer YOUR_API_KEY | | video_id | string · Required | A Video is the developer-facing name for a media Asset, and shares its identifier. — pattern: ^asset_[A-Za-z0-9_-]+$ | | start_seconds | number · Required | ≥ 0 | | end_seconds | number · Required | > 0 | | clip_id | string · Optional | The search result clip this range came from, recorded for reference. — pattern: ^ev_[A-Za-z0-9_-]+$ | | Idempotency-Key | string · Required | minLength: 8; maxLength: 200 | Required scopes: jobs:write, videos:read, files:write. ## Wait and download Save data.id as EXPORT_ID. Poll GET /exports/{export_id} (jobs:read, files:read), honoring Retry-After and a deadline. On succeeded, data.file gives url, content_type (video/mp4), byte_size and duration_seconds. Download GET /exports/{export_id}/file (files:read) with the same Bearer Key; it supports Range requests. Stop on failed or canceled and read failure. Exports are not billed. ```bash curl --fail-with-body "https://api.cerul.ai/exports/YOUR_EXPORT_ID/file" \ -H "Authorization: Bearer YOUR_API_KEY" \ --output clip.mp4 ``` ## Cancel or delete An export ID is also a job ID. POST /jobs/{job_id}/cancel requests cancellation of an export or indexing job; keep polling to a terminal state. DELETE /videos/{video_id} cleans up a video and associated data (videos:write). After a 202 deletion receipt, poll GET /deletions/{deletion_id} until cleanup completes. Successful searches may be charged even with no matches; a search with nothing indexed is not charged. Failed or canceled processing can still settle completed billable work and release unused reservations. Deletions return 202: follow the deletion receipt until cleanup finishes. Read available, reserved and debt credits separately in /billing/wallet. # Complete Bash example ```bash #!/usr/bin/env bash # Requires Bash, curl 7.76+, jq, Python 3, ffprobe and uuidgen. # Place a video at talk.mp4 and set CERUL_API_KEY before running. # Processing and search consume credits. On-screen text indexing is off by default. set -euo pipefail : "${CERUL_API_KEY:?Set CERUL_API_KEY before running}" CERUL_QUICKSTART_RUN_ID=$(uuidgen) export CERUL_QUICKSTART_RUN_ID export SCREEN_TEXT_ENABLED=false CERUL_TMP=$(mktemp -d) trap 'rm -rf "$CERUL_TMP"' EXIT cerul() { curl --fail-with-body --silent --show-error \ --connect-timeout 15 --max-time 120 \ -H "Authorization: Bearer $CERUL_API_KEY" \ -H "Content-Type: application/json" "$@" } cerul https://api.cerul.ai/videos | jq '.data' FILE_SIZE=$(wc -c < ./talk.mp4 | tr -d ' ') CONTENT_SHA256=$(python3 - <<'PY' import hashlib digest = hashlib.sha256() with open('talk.mp4', 'rb') as source: for chunk in iter(lambda: source.read(1024 * 1024), b''): digest.update(chunk) print(digest.hexdigest()) PY ) DURATION_SECONDS=$(ffprobe -v error -show_entries format=duration -of csv=p=0 ./talk.mp4) VIDEO=$(cerul https://api.cerul.ai/videos \ -H "Idempotency-Key: add-video-$CERUL_QUICKSTART_RUN_ID" \ -d "$(jq -n --argjson size "$FILE_SIZE" \ '{filename:"talk.mp4",media_type:"video/mp4",byte_size:$size}')") CERUL_VIDEO_ID=$(printf '%s' "$VIDEO" | jq -er '.data.id') export CERUL_VIDEO_ID printf 'Video ID: %s\n' "$CERUL_VIDEO_ID" UPLOAD_CONTENT_TYPE=$(printf '%s' "$VIDEO" | jq -er '.data.upload.required_headers["Content-Type"]') printf '%s' "$VIDEO" | jq -e '.data.upload.parts | length > 0' >/dev/null printf '%s' "$VIDEO" | jq -c '.data.upload.parts[]' > "$CERUL_TMP/parts.jsonl" # Upload exact byte ranges using only the signed headers, without the API Key. OFFSET=0 while IFS= read -r PART; do PART_URL=$(printf '%s' "$PART" | jq -er '.url') PART_SIZE=$(printf '%s' "$PART" | jq -er '.byte_size') python3 - "$OFFSET" "$PART_SIZE" "$CERUL_TMP/part.bin" <<'PY' import sys offset, size, target = sys.argv[1:] remaining = int(size) with open('talk.mp4', 'rb') as source, open(target, 'wb') as part: source.seek(int(offset)) while remaining: chunk = source.read(min(1024 * 1024, remaining)) if not chunk: raise RuntimeError('Source shorter than reserved upload') part.write(chunk) remaining -= len(chunk) PY curl --fail-with-body --silent --show-error -X PUT "$PART_URL" \ --connect-timeout 15 --max-time 1800 \ -H "Content-Type: $UPLOAD_CONTENT_TYPE" -H "Content-Length: $PART_SIZE" \ --upload-file "$CERUL_TMP/part.bin" OFFSET=$((OFFSET + PART_SIZE)) done < "$CERUL_TMP/parts.jsonl" # Defaults to speech and visual indexing. On-screen text is an explicit paid option. cerul "https://api.cerul.ai/videos/$CERUL_VIDEO_ID/complete" \ -H "Idempotency-Key: complete-video-$CERUL_QUICKSTART_RUN_ID" \ -d "$(jq -n --argjson duration "$DURATION_SECONDS" --arg hash "$CONTENT_SHA256" \ --argjson screen_text "$SCREEN_TEXT_ENABLED" \ '{duration_seconds:$duration,content_sha256:$hash,processing:{screen_text:{enabled:$screen_text,interval_seconds:10}}}')" \ > "$CERUL_TMP/completed.json" DEADLINE=$((SECONDS + 1800)) while :; do VIDEO=$(cerul "https://api.cerul.ai/videos/$CERUL_VIDEO_ID") STATUS=$(printf '%s' "$VIDEO" | jq -er '.data.status') case "$STATUS" in ready) printf '%s' "$VIDEO" | jq '.data.indexing'; break ;; failed) printf '%s' "$VIDEO" | jq '.data.error' >&2; exit 1 ;; uploading|uploaded|processing) ;; *) echo "Unexpected video status: $STATUS" >&2; exit 1 ;; esac if (( SECONDS >= DEADLINE )); then echo "Timed out; save $CERUL_VIDEO_ID and resume polling. The server job continues." >&2 exit 1 fi sleep 5 done SEARCH=$(cerul https://api.cerul.ai/search \ -H "Idempotency-Key: search-quickstart-$CERUL_QUICKSTART_RUN_ID" \ -d "$(jq -n --arg video "$CERUL_VIDEO_ID" \ '{query:"when did they discuss scaling laws",filter:{video_ids:[$video]},limit:5}')") printf '%s' "$SEARCH" | jq '{request_id,usage,warnings,clips:[.data[] | {id,type,text,start_seconds,end_seconds,score,console_url}]}' # A valid search can return no clips (warnings then include no_confident_match). # playback.url and thumbnail_url expire after one hour; store console_url instead. if ! CLIP=$(printf '%s' "$SEARCH" | jq -ce 'first(.data[] | select( .end_seconds > .start_seconds))'); then echo "No clip to export. Review the query or the video's indexing." exit 0 fi EXPORT_BODY=$(printf '%s' "$CLIP" | jq \ '{video_id,start_seconds,end_seconds,clip_id:.id}') EXPORT=$(cerul https://api.cerul.ai/exports \ -H "Idempotency-Key: export-quickstart-$CERUL_QUICKSTART_RUN_ID" -d "$EXPORT_BODY") EXPORT_ID=$(printf '%s' "$EXPORT" | jq -er '.data.id') DEADLINE=$((SECONDS + 1800)) while :; do EXPORT=$(cerul "https://api.cerul.ai/exports/$EXPORT_ID" -D "$CERUL_TMP/export-headers") EXPORT_STATUS=$(printf '%s' "$EXPORT" | jq -er '.data.status') case "$EXPORT_STATUS" in succeeded) break ;; failed|canceled) printf '%s' "$EXPORT" | jq '.data' >&2; exit 1 ;; esac if (( SECONDS >= DEADLINE )); then echo "Timed out; resume polling export $EXPORT_ID. The server job continues." >&2 exit 1 fi RETRY_AFTER=$(awk 'tolower($1)=="retry-after:" {gsub("\r", "", $2); print $2}' "$CERUL_TMP/export-headers") case "$RETRY_AFTER" in ''|*[!0-9]*) RETRY_AFTER=3 ;; esac sleep "$RETRY_AFTER" done printf '%s' "$EXPORT" | jq '.data.file' cerul "https://api.cerul.ai/exports/$EXPORT_ID/file" --output quickstart-clip.mp4 ``` # API reference ```json { "openapi": "3.1.0", "info": { "title": "Cerul API — Daily integration", "version": "1.0.0-foundation", "description": "Customer upload, indexing, search, export and resource management." }, "servers": [ { "url": "https://api.cerul.ai", "description": "Cloud runtime" } ], "security": [ { "bearerAuth": [] } ], "tags": [ { "name": "Videos", "description": "Video ingestion and lifecycle" }, { "name": "Search", "description": "Search clips across your videos" }, { "name": "Jobs", "description": "Asynchronous capability and workflow execution" }, { "name": "Deletions", "description": "Asynchronous workspace-scoped cloud content erasure" } ], "paths": { "/videos": { "get": { "operationId": "listVideos", "x-cerul-required-scopes": [ "videos:read" ], "summary": "List videos", "description": "Lists videos in the caller's workspace, newest first, including unfinished uploads. Returns at most `limit` videos (default 100, maximum 500); pass `next_cursor` back as `cursor` for the next page. `q` matches the filename or the video ID. Per-video `data[].indexing` is authoritative.", "tags": [ "Videos" ], "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } }, { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string", "minLength": 1, "maxLength": 512 } }, { "name": "q", "in": "query", "required": false, "description": "Case-insensitive filename substring or an exact video ID.", "schema": { "type": "string", "minLength": 1, "maxLength": 200 } }, { "name": "status", "in": "query", "required": false, "description": "Restrict to videos whose derived status is one of these values (comma separated).", "schema": { "type": "string", "pattern": "^(uploading|uploaded|processing|ready|failed)(,(uploading|uploaded|processing|ready|failed))*$" } } ], "responses": { "200": { "description": "Videos in the workspace, newest first", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoListResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } }, "post": { "operationId": "createVideo", "x-cerul-required-scopes": [ "videos:write" ], "summary": "Add a video", "description": "Starts ingesting one video and returns it with a derived status. Supply\n`filename`, `media_type` and `byte_size` to upload bytes: the response\ncarries short-lived presigned PUT URLs in `upload.parts`. Upload each\nexact byte range with the returned headers, then call\nPOST /videos/{video_id}/complete. Maximum size is 5 GiB. A verified\nemail is required. Omit library_id to use the workspace default library.\nURL ingestion, including YouTube links, is not available; the reserved\n`url` form returns HTTP 503 `capability_unavailable`.\n", "tags": [ "Videos" ], "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVideoRequest" } } } }, "responses": { "201": { "description": "Video accepted, with upload instructions when bytes are expected", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateVideoResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/videos/{video_id}/complete": { "post": { "operationId": "completeVideoUpload", "x-cerul-required-scopes": [ "videos:write" ], "summary": "Complete a video upload", "description": "Confirms the uploaded bytes and starts indexing speech and visuals unless\n`index` is false. On-screen text is off by default; opt in with\nprocessing.screen_text.enabled and optionally\nprocessing.screen_text.interval_seconds (default 10). Indexing additionally\nrequires jobs:write, videos:read and files:write, a verified email\nand sufficient prepaid credits. Supply the actual duration and SHA-256.\nPoll GET /videos/{video_id} for ready, then inspect `indexing` per type;\nready does not guarantee every type succeeded. Exact\nIdempotency-Key replays return the original operation. Successful\nunique-material billing is deduplicated by `content_sha256`; a distinct\nvideo asset with the same bytes may still be processed independently.\n", "tags": [ "Videos" ], "parameters": [ { "$ref": "#/components/parameters/VideoId" }, { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompleteVideoRequest" } } } }, "responses": { "200": { "description": "Video with its derived status and index job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/videos/{video_id}/index": { "post": { "operationId": "indexVideo", "x-cerul-required-scopes": [ "jobs:write", "videos:read", "collection-items:write", "files:write" ], "summary": "Index a video", "description": "Starts indexing speech and visuals for a video completed with index false, or re-indexes it. On-screen text is off unless processing.screen_text.enabled is true. Requires verified email and sufficient prepaid credits. Idempotent by Idempotency-Key for the same video; successful unique-material billing is deduplicated by content hash.", "tags": [ "Videos" ], "parameters": [ { "$ref": "#/components/parameters/VideoId" }, { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IndexVideoRequest" } } } }, "responses": { "202": { "description": "Index job accepted or replayed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/videos/{video_id}/transcript": { "get": { "operationId": "getVideoTranscript", "x-cerul-required-scopes": [ "videos:read" ], "summary": "Read a video transcript", "description": "Returns the whole transcript once indexing has succeeded, so a caller need not walk from the job to its artifacts.", "tags": [ "Videos" ], "parameters": [ { "$ref": "#/components/parameters/VideoId" }, { "name": "include", "in": "query", "required": false, "description": "`screen_text` also returns the on-screen text read from the video, merged into `segments` in time order with `type: screen_text`. Identical text on adjacent samples is collapsed into one segment. `text` stays speech only. A video without an audio track returns its on-screen text instead of `no_audio_track`.", "schema": { "type": "string", "enum": [ "screen_text" ] } } ], "responses": { "200": { "description": "Transcript segments with timecodes", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TranscriptResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/videos/{video_id}": { "get": { "operationId": "getVideo", "x-cerul-required-scopes": [ "videos:read" ], "summary": "Get a video", "description": "Returns the video with a status derived from its newest index job, so one poll answers whether it is searchable yet.", "tags": [ "Videos" ], "parameters": [ { "$ref": "#/components/parameters/VideoId" } ], "responses": { "200": { "description": "Video", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteVideo", "summary": "Delete a video and reclaim its persisted storage", "description": "Accepts an idempotent asynchronous erasure of the video and all\ncloud-derived content and references. After safety preflight succeeds,\nthe video becomes unavailable immediately, active work is canceled or\nfenced by the deletion workflow, and storage quota is released only\nafter cleanup succeeds. A failed safety preflight has no tombstone or\ncancellation side effect.\nContent-free billing and security audit records may be retained. This\noperation never deletes or modifies local App content.\nReusing the same Idempotency-Key only replays the current receipt. If\na non-retryable safety preflight later becomes resolvable, a new key\nre-arms the same canonical deletion resource without losing completed\ncleanup phases.\n", "tags": [ "Videos" ], "x-cerul-required-scopes": [ "videos:write" ], "parameters": [ { "$ref": "#/components/parameters/VideoId" }, { "$ref": "#/components/parameters/IdempotencyKey" } ], "responses": { "202": { "description": "Video deletion accepted or idempotently replayed", "headers": { "Location": { "description": "Relative URL of the deletion status resource", "required": true, "schema": { "type": "string", "format": "uri-reference" } }, "Cache-Control": { "description": "Deletion responses must not be cached", "required": true, "schema": { "type": "string", "example": "no-store" } }, "Retry-After": { "description": "Suggested delay in seconds before refreshing deletion state", "required": true, "schema": { "type": "integer", "minimum": 1 } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceDeletionResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/exports": { "post": { "operationId": "createExport", "summary": "Export a time range of a video as an MP4 file", "description": "Starts an asynchronous export of one bounded time range. Poll GET /exports/{export_id}; once it succeeds, `file.url` downloads the MP4. Optionally pass the `clip_id` of a search result the range came from. Exports are not billed.", "tags": [ "Videos" ], "x-cerul-required-scopes": [ "jobs:write", "videos:read", "files:write" ], "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExportRequest" } } } }, "responses": { "202": { "description": "Export accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExportResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/exports/{export_id}": { "get": { "operationId": "getExport", "summary": "Get an export", "description": "Returns the export state. Pending exports include Retry-After. `file` is present once the export succeeded and until the file is deleted.", "tags": [ "Videos" ], "x-cerul-required-scopes": [ "jobs:read", "files:read" ], "parameters": [ { "$ref": "#/components/parameters/ExportId" } ], "responses": { "200": { "description": "Export", "headers": { "Retry-After": { "description": "Suggested polling delay in seconds; present while the export is pending.", "schema": { "type": "integer", "minimum": 1 } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ExportResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/exports/{export_id}/file": { "get": { "operationId": "getExportFile", "summary": "Download an exported MP4 file", "description": "Streams the exported MP4. Supports Range requests.", "tags": [ "Videos" ], "x-cerul-required-scopes": [ "files:read" ], "parameters": [ { "$ref": "#/components/parameters/ExportId" }, { "name": "Range", "in": "header", "required": false, "schema": { "type": "string", "pattern": "^bytes=(?:[0-9]+-[0-9]*|-[0-9]+)$" } } ], "responses": { "200": { "description": "MP4 file", "content": { "video/mp4": { "schema": { "type": "string", "format": "binary" } } } }, "206": { "description": "Partial MP4 content", "content": { "video/mp4": { "schema": { "type": "string", "format": "binary" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/search": { "post": { "operationId": "search", "x-cerul-required-scopes": [ "videos:read" ], "summary": "Search your videos", "description": "Returns the clips that best answer the query, across all videos in the workspace unless `filter.video_ids` narrows it. Requires a verified email and sufficient prepaid credits. Results are ordered by fused relevance; `score` is the raw similarity within the clip's own `type` and is not comparable across types or a correctness probability. When nothing is close enough the response has an empty `data` and the `no_confident_match` warning; that completed search is still billed. A search with no indexed videos is not billed. `playback.url` and `thumbnail_url` expire after one hour; store `console_url` instead. Reuse the same Idempotency-Key only with unchanged input.", "tags": [ "Search" ], "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchRequest" } } } }, "responses": { "200": { "description": "Search results", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/jobs/{job_id}": { "get": { "operationId": "getJob", "x-cerul-required-scopes": [ "jobs:read" ], "summary": "Get job state", "description": "Returns state and progress. Pending jobs include Retry-After; wait that many seconds before polling again. Failed jobs expose their asynchronous execution error.", "tags": [ "Jobs" ], "parameters": [ { "$ref": "#/components/parameters/JobId" } ], "responses": { "200": { "description": "Job", "headers": { "Retry-After": { "description": "Suggested polling delay in seconds; present while the job is pending.", "schema": { "type": "integer", "minimum": 1 } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/jobs/{job_id}/cancel": { "post": { "operationId": "cancelJob", "x-cerul-required-scopes": [ "jobs:write" ], "summary": "Request idempotent cancellation of a job", "description": "Queued cancellation releases unused prepaid reservations. Running jobs stop cooperatively, settle durable successful billable units, and release the remainder. Canceling a terminal job does not reverse completed usage.", "tags": [ "Jobs" ], "parameters": [ { "$ref": "#/components/parameters/JobId" }, { "$ref": "#/components/parameters/IdempotencyKey" } ], "responses": { "200": { "description": "Job cancellation state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/deletions/{deletion_id}": { "get": { "operationId": "getResourceDeletion", "x-cerul-required-scopes": [], "summary": "Get asynchronous cloud deletion state", "description": "Requires videos:write for a video deletion and files:write for a file deletion.", "tags": [ "Deletions" ], "x-cerul-dynamic-required-scopes": "deletion_resource_type", "parameters": [ { "$ref": "#/components/parameters/DeletionId" } ], "responses": { "200": { "description": "Workspace-scoped deletion state", "headers": { "Cache-Control": { "description": "Deletion status responses must not be cached", "schema": { "type": "string", "example": "no-store" } }, "Retry-After": { "description": "Suggested polling delay in seconds while deletion is queued or running", "schema": { "type": "integer", "minimum": 1 } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResourceDeletionResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/videos/{video_id}/content": { "get": { "operationId": "getVideoContent", "x-cerul-required-scopes": [ "videos:read" ], "summary": "Stream authenticated source video", "tags": [ "Videos" ], "description": "Downloads the original uploaded file. Requires videos:read. Supports a single HTTP byte range for seeking; content remains workspace private and unavailable after deletion. Full and ranged responses preserve the uploaded video's media type, represented by video/*; playback support depends on the client's container and codec support.", "parameters": [ { "$ref": "#/components/parameters/VideoId" }, { "in": "header", "name": "Range", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Private source video", "content": { "video/*": { "schema": { "type": "string", "format": "binary" } }, "video/mp4": { "schema": { "type": "string", "format": "binary" } }, "video/webm": { "schema": { "type": "string", "format": "binary" } } } }, "206": { "description": "Requested byte range", "headers": { "Content-Range": { "schema": { "type": "string" } } }, "content": { "video/*": { "schema": { "type": "string", "format": "binary" } }, "video/webm": { "schema": { "type": "string", "format": "binary" } }, "video/mp4": { "schema": { "type": "string", "format": "binary" } } } }, "416": { "description": "Unsatisfiable byte range" }, "default": { "$ref": "#/components/responses/Error" } } } } }, "components": { "schemas": { "VideoListResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } }, "next_cursor": { "description": "Pass as `cursor` to read the next page; null on the last page.", "type": [ "string", "null" ] } } } ] }, "ResponseMetadata": { "type": "object", "required": [ "request_id", "usage", "warnings" ], "properties": { "request_id": { "$ref": "#/components/schemas/RequestId" }, "usage": { "$ref": "#/components/schemas/Usage" }, "warnings": { "type": "array", "items": { "type": "string" } } } }, "RequestId": { "type": "string", "pattern": "^req_[A-Za-z0-9]+$" }, "Usage": { "type": "object", "additionalProperties": false, "required": [ "billable", "quantity", "unit" ], "properties": { "billable": { "description": "True when this logical request is metered and consumes Cerul credits, whether funded by the one-time free grant or prepaid top-ups.", "type": "boolean" }, "quantity": { "description": "Quantity metered synchronously for this response. Async submissions report zero and identify deferred settlement in usage_pending.", "type": "number", "minimum": 0 }, "unit": { "description": "Unit for quantity, such as request.", "type": "string" }, "usage_pending": { "description": "Indicates that any metering happens when the accepted background job settles, not in this response.", "type": "string", "enum": [ "background_job" ] } } }, "Video": { "type": "object", "additionalProperties": false, "required": [ "id", "status", "playable", "indexing", "filename", "media_type", "byte_size", "duration_seconds", "summary", "summary_generated", "suggested_queries", "error", "created_at", "indexed_at", "index_job_id" ], "properties": { "id": { "$ref": "#/components/schemas/VideoId" }, "status": { "description": "Derived from the newest index Job, never stored. `ready` means that Job has a retained searchable index; an older successful index can still be searchable after a newer re-index fails.", "type": "string", "enum": [ "uploading", "uploaded", "processing", "ready", "failed" ] }, "playable": { "description": "Whether the web-playable copy is ready. A video can be playable before it is searchable.", "type": "boolean" }, "indexing": { "description": "Per-type result for the newest retained index; null when the newest Job has no valid index, including after a failed re-index.", "oneOf": [ { "$ref": "#/components/schemas/VideoIndexing" }, { "type": "null" } ] }, "filename": { "type": [ "string", "null" ] }, "media_type": { "type": [ "string", "null" ] }, "byte_size": { "type": [ "integer", "null" ], "minimum": 0 }, "duration_seconds": { "type": [ "number", "null" ], "minimum": 0 }, "summary": { "description": "One or two sentences generated from the transcript and on-screen text after indexing. Generated content; see summary_generated.", "type": [ "string", "null" ], "maxLength": 600 }, "summary_generated": { "description": "True when summary was written by a model rather than taken from the video.", "type": "boolean" }, "suggested_queries": { "description": "Up to six phrases from the video that make good searches.", "type": "array", "maxItems": 6, "items": { "$ref": "#/components/schemas/SuggestedQuery" } }, "error": { "description": "Why indexing failed, when it did.", "type": [ "string", "null" ] }, "created_at": { "type": [ "string", "null" ], "format": "date-time" }, "indexed_at": { "type": [ "string", "null" ], "format": "date-time" }, "index_job_id": { "type": [ "string", "null" ] }, "upload": { "description": "Present only in the response that created the video, while bytes are still expected.", "$ref": "#/components/schemas/VideoUploadInstructions" } } }, "VideoId": { "description": "A Video is the developer-facing name for a media Asset, and shares its identifier.", "type": "string", "pattern": "^asset_[A-Za-z0-9_-]+$" }, "VideoIndexing": { "type": "object", "additionalProperties": false, "required": [ "speech", "screen_text", "visual" ], "properties": { "speech": { "$ref": "#/components/schemas/VideoIndexingResult" }, "screen_text": { "$ref": "#/components/schemas/VideoIndexingResult" }, "visual": { "$ref": "#/components/schemas/VideoIndexingResult" } } }, "VideoIndexingResult": { "type": "object", "additionalProperties": false, "required": [ "status", "count", "reason" ], "properties": { "status": { "type": "string", "enum": [ "indexed", "absent", "failed" ] }, "count": { "description": "Number of indexed units of this type.", "type": "integer", "minimum": 0 }, "reason": { "type": [ "string", "null" ], "enum": [ "no_audio_track", "partial_failure", "asr_unavailable", "embedding_failed", "decode_failed", "model_unavailable", "startup_failed", "timeout", "oom", "invalid_output", "internal_error", "disabled", "no_text_detected", null ] }, "failed_count": { "description": "Present only when status is indexed and reason is partial_failure; omitted for absent and failed outcomes.", "type": "integer", "minimum": 1 } } }, "SuggestedQuery": { "type": "object", "additionalProperties": false, "required": [ "text", "type", "start_seconds" ], "properties": { "text": { "description": "A phrase taken verbatim from the video's transcript or on-screen text.", "type": "string", "minLength": 1, "maxLength": 160 }, "type": { "type": "string", "enum": [ "speech", "screen_text" ] }, "start_seconds": { "type": "number", "minimum": 0 } } }, "VideoUploadInstructions": { "type": "object", "additionalProperties": false, "required": [ "method", "url", "required_headers", "expires_at", "parts" ], "properties": { "method": { "type": "string", "const": "PUT" }, "url": { "type": "string", "format": "uri" }, "required_headers": { "type": "object", "additionalProperties": false, "required": [ "Content-Type", "Content-Length" ], "properties": { "Content-Type": { "type": "string", "minLength": 3, "maxLength": 127 }, "Content-Length": { "type": "string", "pattern": "^[1-9][0-9]{0,9}$" } } }, "expires_at": { "type": "string", "format": "date-time" }, "parts": { "description": "Upload each part with its own PUT. A dropped connection costs one part, not the whole file. A small file has exactly one.", "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": [ "part_number", "url", "byte_size" ], "properties": { "part_number": { "type": "integer", "minimum": 1, "maximum": 10000 }, "url": { "type": "string", "format": "uri" }, "byte_size": { "type": "integer", "minimum": 1 } } } } } }, "ErrorEnvelope": { "type": "object", "additionalProperties": false, "required": [ "request_id", "error" ], "properties": { "request_id": { "$ref": "#/components/schemas/RequestId" }, "error": { "$ref": "#/components/schemas/ErrorDetail" } } }, "ErrorDetail": { "type": "object", "additionalProperties": false, "required": [ "code", "message", "retryable" ], "properties": { "code": { "type": "string", "enum": [ "invalid_request", "invalid_credentials", "invalid_grant", "invalid_client", "unauthorized", "forbidden", "permission_denied", "already_exists", "session_expired", "not_found", "no_audio_track", "conflict", "authorization_pending", "slow_down", "access_denied", "expired_token", "capability_unavailable", "scope_violation", "data_egress_not_authorized", "insufficient_entitlement", "rate_limited", "internal_error" ] }, "message": { "type": "string" }, "retryable": { "type": "boolean" }, "retry_after_seconds": { "description": "Suggested delay before retrying a throttled request, when supplied.", "type": "integer", "minimum": 1 }, "field": { "type": [ "string", "null" ] }, "details": { "type": "object", "additionalProperties": true } } }, "CreateVideoRequest": { "oneOf": [ { "$ref": "#/components/schemas/CreateVideoFromUrlRequest" }, { "$ref": "#/components/schemas/CreateVideoUploadRequest" } ] }, "CreateVideoFromUrlRequest": { "type": "object", "additionalProperties": false, "required": [ "url" ], "properties": { "url": { "description": "Public source to fetch. Planned; currently answers capability_unavailable.", "type": "string", "format": "uri", "maxLength": 2048 } } }, "CreateVideoUploadRequest": { "type": "object", "additionalProperties": false, "required": [ "filename", "media_type", "byte_size" ], "properties": { "filename": { "type": "string", "minLength": 1, "maxLength": 255 }, "media_type": { "type": "string", "minLength": 3, "maxLength": 127, "pattern": "^[Vv][Ii][Dd][Ee][Oo]/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$" }, "byte_size": { "type": "integer", "minimum": 1, "maximum": 5368709120 } } }, "CreateVideoResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Video" } } } ] }, "CompleteVideoRequest": { "oneOf": [ { "$ref": "#/components/schemas/CompleteVideoWithoutIndexRequest" }, { "$ref": "#/components/schemas/CompleteAndIndexVideoRequest" } ] }, "CompleteVideoWithoutIndexRequest": { "type": "object", "additionalProperties": false, "required": [ "index" ], "properties": { "duration_seconds": { "description": "Source duration, used for quota admission before transcription.", "type": "number", "minimum": 0 }, "content_sha256": { "description": "Declares source identity for upload completion. When indexing occurs, the Worker verifies it against the uploaded bytes and successful unique-material billing is deduplicated by this hash; a distinct video asset may still be processed independently.", "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }, "index": { "description": "Leave the uploaded video unindexed.", "type": "boolean", "const": false } } }, "CompleteAndIndexVideoRequest": { "type": "object", "additionalProperties": false, "required": [ "duration_seconds", "content_sha256" ], "properties": { "duration_seconds": { "description": "Source duration, used for quota admission before transcription.", "type": "number", "exclusiveMinimum": 0 }, "content_sha256": { "description": "Declares source identity for indexing. The Worker verifies it against the uploaded bytes and successful unique-material billing is deduplicated by this hash; a distinct video asset may still be processed independently.", "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }, "index": { "description": "Start indexing once the upload is confirmed. Defaults to true.", "type": "boolean", "enum": [ true ] }, "processing": { "$ref": "#/components/schemas/VideoProcessingOptions" } } }, "VideoProcessingOptions": { "type": "object", "additionalProperties": false, "properties": { "screen_text": { "type": "object", "additionalProperties": false, "properties": { "enabled": { "description": "Index text visible on screen, sampled independently of speech. Defaults to false.", "type": "boolean", "default": false }, "interval_seconds": { "description": "On-screen text sampling interval. Defaults to 10 seconds.", "type": "integer", "minimum": 1, "maximum": 3600, "default": 10 } } } } }, "VideoResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Video" } } } ] }, "IndexVideoRequest": { "type": "object", "additionalProperties": false, "description": "To re-index a video, send an empty body; the duration and content hash from its last successful index are reused. Both fields are required until an index succeeds, and must be sent together.", "dependentRequired": { "duration_seconds": [ "content_sha256" ], "content_sha256": [ "duration_seconds" ] }, "properties": { "duration_seconds": { "type": "number", "exclusiveMinimum": 0 }, "content_sha256": { "description": "Declares source identity for indexing. The Worker verifies it against the uploaded bytes and successful unique-material billing is deduplicated by this hash; a distinct video asset may still be processed independently.", "type": "string", "pattern": "^[a-fA-F0-9]{64}$" }, "processing": { "$ref": "#/components/schemas/VideoProcessingOptions" } } }, "JobResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Job" } } } ] }, "Job": { "type": "object", "additionalProperties": false, "required": [ "id", "status", "type", "video_id", "created_at", "updated_at" ], "properties": { "id": { "$ref": "#/components/schemas/JobId" }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed", "cancel_requested", "canceled" ] }, "type": { "description": "What the job does. `index` builds a video's searchable index, `export` produces an MP4 file, `annotate` produces semantic annotations; `other` covers internal jobs.", "type": "string", "enum": [ "index", "export", "annotate", "other" ] }, "video_id": { "description": "The video the job works on, when it has exactly one.", "oneOf": [ { "$ref": "#/components/schemas/VideoId" }, { "type": "null" } ] }, "progress": { "description": "Current stage and its progress. Index stages are upload, playable, speech, visual and index; speech and visual run at the same time.", "type": "object", "additionalProperties": false, "required": [ "stage", "completed", "total" ], "properties": { "stage": { "type": "string" }, "completed": { "type": "integer", "minimum": 0 }, "total": { "type": "integer", "minimum": 1 } } }, "failure": { "type": [ "string", "null" ], "description": "Failure reason for a terminal job, including media processing startup failures." }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "JobId": { "type": "string", "pattern": "^job_[A-Za-z0-9_-]+$" }, "TranscriptResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Transcript" } } } ] }, "Transcript": { "type": "object", "additionalProperties": false, "required": [ "video_id", "format", "segments", "text" ], "properties": { "video_id": { "type": [ "string", "null" ] }, "format": { "type": "string", "enum": [ "json", "srt", "vtt" ] }, "segments": { "type": "array", "items": { "$ref": "#/components/schemas/TranscriptSegment" } }, "text": { "type": "string" } } }, "TranscriptSegment": { "type": "object", "additionalProperties": false, "required": [ "type", "start_seconds", "end_seconds", "text" ], "properties": { "type": { "description": "`speech` for the transcript; `screen_text` only when requested with `include=screen_text`.", "type": "string", "enum": [ "speech", "screen_text" ] }, "start_seconds": { "type": [ "number", "null" ], "minimum": 0 }, "end_seconds": { "type": [ "number", "null" ], "minimum": 0 }, "text": { "type": "string" } } }, "ResourceDeletionResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/ResourceDeletion" } } } ] }, "ResourceDeletion": { "type": "object", "additionalProperties": false, "required": [ "id", "resource_type", "resource_id", "status", "status_url", "requested_at", "updated_at", "completed_at", "failure" ], "properties": { "id": { "$ref": "#/components/schemas/DeletionId" }, "resource_type": { "type": "string", "enum": [ "video", "file" ] }, "resource_id": { "oneOf": [ { "$ref": "#/components/schemas/AssetId" }, { "$ref": "#/components/schemas/ArtifactId" } ] }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed" ] }, "status_url": { "type": "string", "format": "uri-reference" }, "requested_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "completed_at": { "type": [ "string", "null" ], "format": "date-time" }, "failure": { "oneOf": [ { "$ref": "#/components/schemas/DeletionFailure" }, { "type": "null" } ] } } }, "DeletionId": { "type": "string", "pattern": "^del_[A-Za-z0-9_-]+$" }, "AssetId": { "type": "string", "pattern": "^asset_[A-Za-z0-9_-]+$" }, "ArtifactId": { "type": "string", "pattern": "^artifact_[A-Za-z0-9_-]+$" }, "DeletionFailure": { "type": "object", "additionalProperties": false, "required": [ "code", "retryable" ], "properties": { "code": { "type": "string", "enum": [ "deletion_failed" ] }, "retryable": { "type": "boolean" } } }, "ExportRequest": { "type": "object", "additionalProperties": false, "required": [ "video_id", "start_seconds", "end_seconds" ], "properties": { "video_id": { "$ref": "#/components/schemas/VideoId" }, "start_seconds": { "type": "number", "minimum": 0 }, "end_seconds": { "type": "number", "exclusiveMinimum": 0 }, "clip_id": { "description": "The search result clip this range came from, recorded for reference.", "$ref": "#/components/schemas/ClipId" } } }, "ClipId": { "description": "Identifies one search result clip. Stable across searches of the same index.", "type": "string", "pattern": "^ev_[A-Za-z0-9_-]+$" }, "ExportResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Export" } } } ] }, "Export": { "type": "object", "additionalProperties": false, "required": [ "id", "status", "video_id", "start_seconds", "end_seconds", "clip_id", "file", "failure", "created_at", "updated_at" ], "properties": { "id": { "$ref": "#/components/schemas/ExportId" }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed", "cancel_requested", "canceled" ] }, "video_id": { "$ref": "#/components/schemas/VideoId" }, "start_seconds": { "type": "number", "minimum": 0 }, "end_seconds": { "type": "number", "minimum": 0 }, "clip_id": { "oneOf": [ { "$ref": "#/components/schemas/ClipId" }, { "type": "null" } ] }, "file": { "description": "The exported MP4 once the export succeeded; null before that and after the file is deleted.", "oneOf": [ { "$ref": "#/components/schemas/ExportFile" }, { "type": "null" } ] }, "failure": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "ExportId": { "description": "An export is identified by the job that produces it.", "type": "string", "pattern": "^job_[A-Za-z0-9_-]+$" }, "ExportFile": { "type": "object", "additionalProperties": false, "required": [ "url", "content_type", "byte_size", "duration_seconds" ], "properties": { "url": { "description": "Authenticated download URL (GET /exports/{export_id}/file) using the same API key.", "type": "string", "format": "uri" }, "content_type": { "type": "string", "const": "video/mp4" }, "byte_size": { "type": "integer", "minimum": 0 }, "duration_seconds": { "type": "number", "minimum": 0 } } }, "SearchRequest": { "type": "object", "additionalProperties": false, "required": [ "query" ], "properties": { "query": { "type": "string", "minLength": 1, "maxLength": 1000 }, "filter": { "description": "Omit to search every video in the workspace.", "$ref": "#/components/schemas/SearchFilter" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "include": { "description": "Optional extra blocks. `debug` adds model, version and ranking details to each clip.", "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": [ "debug" ] } } } }, "SearchFilter": { "type": "object", "additionalProperties": false, "properties": { "video_ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/components/schemas/VideoId" } } } }, "SearchResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Clip" } } } } ] }, "Clip": { "type": "object", "additionalProperties": false, "required": [ "id", "video_id", "start_seconds", "end_seconds", "score", "type", "text", "related", "context", "thumbnail_url", "playback", "console_url", "video" ], "properties": { "id": { "$ref": "#/components/schemas/ClipId" }, "video_id": { "$ref": "#/components/schemas/VideoId" }, "start_seconds": { "type": "number", "minimum": 0 }, "end_seconds": { "type": "number", "minimum": 0 }, "score": { "description": "Raw similarity within this clip's type, 0 to 1. Not comparable across types.", "type": "number", "minimum": 0, "maximum": 1 }, "type": { "$ref": "#/components/schemas/ClipType" }, "text": { "description": "Verbatim transcript or on-screen text; null for a visual match.", "type": [ "string", "null" ], "maxLength": 4000 }, "related": { "description": "Other types that matched in the same stretch of the video.", "type": "array", "maxItems": 10, "items": { "$ref": "#/components/schemas/ClipMatch" } }, "context": { "$ref": "#/components/schemas/ClipContext" }, "thumbnail_url": { "description": "Signed JPEG link, valid for one hour; null while the web-playable copy is not ready.", "type": [ "string", "null" ], "format": "uri" }, "playback": { "description": "Signed link that plays this clip, valid for one hour; null while the web-playable copy is not ready.", "oneOf": [ { "$ref": "#/components/schemas/PlaybackLink" }, { "type": "null" } ] }, "console_url": { "description": "Permanent Console link to this moment; opening it requires signing in to the workspace.", "type": "string", "format": "uri" }, "video": { "$ref": "#/components/schemas/ClipVideo" }, "debug": { "$ref": "#/components/schemas/ClipDebug" } } }, "ClipType": { "description": "What matched. `speech` is spoken words, `screen_text` is text visible on screen, `visual` is the picture itself.", "type": "string", "enum": [ "speech", "screen_text", "visual" ] }, "ClipMatch": { "type": "object", "additionalProperties": false, "required": [ "type", "start_seconds", "end_seconds", "text" ], "properties": { "type": { "$ref": "#/components/schemas/ClipType" }, "start_seconds": { "type": "number", "minimum": 0 }, "end_seconds": { "type": "number", "minimum": 0 }, "text": { "description": "Verbatim transcript or on-screen text; null for a visual match.", "type": [ "string", "null" ], "maxLength": 4000 } } }, "ClipContext": { "description": "Verbatim transcript and on-screen text just before and after the clip (up to 15 seconds, at most two lines each side). Never generated.", "type": "object", "additionalProperties": false, "required": [ "before", "after" ], "properties": { "before": { "type": "array", "maxItems": 2, "items": { "$ref": "#/components/schemas/ClipContextLine" } }, "after": { "type": "array", "maxItems": 2, "items": { "$ref": "#/components/schemas/ClipContextLine" } } } }, "ClipContextLine": { "type": "object", "additionalProperties": false, "required": [ "type", "start_seconds", "end_seconds", "text" ], "properties": { "type": { "type": "string", "enum": [ "speech", "screen_text" ] }, "start_seconds": { "type": "number", "minimum": 0 }, "end_seconds": { "type": "number", "minimum": 0 }, "text": { "type": "string", "maxLength": 4000 } } }, "PlaybackLink": { "type": "object", "additionalProperties": false, "required": [ "url", "format", "expires_at" ], "properties": { "url": { "description": "Signed link to the web-playable copy. For a clip it ends with a `#t=start,end` media fragment that browsers honour.", "type": "string", "format": "uri" }, "format": { "type": "string", "enum": [ "mp4", "hls" ] }, "expires_at": { "type": "string", "format": "date-time" } } }, "ClipVideo": { "type": "object", "additionalProperties": false, "required": [ "filename", "duration_seconds", "summary", "summary_generated" ], "properties": { "filename": { "type": [ "string", "null" ] }, "duration_seconds": { "type": [ "number", "null" ], "minimum": 0 }, "summary": { "type": [ "string", "null" ], "maxLength": 600 }, "summary_generated": { "type": "boolean" } } }, "ClipDebug": { "type": "object", "additionalProperties": false, "required": [ "rank", "index_job_id", "provenance" ], "properties": { "rank": { "description": "Position after fusing all types, starting at 1.", "type": "integer", "minimum": 1 }, "index_job_id": { "$ref": "#/components/schemas/JobId" }, "provenance": { "oneOf": [ { "$ref": "#/components/schemas/EvidenceProvenance" }, { "type": "null" } ] } } }, "EvidenceProvenance": { "type": "object", "additionalProperties": false, "required": [ "station", "model_id", "model_version", "segment_key" ], "properties": { "station": { "type": "string", "enum": [ "speech", "visual" ] }, "model_id": { "type": "string", "minLength": 1, "maxLength": 128 }, "model_version": { "type": "string", "minLength": 1, "maxLength": 128 }, "segment_key": { "type": "string", "minLength": 8, "maxLength": 256 } } } }, "responses": { "Error": { "description": "Standard Cerul error envelope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } } }, "parameters": { "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 8, "maxLength": 200 } }, "VideoId": { "name": "video_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/VideoId" } }, "ExportId": { "name": "export_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ExportId" } }, "JobId": { "name": "job_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/JobId" } }, "DeletionId": { "name": "deletion_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/DeletionId" } } }, "securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "Cerul access token" } } } } ```