# Cerul API Documentation: https://cerul.ai/docs Base URL: https://api.cerul.ai (no /v1). # 快速開始 將 YOUR_API_KEY 換成在 Console 建立的 Key,直接執行以下請求即可列出影片,無需先上傳檔案。 ```bash curl --fail-with-body "https://api.cerul.ai/videos" \ -H "Authorization: Bearer YOUR_API_KEY" ``` 影片清單位於 data;新工作區回傳空清單是正常結果。已有 ready 影片可直接[搜尋](/docs.md?locale=zh-Hant&page=search),否則先[上傳影片](/docs.md?locale=zh-Hant&page=upload)。 ## 請求參數 | 參數 | 類型 · 必填 | 說明 | | --- | --- | --- | | Authorization | string · 必填 | Bearer YOUR_API_KEY | | library_id | string · 選填 | pattern: ^lib_[A-Za-z0-9_-]+$ | ## 閱讀回傳結果 data[].id 是影片 ID;filename 是檔名;status 表示處理狀態。ready 表示可搜尋,但仍需檢查 coverage 和 warnings,確認語音、畫面和 OCR 的實際結果。 ## 完整範例 [下載完整範例](/docs-quickstart.sh) 請在同一個 Bash 工作階段執行,需要 curl(7.76+)、jq、Python 3 與 ffprobe。影片放在 talk.mp4。處理與搜尋會消耗額度;重試相同請求時保留本次 run ID。 基礎位址為 https://api.cerul.ai,不含 /v1;標頭使用 Authorization: Bearer 。上傳、索引、搜尋或匯出前先驗證帳號信箱。Key 僅放伺服器端;同一 Workspace 的 Key、網頁與 CLI 雲端呼叫共用錢包。 ## 重試與錯誤 依介面要求傳送 8–200 字元的 Idempotency-Key,搜尋也需要。回應不確定時保留原鍵與原輸入;不同操作或輸入使用新鍵。輪詢未完成工作時遵循 Retry-After 並設定期限。用戶端逾時不會取消伺服器工作。 • 401 · invalid_credentials: 確認憑證是否錯誤、過期或已撤銷。 • 403 · permission_denied / insufficient_entitlement: 檢查權限、信箱驗證、權益與錢包餘額。 • 429 · rate_limited: 若回傳 retry_after_seconds,依其退避;檢查並行限制。 --- # 上傳影片 將影片放在 talk.mp4 並替換 YOUR_API_KEY。此請求使用真實檔案大小建立影片並取得上傳網址,尚未傳送檔案內容。 ```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}" ``` ## 請求參數 | 參數 | 類型 · 必填 | 說明 | | --- | --- | --- | | Authorization | string · 必填 | Bearer YOUR_API_KEY | | filename | string · 必填 | minLength: 1; maxLength: 255 | | media_type | string · 必填 | minLength: 3; maxLength: 127; pattern: ^[Vv][Ii][Dd][Ee][Oo]/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$ | | byte_size | integer · 必填 | ≥ 1; ≤ 5368709120 | | library_id | string · 選填 | Omit to file the video in the workspace default library. — pattern: ^lib_[A-Za-z0-9_-]+$ | | execution_policy | string · 選填 | local_only, prefer_local, cloud_allowed, cloud_required | | Idempotency-Key | string · 必填 | minLength: 8; maxLength: 200 | Required scopes: assets:write. ## 上傳檔案內容 保存 data.id。依 data.upload.parts 順序,將精確的位元組區間 PUT 到臨時網址,並使用 data.upload.required_headers。不要將 API Key 傳送至上傳網址。單檔最大 5 GiB,不支援 URL 匯入。 [下載完整範例](/docs-quickstart.sh) 請在同一個 Bash 工作階段執行,需要 curl(7.76+)、jq、Python 3 與 ffprobe。影片放在 talk.mp4。處理與搜尋會消耗額度;重試相同請求時保留本次 run ID。 ## 確認上傳完成 所有分片成功後,呼叫 POST /videos/{video_id}/complete,附上 Bearer、Content-Type: application/json 和新的 Idempotency-Key,提交真實時長及 SHA-256。確認完成預設啟動索引,不要重複提交索引請求。 | 參數 | 類型 · 必填 | 說明 | | --- | --- | --- | | Authorization | string · 必填 | Bearer YOUR_API_KEY | | duration_seconds | number · 索引時必填 | Source duration, used for quota admission before transcription. — > 0 | | content_sha256 | string · 索引時必填 | 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 · 選填 | false: upload only; true: start indexing — default: true | | processing | object · 選填 | | | processing.ocr | object · 選填 | | | processing.ocr.enabled | boolean · 選填 | Extract visible text as independently sampled evidence. Defaults to false. — default: false | | processing.ocr.interval_seconds | integer · 選填 | OCR sampling interval. Defaults to 10 seconds. — ≥ 1; ≤ 3600; default: 10 | | execution_policy | string · 選填 | local_only, prefer_local, cloud_allowed, cloud_required | | video_id | string · 必填 | A Video is the developer-facing name for a media Asset, and shares its identifier. — pattern: ^asset_[A-Za-z0-9_-]+$ | | Idempotency-Key | string · 必填 | minLength: 8; maxLength: 200 | Required scopes: assets:write, library-items:write. 索引另需 jobs:write、assets:read、artifacts:write。僅保存影片時設 index:false,之後可呼叫 POST /videos/{video_id}/index。 ## 等待索引 以截止時間輪詢 GET /videos/{video_id}:ready 可搜尋,failed 時檢查 error,processing 時繼續等待。用戶端逾時不會取消服務端工作。 OCR 預設關閉。完成上傳前設定 OCR_ENABLED=true,即可每 10 秒取樣畫面文字。ready 表示可搜尋;請檢查 coverage 與 warnings,確認語音、畫面和 OCR 的實際覆蓋。沒有音軌的影片沒有轉錄。 --- # 搜尋 替換 YOUR_API_KEY 和 YOUR_VIDEO_ID 即可搜尋已就緒影片。重試相同請求時保留 REQUEST_KEY。 ```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 }' ``` ## 請求參數 | 參數 | 類型 · 必填 | 說明 | | --- | --- | --- | | Authorization | string · 必填 | Bearer YOUR_API_KEY | | query | string · 必填 | minLength: 1; maxLength: 1000 | | filter | object · 選填 | Omit to search the whole workspace. | | filter.video_ids | array · 選填 | uniqueItems | | filter.library_ids | array · 選填 | uniqueItems | | execution_policy | string · 選填 | local_only, prefer_local, cloud_allowed, cloud_required | | limit | integer · 選填 | ≥ 1; ≤ 100; default: 20 | | Idempotency-Key | string · 必填 | minLength: 8; maxLength: 200 | Required scopes: libraries:read, assets:read. ## 閱讀結果 每個 data 項目包含 evidence 與 score。evidence.id 標識證據,asset_id 標識來源;start_seconds 和 end_seconds 是影片中的起止秒數。quote 是證據文字,kind 和 modality 區分語音、OCR 與畫面。score 是相關性排序,不是正確機率。 保留 request_id、usage 和 warnings。展示來源、時間與證據文字,先核對原片段,再[匯出剪輯](/docs.md?locale=zh-Hant&page=clips)。 ## 沒有命中 data 空陣列不等於請求失敗。先檢查就緒狀態、篩選與 coverage,再調整查詢。有效檢索無命中仍可能收費;沒有已索引證據則不收搜尋費。GET /videos/{video_id}/transcript 讀取語音轉錄,無音軌可能回傳 422 no_audio_track。 --- # 匯出與管理 替換 Key 與影片 ID,建立 30–45 秒的剪輯。請先確認原影片包含此範圍;回傳的是非同步工作,不是已完成的下載。 ```bash REQUEST_KEY="clip-$(uuidgen)" curl --fail-with-body "https://api.cerul.ai/clips" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $REQUEST_KEY" \ -d '{"video_id":"YOUR_VIDEO_ID","start_s":30,"end_s":45}' ``` ## 請求參數 | 參數 | 類型 · 必填 | 說明 | | --- | --- | --- | | Authorization | string · 必填 | Bearer YOUR_API_KEY | | video_id | string · 必填 | A Video is the developer-facing name for a media Asset, and shares its identifier. — pattern: ^asset_[A-Za-z0-9_-]+$ | | start_s | number · 必填 | ≥ 0 | | end_s | number · 必填 | > 0 | | evidence_ids | array · 選填 | maxItems: 100; uniqueItems | | execution_policy | string · 選填 | local_only, prefer_local, cloud_allowed, cloud_required | | Idempotency-Key | string · 必填 | minLength: 8; maxLength: 200 | Required scopes: jobs:write, assets:read, artifacts:write. ## 等待與下載 保存 data.id 為 JOB_ID。依 Retry-After 和截止時間輪詢 GET /jobs/{job_id}。succeeded 後呼叫 GET /jobs/{job_id}/artifacts,按類型與中繼資料選擇剪輯,再以 Bearer 下載 GET /artifacts/{artifact_id}/content。查詢工作需 jobs:read,讀取產物需 artifacts:read。failed 或 canceled 時停止輪詢。 ```bash curl --fail-with-body "https://api.cerul.ai/artifacts/YOUR_ARTIFACT_ID/content" \ -H "Authorization: Bearer YOUR_API_KEY" \ --output clip.mp4 ``` ## 取消或刪除 POST /jobs/{job_id}/cancel 請求取消,需持續查詢到終態。DELETE /videos/{video_id} 清理影片與關聯資料(assets:write);DELETE /artifacts/{artifact_id} 刪除單個產物(artifacts:write)。收到 202 刪除回執後,輪詢 GET /deletions/{deletion_id} 直到清理完成。 成功檢索即使沒有命中也可能收費;沒有已索引證據的請求不收搜尋費。失敗或取消仍可能結算成功處理的部分,並釋放剩餘預留。刪除回傳 202 後應持續查詢回執直到清理完成。透過 /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. OCR 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 OCR_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. OCR 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 ocr "$OCR_ENABLED" \ '{duration_seconds:$duration,content_sha256:$hash,processing:{ocr:{enabled:$ocr,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.coverage'; 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,data}' # A valid search can return no results. Export only a bounded evidence span. if ! HIT=$(printf '%s' "$SEARCH" | jq -ce 'first(.data[] | select( (.evidence.start_seconds | type) == "number" and (.evidence.end_seconds | type) == "number" and .evidence.end_seconds > .evidence.start_seconds))'); then echo "No evidence span to export. Review the query or available coverage." exit 0 fi CLIP_BODY=$(printf '%s' "$HIT" | jq --arg video "$CERUL_VIDEO_ID" \ '{video_id:$video,start_s:.evidence.start_seconds,end_s:.evidence.end_seconds,evidence_ids:[.evidence.id]}') CLIP=$(cerul https://api.cerul.ai/clips \ -H "Idempotency-Key: clip-quickstart-$CERUL_QUICKSTART_RUN_ID" -d "$CLIP_BODY") CLIP_JOB_ID=$(printf '%s' "$CLIP" | jq -er '.data.id') DEADLINE=$((SECONDS + 1800)) while :; do JOB=$(cerul "https://api.cerul.ai/jobs/$CLIP_JOB_ID" -D "$CERUL_TMP/job-headers") CLIP_STATUS=$(printf '%s' "$JOB" | jq -er '.data.status') case "$CLIP_STATUS" in succeeded) break ;; failed|canceled) printf '%s' "$JOB" | jq '.data' >&2; exit 1 ;; esac if (( SECONDS >= DEADLINE )); then echo "Timed out; resume polling job $CLIP_JOB_ID. The server job continues." >&2 exit 1 fi RETRY_AFTER=$(awk 'tolower($1)=="retry-after:" {gsub("\r", "", $2); print $2}' "$CERUL_TMP/job-headers") case "$RETRY_AFTER" in ''|*[!0-9]*) RETRY_AFTER=3 ;; esac sleep "$RETRY_AFTER" done ARTIFACTS=$(cerul "https://api.cerul.ai/jobs/$CLIP_JOB_ID/artifacts") ARTIFACT_ID=$(printf '%s' "$ARTIFACTS" | jq -er '.data[0].id') cerul "https://api.cerul.ai/artifacts/$ARTIFACT_ID" | jq '.data' cerul "https://api.cerul.ai/artifacts/$ARTIFACT_ID/content" --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, clip export and resource management." }, "servers": [ { "url": "https://api.cerul.ai", "description": "Cloud runtime" } ], "security": [ { "bearerAuth": [] } ], "tags": [ { "name": "Libraries", "description": "Content collection and membership" }, { "name": "Videos", "description": "Video ingestion and lifecycle" }, { "name": "Search", "description": "Scoped retrieval and evidence" }, { "name": "Jobs", "description": "Asynchronous capability and workflow execution" }, { "name": "Artifacts", "description": "Structured, renderable outputs and human review" }, { "name": "Deletions", "description": "Asynchronous workspace-scoped cloud content erasure" } ], "paths": { "/libraries": { "get": { "operationId": "listLibraries", "x-cerul-required-scopes": [ "libraries:read" ], "summary": "List libraries visible to the caller", "tags": [ "Libraries" ], "responses": { "200": { "description": "Libraries", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LibraryListResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } }, "post": { "operationId": "createLibrary", "x-cerul-required-scopes": [ "libraries:write" ], "summary": "Create a library", "tags": [ "Libraries" ], "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateLibraryRequest" } } } }, "responses": { "201": { "description": "Library created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LibraryResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/libraries/{library_id}/items": { "post": { "operationId": "addLibraryItem", "x-cerul-required-scopes": [ "library-items:write" ], "summary": "Add an asset to a library", "tags": [ "Libraries" ], "parameters": [ { "$ref": "#/components/parameters/LibraryId" }, { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AddLibraryItemRequest" } } } }, "responses": { "202": { "description": "Library item accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LibraryItemResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/videos": { "get": { "operationId": "listVideos", "x-cerul-required-scopes": [ "assets:read" ], "summary": "List videos", "description": "Lists videos in the caller's workspace, optionally restricted to one owned library, including its unfinished uploads. Empty libraries return an empty list; missing or foreign libraries return not_found. Per-video `data[].coverage` is authoritative. This collection does not aggregate coverage failures into a top-level warning without video identity.", "tags": [ "Videos" ], "parameters": [ { "name": "library_id", "in": "query", "required": false, "schema": { "$ref": "#/components/schemas/LibraryId" } } ], "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": [ "assets: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": [ "assets:write", "library-items:write" ], "summary": "Complete a video upload", "description": "Confirms the uploaded bytes, files the video in a library, and starts\nspeech and visual evidence indexing unless `index` is false. OCR is\ndisabled by default; opt in with processing.ocr.enabled and optionally\nprocessing.ocr.interval_seconds (default 10). Indexing additionally\nrequires jobs:write, assets:read and artifacts:write, a verified email\nand sufficient prepaid credits. Supply the actual duration and SHA-256.\nPoll GET /videos/{video_id} for ready, then inspect per-modality coverage;\nready does not guarantee every modality 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", "assets:read", "library-items:write", "artifacts:write" ], "summary": "Index a video", "description": "Starts speech and keyframe evidence indexing for a video completed with index false. OCR is disabled unless processing.ocr.enabled is true. Requires verified email and sufficient prepaid credits. execution_policy defaults to cloud_required. 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": [ "artifacts: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" } ], "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": [ "assets: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": [ "assets: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" } } } }, "/clips": { "post": { "operationId": "createClipExport", "summary": "Create an asynchronous cloud clip export from a bounded time range", "description": "The resulting clip_gallery Artifact exposes authenticated content at its content_url. In addition to jobs:write, authorization follows the media.clip@1 required_scopes in the canonical capability registry.", "tags": [ "Videos" ], "x-cerul-required-scopes": [ "jobs:write", "assets:read", "artifacts:write" ], "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClipExportRequest" } } } }, "responses": { "202": { "description": "Clip export job accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/search": { "post": { "operationId": "searchEvidence", "x-cerul-required-scopes": [ "libraries:read", "assets:read" ], "summary": "Search scoped libraries and assets for evidence", "description": "Cloud requests require verified email and sufficient prepaid credits. Omit filter to search the current workspace, or restrict by filter.video_ids or filter.library_ids. Scores rank candidate evidence; they are not correctness probabilities. A completed Cloud search with no matches may still be billable; a search with no indexed evidence is not billable. 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" } } } }, "/jobs/{job_id}/artifacts": { "get": { "operationId": "listJobArtifacts", "x-cerul-required-scopes": [ "jobs:read", "artifacts:read" ], "summary": "List artifacts created by a job", "tags": [ "Artifacts" ], "parameters": [ { "$ref": "#/components/parameters/JobId" } ], "responses": { "200": { "description": "Job artifacts", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtifactListResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } } }, "/artifacts/{artifact_id}": { "get": { "operationId": "getArtifact", "x-cerul-required-scopes": [ "artifacts:read" ], "summary": "Get a structured artifact", "tags": [ "Artifacts" ], "parameters": [ { "$ref": "#/components/parameters/ArtifactId" } ], "responses": { "200": { "description": "Artifact", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ArtifactResponse" } } } }, "default": { "$ref": "#/components/responses/Error" } } }, "delete": { "operationId": "deleteArtifact", "summary": "Delete an artifact and reclaim generated-object storage", "description": "Accepts an idempotent asynchronous erasure of the Artifact, its\nreviews, exclusive generated content, and retained references. The\nArtifact becomes unavailable immediately after safety preflight. A\nfailed preflight has no tombstone or cancellation side effect. Its\nsource Asset and unrelated sibling Artifacts are not deleted.\nContent-free billing and security audit records may be retained.\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": [ "Artifacts" ], "x-cerul-required-scopes": [ "artifacts:write" ], "parameters": [ { "$ref": "#/components/parameters/ArtifactId" }, { "$ref": "#/components/parameters/IdempotencyKey" } ], "responses": { "202": { "description": "Artifact 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" } } } }, "/deletions/{deletion_id}": { "get": { "operationId": "getResourceDeletion", "x-cerul-required-scopes": [], "summary": "Get asynchronous cloud deletion state", "description": "Requires assets:write for an Asset deletion and artifacts:write for an Artifact 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": [ "assets:read" ], "summary": "Stream authenticated source video", "tags": [ "Videos" ], "description": "Requires assets: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" } } } }, "/artifacts/{artifact_id}/content": { "get": { "operationId": "getArtifactContent", "x-cerul-required-scopes": [ "artifacts:read" ], "summary": "Stream authenticated content for a cloud Artifact", "tags": [ "Artifacts" ], "parameters": [ { "$ref": "#/components/parameters/ArtifactId" } ], "responses": { "200": { "description": "Private Artifact content", "headers": { "Cache-Control": { "schema": { "type": "string" } } }, "content": { "application/x-ndjson": { "schema": { "type": "string" } }, "text/html": { "schema": { "type": "string" } }, "video/mp4": { "schema": { "type": "string", "format": "binary" } } } }, "default": { "$ref": "#/components/responses/Error" } } } } }, "components": { "schemas": { "LibraryListResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Library" } } } } ] }, "ResponseMetadata": { "type": "object", "required": [ "request_id", "execution", "usage", "warnings" ], "properties": { "request_id": { "$ref": "#/components/schemas/RequestId" }, "execution": { "$ref": "#/components/schemas/Execution" }, "usage": { "$ref": "#/components/schemas/Usage" }, "warnings": { "type": "array", "items": { "type": "string" } } } }, "RequestId": { "type": "string", "pattern": "^req_[A-Za-z0-9]+$" }, "Execution": { "type": "object", "additionalProperties": false, "required": [ "location" ], "properties": { "location": { "$ref": "#/components/schemas/RuntimeLocation" }, "capability_id": { "type": [ "string", "null" ] }, "capability_version": { "type": [ "string", "null" ] } } }, "RuntimeLocation": { "type": "string", "enum": [ "local", "cloud" ] }, "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" ] } } }, "Library": { "type": "object", "additionalProperties": false, "required": [ "id", "name", "ingestion_profile", "created_at", "updated_at" ], "properties": { "id": { "$ref": "#/components/schemas/LibraryId" }, "workspace_id": { "oneOf": [ { "$ref": "#/components/schemas/WorkspaceId" }, { "type": "null" } ] }, "name": { "type": "string", "minLength": 1 }, "ingestion_profile": { "$ref": "#/components/schemas/IngestionProfile" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "LibraryId": { "type": "string", "pattern": "^lib_[A-Za-z0-9_-]+$" }, "WorkspaceId": { "type": "string", "pattern": "^ws_[A-Za-z0-9_-]+$" }, "IngestionProfile": { "type": "object", "additionalProperties": false, "required": [ "id", "version", "required_capabilities", "enrichment_schema", "segmentation_policy", "metadata_schema" ], "properties": { "id": { "type": "string" }, "version": { "type": "string" }, "required_capabilities": { "type": "array", "items": { "type": "string" } }, "enrichment_schema": { "type": "object", "additionalProperties": true }, "segmentation_policy": { "type": "object", "additionalProperties": true }, "metadata_schema": { "type": "object", "additionalProperties": true } } }, "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 } } }, "CreateLibraryRequest": { "type": "object", "additionalProperties": false, "required": [ "name", "ingestion_profile" ], "properties": { "name": { "type": "string", "minLength": 1 }, "ingestion_profile": { "$ref": "#/components/schemas/IngestionProfile" } } }, "LibraryResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Library" } } } ] }, "AddLibraryItemRequest": { "type": "object", "additionalProperties": false, "required": [ "asset_id" ], "properties": { "asset_id": { "$ref": "#/components/schemas/AssetId" } } }, "AssetId": { "type": "string", "pattern": "^asset_[A-Za-z0-9_-]+$" }, "LibraryItemResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/LibraryItem" } } } ] }, "LibraryItem": { "type": "object", "additionalProperties": false, "required": [ "id", "library_id", "asset_id", "enrichment", "created_at", "updated_at" ], "properties": { "id": { "$ref": "#/components/schemas/LibraryItemId" }, "library_id": { "$ref": "#/components/schemas/LibraryId" }, "asset_id": { "$ref": "#/components/schemas/AssetId" }, "enrichment": { "type": "object", "additionalProperties": true }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "LibraryItemId": { "type": "string", "pattern": "^li_[A-Za-z0-9_-]+$" }, "VideoListResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } } } } ] }, "Video": { "type": "object", "additionalProperties": false, "required": [ "id", "status", "coverage", "filename", "media_type", "byte_size", "duration_seconds", "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 searchable.", "type": "string", "enum": [ "uploading", "uploaded", "processing", "ready", "failed" ] }, "coverage": { "description": "Per-modality result for the retained index generation; null before a valid generation exists.", "oneOf": [ { "$ref": "#/components/schemas/VideoCoverage" }, { "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 }, "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" }, "annotate_job_id": { "type": [ "string", "null" ], "description": "Latest annotation job for this uploaded video." }, "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_-]+$" }, "VideoCoverage": { "type": "object", "additionalProperties": false, "required": [ "speech", "visual", "ocr" ], "properties": { "speech": { "$ref": "#/components/schemas/VideoModalityCoverage" }, "visual": { "$ref": "#/components/schemas/VideoModalityCoverage" }, "ocr": { "$ref": "#/components/schemas/VideoModalityCoverage" } } }, "VideoModalityCoverage": { "type": "object", "additionalProperties": false, "required": [ "status", "indexed_count", "reason" ], "properties": { "status": { "type": "string", "enum": [ "indexed", "absent", "failed" ] }, "indexed_count": { "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 } } }, "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 } } } } } }, "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 }, "library_id": { "description": "Omit to file the video in the workspace default library.", "$ref": "#/components/schemas/LibraryId" }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } }, "ExecutionPolicy": { "type": "string", "enum": [ "local_only", "prefer_local", "cloud_allowed", "cloud_required" ] }, "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 }, "library_id": { "description": "Omit to file the video in the workspace default library.", "$ref": "#/components/schemas/LibraryId" }, "data_egress_confirmed": { "description": "Desktop replication only. Must be true when source_installation_id is present, so a copy of local content always follows one explicit user action.", "type": "boolean", "const": true }, "source_installation_id": { "type": "string", "minLength": 1, "maxLength": 200 }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } }, "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": { "oneOf": [ { "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 }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } }, { "type": "object", "additionalProperties": false, "required": [ "index", "source_installation_id", "local_asset_id", "data_egress_confirmed" ], "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 }, "source_installation_id": { "description": "Desktop replication only. Present with local_asset_id and data_egress_confirmed, this records the AssetReplicaLink for a copy the user explicitly confirmed.", "type": "string", "minLength": 1, "maxLength": 200 }, "local_asset_id": { "type": "string", "minLength": 1, "maxLength": 200 }, "content_identity": { "description": "Sent only after the user confirms the copy, and only as sha256:<64 hex>.", "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, "data_egress_confirmed": { "description": "Must be true to replicate local content to the cloud. A single explicit user action.", "type": "boolean", "const": true }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } } ] }, "CompleteAndIndexVideoRequest": { "oneOf": [ { "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" }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } }, { "type": "object", "additionalProperties": false, "required": [ "duration_seconds", "content_sha256", "source_installation_id", "local_asset_id", "data_egress_confirmed" ], "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" }, "source_installation_id": { "description": "Desktop replication only. Present with local_asset_id and data_egress_confirmed, this records the AssetReplicaLink for a copy the user explicitly confirmed.", "type": "string", "minLength": 1, "maxLength": 200 }, "local_asset_id": { "type": "string", "minLength": 1, "maxLength": 200 }, "content_identity": { "description": "Sent only after the user confirms the copy, and only as sha256:<64 hex>.", "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, "data_egress_confirmed": { "description": "Must be true to replicate local content to the cloud. A single explicit user action.", "type": "boolean", "const": true }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } } ] }, "VideoProcessingOptions": { "type": "object", "additionalProperties": false, "properties": { "ocr": { "type": "object", "additionalProperties": false, "properties": { "enabled": { "description": "Extract visible text as independently sampled evidence. Defaults to false.", "type": "boolean", "default": false }, "interval_seconds": { "description": "OCR 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, "required": [ "duration_seconds", "content_sha256" ], "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}$" }, "library_id": { "$ref": "#/components/schemas/LibraryId" }, "processing": { "$ref": "#/components/schemas/VideoProcessingOptions" }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } }, "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", "execution_policy", "scope", "created_at", "updated_at" ], "properties": { "id": { "$ref": "#/components/schemas/JobId" }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed", "cancel_requested", "canceled" ] }, "capability_id": { "type": [ "string", "null" ] }, "capability_version": { "type": [ "string", "null" ] }, "progress": { "description": "Annotation stage progress. Failed media segments are reported in the validation artifact and coverage manifest.", "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." }, "workflow": { "oneOf": [ { "$ref": "#/components/schemas/WorkflowReference" }, { "type": "null" } ] }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" }, "scope": { "$ref": "#/components/schemas/Scope" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "JobId": { "type": "string", "pattern": "^job_[A-Za-z0-9_-]+$" }, "WorkflowReference": { "type": "object", "additionalProperties": false, "required": [ "id", "version" ], "properties": { "id": { "type": "string" }, "version": { "type": "string" } } }, "Scope": { "type": "object", "additionalProperties": false, "required": [ "library_ids", "asset_ids" ], "properties": { "library_ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/components/schemas/LibraryId" } }, "asset_ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/components/schemas/AssetId" } } } }, "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": [ "start_s", "end_s", "text" ], "properties": { "start_s": { "type": [ "number", "null" ], "minimum": 0 }, "end_s": { "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": [ "asset", "artifact" ] }, "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_-]+$" }, "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" } } }, "ClipExportRequest": { "type": "object", "additionalProperties": false, "required": [ "video_id", "start_s", "end_s" ], "properties": { "video_id": { "$ref": "#/components/schemas/VideoId" }, "start_s": { "type": "number", "minimum": 0 }, "end_s": { "type": "number", "exclusiveMinimum": 0 }, "evidence_ids": { "type": "array", "maxItems": 100, "uniqueItems": true, "items": { "$ref": "#/components/schemas/EvidenceId" } }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" } } }, "EvidenceId": { "type": "string", "pattern": "^ev_[A-Za-z0-9_-]+$" }, "SearchRequest": { "type": "object", "additionalProperties": false, "required": [ "query" ], "properties": { "query": { "type": "string", "minLength": 1, "maxLength": 1000 }, "filter": { "description": "Omit to search the whole workspace.", "$ref": "#/components/schemas/SearchFilter" }, "execution_policy": { "$ref": "#/components/schemas/ExecutionPolicy" }, "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } } }, "SearchFilter": { "type": "object", "additionalProperties": false, "properties": { "video_ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/components/schemas/VideoId" } }, "library_ids": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/components/schemas/LibraryId" } } } }, "SearchResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/SearchResult" } } } } ] }, "SearchResult": { "type": "object", "additionalProperties": false, "required": [ "evidence", "score" ], "properties": { "evidence": { "$ref": "#/components/schemas/Evidence" }, "score": { "type": "number", "minimum": 0, "maximum": 1 } } }, "Evidence": { "type": "object", "additionalProperties": false, "required": [ "id", "asset_id", "kind", "start_seconds", "end_seconds", "quote", "locators" ], "properties": { "id": { "$ref": "#/components/schemas/EvidenceId" }, "asset_id": { "$ref": "#/components/schemas/AssetId" }, "kind": { "type": "string", "enum": [ "transcript", "frame", "video_clip", "segment" ] }, "start_seconds": { "type": "number", "minimum": 0 }, "end_seconds": { "type": "number", "minimum": 0 }, "quote": { "type": "string" }, "modality": { "description": "Processing station that produced this evidence unit.", "type": "string", "enum": [ "speech", "ocr", "keyframe" ] }, "payload": { "description": "Bounded text or a stable source-frame identity. Private perception-archive coordinates are never exposed.", "$ref": "#/components/schemas/EvidencePayload" }, "provenance": { "description": "Station, model, version, and segment identity used to produce the unit.", "$ref": "#/components/schemas/EvidenceProvenance" }, "language": { "type": [ "string", "null" ] }, "locators": { "type": "array", "items": { "$ref": "#/components/schemas/EvidenceLocator" } } } }, "EvidencePayload": { "oneOf": [ { "$ref": "#/components/schemas/EvidenceTextPayload" }, { "$ref": "#/components/schemas/EvidenceImagePayload" } ] }, "EvidenceTextPayload": { "type": "object", "additionalProperties": false, "required": [ "text" ], "properties": { "text": { "type": "string", "minLength": 1, "maxLength": 4000 } } }, "EvidenceImagePayload": { "type": "object", "additionalProperties": false, "required": [ "image_ref" ], "properties": { "image_ref": { "$ref": "#/components/schemas/EvidenceImageReference" } } }, "EvidenceImageReference": { "type": "object", "additionalProperties": false, "required": [ "source_asset_id", "timestamp_seconds", "content_sha256" ], "properties": { "source_asset_id": { "$ref": "#/components/schemas/AssetId" }, "timestamp_seconds": { "type": "number", "minimum": 0 }, "content_sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } } }, "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 } } }, "EvidenceLocator": { "type": "object", "additionalProperties": false, "required": [ "type", "url" ], "properties": { "type": { "type": "string", "enum": [ "local", "cloud" ] }, "url": { "type": "string", "format": "uri" } } }, "ArtifactListResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Artifact" } } } } ] }, "Artifact": { "type": "object", "additionalProperties": false, "required": [ "id", "kind", "schema_version", "evidence_ids", "payload", "created_at" ], "properties": { "id": { "$ref": "#/components/schemas/ArtifactId" }, "kind": { "type": "string", "enum": [ "clip_gallery", "annotation_set", "annotation_layer", "validation_report", "comparison", "timeline", "highlight_reel", "storyboard", "dataset_manifest", "review_queue" ] }, "schema_version": { "type": "string" }, "job_id": { "oneOf": [ { "$ref": "#/components/schemas/JobId" }, { "type": "null" } ] }, "response_id": { "oneOf": [ { "$ref": "#/components/schemas/ResponseId" }, { "type": "null" } ] }, "evidence_ids": { "type": "array", "items": { "$ref": "#/components/schemas/EvidenceId" } }, "payload": { "type": "object", "additionalProperties": true }, "content_url": { "type": "string", "format": "uri-reference" }, "created_at": { "type": "string", "format": "date-time" } } }, "ResponseId": { "type": "string", "pattern": "^resp_[A-Za-z0-9_-]+$" }, "ArtifactResponse": { "unevaluatedProperties": false, "allOf": [ { "$ref": "#/components/schemas/ResponseMetadata" }, { "type": "object", "required": [ "data" ], "properties": { "data": { "$ref": "#/components/schemas/Artifact" } } } ] } }, "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 } }, "LibraryId": { "name": "library_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/LibraryId" } }, "VideoId": { "name": "video_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/VideoId" } }, "JobId": { "name": "job_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/JobId" } }, "ArtifactId": { "name": "artifact_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/ArtifactId" } }, "DeletionId": { "name": "deletion_id", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/DeletionId" } } }, "securitySchemes": { "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "Cerul access token" } } } } ```