{"openapi":"3.1.0","info":{"title":"Playhead API","version":"0.2.0","summary":"Let software watch a video.","description":"Playhead turns a video into contact sheets — grids of frames in reading order, with the timestamp burned into the pixels of every frame — plus a transcript with word-level timings and measured cuts, freezes, black frames and silences.\n\nIt exists because a language model cannot decode a video file. Asked about one it answers from the title, the description or the transcript, none of which contain what is on screen. These endpoints return images a model can actually look at, stamped so it cannot lose track of *when* it is looking.\n\n### Choosing an endpoint\n\n- The question is about what is **shown** → `POST /v1/frames`.\n- The question is about what is **said** → `POST /v1/transcript`. It returns no images, so it costs no vision tokens and is far cheaper.\n- The file is **private, geo-blocked or local** → `POST /v1/uploads` first, then pass the returned id.\n\n### Detail levels\n\nLooking closer means *fewer frames per sheet*, never a larger image — models downscale anything past 1568 px on the long edge, so a bigger sheet costs more and reads no better.\n\n- `overview` — survey a long video in full. 0.5–2 fps (adaptive), 30 frames per sheet, no window limit, 1 credit/min, at most 14 sheet(s) per call.\n- `standard` — what happens when, cuts, overlays appearing. 5 fps, 30 frames per sheet, no window limit, 2 credits/min, at most 12 sheet(s) per call.\n- `detail` — overlays, objects, rough text blocks. 5 fps, 6 frames per sheet, windows up to 30s, 4 credits/min, at most 10 sheet(s) per call.\n- `fine` — small details, type, logos, subtle transitions. 10 fps, 2 frames per sheet, windows up to 10s, 2 credits/sec, at most 12 sheet(s) per call.\n- `motion` — animations, motion paths, easing curves. 30 fps, 2 frames per sheet, windows up to 3s, 3 credits/sec, at most 24 sheet(s) per call.\n- `single` — maximum detail on one moment. 1 fps, 1 frames per sheet, windows up to 0.2s, 1 credit flat, at most 1 sheet(s) per call.\n\nOmit `detail` and `auto` picks a level from the window length. Omit `start` and `end` and the response covers the **complete** video.\n\n### Coverage\n\nEvery frames response carries a `coverage` object stating what was covered, whether that was the whole video, and every second that was not. Nothing is silently left out — if a level's window cap truncated the request, `coverage.truncated` names where to resume.\n\n### Plans\n\n- `free` — $0/mo, 60 credits, no `motion` level\n- `creator` — $19/mo, 600 credits\n- `pro` — $39/mo, 2000 credits\n- `studio` — $109/mo, 7500 credits\n- `enterprise` — $0/mo, 30000 credits\n\nOne credit is roughly one minute of video at `standard`. `fine` and `motion` are billed per second because they extract many times the frames, so a three-second window costs three seconds' worth. Asking again about a window already paid for costs 20% of the original.","termsOfService":"https://tryplayhead.com/terms","contact":{"name":"Playhead","url":"https://tryplayhead.com/docs"},"license":{"name":"Proprietary","url":"https://tryplayhead.com/terms"}},"servers":[{"url":"https://api.tryplayhead.com","description":"Production"}],"externalDocs":{"description":"Documentation","url":"https://tryplayhead.com/docs"},"security":[{"bearerAuth":[]}],"tags":[{"name":"Video","description":"Turning a video into something readable"},{"name":"Uploads","description":"Your own footage, without publishing it"},{"name":"Jobs","description":"Queued work"},{"name":"Account","description":"Credits, plan and discovery"}],"paths":{"/v1/frames":{"post":{"operationId":"createFrames","tags":["Video"],"summary":"Turn a video into contact sheets","description":"The endpoint everything else is built on. Returns contact sheets — grids of frames in reading order with the timestamp burned into each one — plus the exact time of every frame, what was covered, the estimated vision-token cost, and the cuts, freezes, black frames and silences the detectors measured.\n\nOmit `start` and `end` to cover the whole video. With `sync: true` (the default) the work happens inside this request, which for a long video can take minutes; pass `sync: false` to get a `job_id` back immediately and poll `GET /v1/jobs/{jobId}`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FramesRequest"},"examples":{"wholeVideo":{"summary":"The complete video, level picked automatically","value":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}},"animationDetail":{"summary":"Three seconds at 30 fps, frames 33 ms apart","value":{"url":"https://www.tiktok.com/@user/video/7300000000000000000","detail":"motion","start":4,"end":7}},"longVideoQueued":{"summary":"An hour-long recording, surveyed asynchronously","value":{"url":"https://vimeo.com/76979871","detail":"overview","sync":false}}}}}},"responses":{"200":{"description":"The sheets, when `sync` was true — or the queued job, when it was false.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/FramesResult"},{"$ref":"#/components/schemas/QueuedJob"}]}}}},"400":{"description":"The request cannot be run as written. `window_too_long_for_detail` carries a `suggestion` object holding the exact call that would work.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Out of credits. Buying more is a different action, not a retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated, but this credential or plan may not do this.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The video could not be fetched — private, geo-blocked, deleted, or behind a login. Upload the file instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited, or too many jobs at once for this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/transcript":{"post":{"operationId":"createTranscript","tags":["Video"],"summary":"Get the words, with the times they were said","description":"Text only — no images, so **no vision tokens at all**. This is the call to make whenever the question is about what is said rather than what is shown; it is the cheapest endpoint in the API by a wide margin.\n\nThe uploader's own captions are preferred when they exist, because a human typed the names; otherwise the audio is transcribed. The response says which of the two happened.\n\nSet `language` when the audio is mostly music, crowd or engine noise: detection misfires there and a wrong language returns fluent nonsense. Every response also reports whether the audio was speech at all, so music scored over a video is not handed back as dialogue.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptRequest"},"examples":{"plain":{"summary":"Everything that was said","value":{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}},"wordTimings":{"summary":"Word-level timings, for aligning to frames","value":{"url":"https://vimeo.com/76979871","words":true}},"subtitles":{"summary":"An SRT file","value":{"url":"upl_01HZY6M8Q2","format":"srt"}}}}}},"responses":{"200":{"description":"The transcript, or the queued job.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TranscriptResult"},{"$ref":"#/components/schemas/QueuedJob"}]}}}},"400":{"description":"The request cannot be run as written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Out of credits.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The video could not be fetched.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/uploads":{"post":{"operationId":"createUpload","tags":["Uploads"],"summary":"Send your own file","description":"For footage that is private, geo-blocked, or never was on a platform at all. Returns an `upl_…` id that is a valid `url` for every later call about the same video.\n\nEditing and delivery containers work, not just web video: mp4, mov (including ProRes), mkv, webm, avi, mxf (XAVC, XDCAM, AVC-Intra, DNxHD), gxf, lxf, ts and dv. Camera raw — .r3d, .braw, .ari, .crm — needs the manufacturer's own decoder and is refused by name, with the export to make instead.\n\nWhen the file carries SMPTE timecode, every tile is stamped with it alongside the elapsed time, and `video.start_timecode` reports where the source's own clock starts.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"The upload id and what was probed from the file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResult"}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"ffprobe cannot read the file as media.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Upload rate limited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/uploads/tickets":{"post":{"operationId":"createUploadTicket","tags":["Uploads"],"summary":"Get a signed upload URL that needs no key","description":"Returns a short-lived, signed URL that anybody holding the bytes can POST to without a credential of their own. This is how an agent running on a machine the API cannot reach sends a local file up: the ticket carries the authorization, so no key ever leaves your server.","responses":{"200":{"description":"The ticket and where to send the bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadTicket"}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/jobs/{jobId}":{"get":{"operationId":"getJob","tags":["Jobs"],"summary":"Status and result of a queued job","description":"Poll this after a call made with `sync: false`. `status` moves `queued` → `running` → `succeeded` or `failed`; the `result` field appears only on success, and holds exactly what the synchronous call would have returned. A failure carries the same actionable error body a synchronous call would have thrown.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"example":"job_01HZY6M8Q2"}],"responses":{"200":{"description":"The job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such job, or it belongs to another organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/credits":{"get":{"operationId":"getCredits","tags":["Account"],"summary":"Balance, plan and ledger","description":"Two balances, and they behave nothing alike: `credits_balance` is the monthly grant and resets on `credits_renew_at`; `topup_balance` is bought credit and never resets. `wallet` says which of the two *this* credential spends.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":200,"default":20},"description":"How many ledger entries to return."}],"responses":{"200":{"description":"The balances, the plan's limits, and recent ledger entries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsResult"}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/detail-levels":{"get":{"operationId":"getDetailLevels","tags":["Account"],"summary":"The levels and what they cost","description":"The six levels with their frame rates, grids, window caps and prices — read straight from the engine, so a client built against this cannot drift from what the server enforces. Needs no authentication.","security":[],"responses":{"200":{"description":"Levels and plans.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DetailLevelsResult"}}}}}}},"/v1/videos/{videoId}":{"delete":{"operationId":"deleteVideo","tags":["Video"],"summary":"Erase a video and everything derived from it","description":"Hard deletion: stored sheets, the transcript, the cached source file and every derived artifact. This is the GDPR erasure path, so it does not tombstone — after it returns, the data is gone and a later call about the same URL is a fresh, chargeable run.","parameters":[{"name":"videoId","in":"path","required":true,"schema":{"type":"string"},"example":"vid_01HZY6M8Q2"}],"responses":{"200":{"description":"What was removed.","content":{"application/json":{"schema":{"type":"object","properties":{"video_id":{"type":"string"},"deleted":{"type":"boolean"}}}}}},"401":{"description":"Missing, malformed or expired credential.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such video.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/healthz":{"get":{"operationId":"healthCheck","tags":["Account"],"summary":"Liveness, version and toolchain","security":[],"responses":{"200":{"description":"Service status.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"version":{"type":"string"},"ffmpeg":{"type":"boolean"},"yt_dlp":{"type":"boolean"}}}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key from https://tryplayhead.com/dashboard/keys, sent as `Authorization: Bearer sk_live_…`. Keys are shown once and stored only as a hash, so a lost key is replaced rather than recovered. OAuth 2.1 access tokens are accepted on the same header — see `/.well-known/oauth-authorization-server`."}},"schemas":{"DetailLevel":{"type":"string","enum":["auto","overview","standard","detail","fine","motion","single"],"default":"auto","description":"How closely to look.\n\n- `overview` — survey a long video in full. 0.5–2 fps (adaptive), 30 frames per sheet, no window limit, 1 credit/min, at most 14 sheet(s) per call.\n- `standard` — what happens when, cuts, overlays appearing. 5 fps, 30 frames per sheet, no window limit, 2 credits/min, at most 12 sheet(s) per call.\n- `detail` — overlays, objects, rough text blocks. 5 fps, 6 frames per sheet, windows up to 30s, 4 credits/min, at most 10 sheet(s) per call.\n- `fine` — small details, type, logos, subtle transitions. 10 fps, 2 frames per sheet, windows up to 10s, 2 credits/sec, at most 12 sheet(s) per call.\n- `motion` — animations, motion paths, easing curves. 30 fps, 2 frames per sheet, windows up to 3s, 3 credits/sec, at most 24 sheet(s) per call.\n- `single` — maximum detail on one moment. 1 fps, 1 frames per sheet, windows up to 0.2s, 1 credit flat, at most 1 sheet(s) per call."},"Timestamp":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`)."},"FramesRequest":{"type":"object","description":"Exactly one of `url` or `upload_id` is required.","properties":{"url":{"type":"string","description":"A link to the video. YouTube, TikTok, Instagram, X, Facebook (including Meta Ad Library), Vimeo, Twitch, Dailymotion and Reddit are recognised by name; any other page or direct media URL is attempted generically. An `upl_…` id from POST /v1/uploads is also accepted here.","examples":["https://www.youtube.com/watch?v=dQw4w9WgXcQ","https://www.tiktok.com/@user/video/7300000000000000000","https://www.facebook.com/ads/library/?id=1234567890","upl_01HZY6M8Q2"]},"upload_id":{"type":"string","description":"The id returned by POST /v1/uploads. Use instead of `url`."},"detail":{"$ref":"#/components/schemas/DetailLevel"},"start":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Window start. Omit **both** `start` and `end` to cover the whole video."},"end":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Window end."},"transcribe":{"type":"boolean","default":true,"description":"Transcribe the audio alongside the frames."},"language":{"type":"string","default":"auto","description":"Force a language, e.g. `en`. Worth setting whenever the audio is mostly music or noise — detection misfires there, and naming the language also skips a model load."},"qa":{"type":"boolean","default":true,"description":"Run the cut, freeze, black-frame and silence detectors. Skipped automatically on windows longer than 15 minutes, in which case `technical.skipped` is true and the empty lists mean nothing."},"signals":{"oneOf":[{"type":"string","enum":["off","basic","full"]},{"type":"boolean"}],"default":"full","description":"How much of the between-frame pass to run. A sheet at 5 fps shows one frame in six, so a one-frame flash and a three-frame cut are invisible on it. This pass reads every frame at 32x32 grayscale and reports what happened between the tiles."},"overlap":{"type":"boolean","default":false,"description":"Overlap consecutive sheets by one frame."},"fps":{"type":"number","exclusiveMinimum":0,"maximum":120,"description":"Override the level's frame rate. For callers who know why."},"cols":{"type":"integer","minimum":1,"maximum":16},"rows":{"type":"integer","minimum":1,"maximum":16},"sheet_width":{"type":"integer","minimum":200,"maximum":1568},"return":{"type":"string","enum":["urls","base64"],"default":"urls","description":"`urls` returns signed, time-limited links; `base64` inlines the JPEG bytes, which is what an MCP client needs and what makes a response large."},"sync":{"type":"boolean","default":true,"description":"Run the work inside this request. `false` queues it and returns a `job_id` — the right choice for anything long."},"webhook_url":{"type":"string","format":"uri","description":"Called once when a queued job reaches a terminal state."},"idempotency_key":{"type":"string","description":"Replaying the same key returns the first result rather than charging twice."}},"anyOf":[{"required":["url"]},{"required":["upload_id"]}]},"TranscriptRequest":{"type":"object","description":"Exactly one of `url` or `upload_id` is required.","properties":{"url":{"type":"string","description":"A link to the video. YouTube, TikTok, Instagram, X, Facebook (including Meta Ad Library), Vimeo, Twitch, Dailymotion and Reddit are recognised by name; any other page or direct media URL is attempted generically. An `upl_…` id from POST /v1/uploads is also accepted here.","examples":["https://www.youtube.com/watch?v=dQw4w9WgXcQ","https://www.tiktok.com/@user/video/7300000000000000000","https://www.facebook.com/ads/library/?id=1234567890","upl_01HZY6M8Q2"]},"upload_id":{"type":"string","description":"The id returned by POST /v1/uploads. Use instead of `url`."},"language":{"type":"string","default":"auto"},"words":{"type":"boolean","default":false,"description":"Include per-word timings."},"start":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`)."},"end":{"oneOf":[{"type":"number"},{"type":"string"}],"description":"Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`)."},"format":{"type":"string","enum":["json","srt","vtt","text"],"default":"json"},"sync":{"type":"boolean","default":true},"webhook_url":{"type":"string","format":"uri"},"idempotency_key":{"type":"string"}},"anyOf":[{"required":["url"]},{"required":["upload_id"]}]},"Video":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string","description":"`youtube`, `tiktok`, `instagram`, `x`, `facebook`, `vimeo`, `reddit`, `twitch`, `dailymotion`, `web` for a generically extracted page, or `upload`."},"duration":{"type":"number","description":"Seconds."},"width":{"type":"integer"},"height":{"type":"integer"},"orientation":{"type":"string"},"src_fps":{"type":"number"},"title":{"type":["string","null"]},"language":{"type":["string","null"]},"content_box":{"type":["object","null"],"description":"The picture inside the frame when the source was letterboxed. Present means the tiles show this region, not the full frame.","properties":{"x":{"type":"integer"},"y":{"type":"integer"},"width":{"type":"integer"},"height":{"type":"integer"}}},"carousel":{"type":["object","null"],"description":"Present when the post is a photo carousel — N stills with no timeline. One second is one slide: second 0 is slide 1. The number is an index wearing the units the rest of the API speaks, not a duration.","properties":{"slides":{"type":"integer"},"audio_seconds":{"type":["number","null"]}}},"start_timecode":{"type":["string","null"],"description":"SMPTE timecode of the source's first frame, e.g. `14:36:27:07`. Present on camera and deck formats, absent on everything from the web. A different clock from the elapsed seconds everywhere else in this response — both are true, and neither should be converted into the other."},"interlaced":{"type":"boolean"},"audio_tracks":{"type":"integer","description":"Streams, not channels. A camera master routinely has eight."}}},"Sheet":{"type":"object","description":"One contact sheet: a grid of frames in reading order, each with its timestamp burned into the pixels above it.","properties":{"page":{"type":"integer","description":"1-based, in reading order."},"t0":{"type":"number","description":"Time of the first frame, in seconds."},"t1":{"type":"number","description":"Time of the last frame."},"n_frames":{"type":"integer"},"frame_times":{"type":"array","items":{"type":"number"},"description":"The exact second of every tile, left to right then top to bottom."},"width":{"type":"integer"},"height":{"type":"integer"},"bytes":{"type":"integer"},"estimated_vision_tokens":{"type":"integer"},"spoken":{"type":"string","description":"What was said across this sheet's window."},"qa_flags":{"type":"array","items":{"type":"string"},"description":"Anything the detectors flagged inside this sheet's window."},"url":{"type":"string","description":"Signed and time-limited. Present when `return` was `urls`."},"b64":{"type":"string","description":"JPEG bytes. Present when `return` was `base64`."}}},"Coverage":{"type":"object","description":"What this response actually covered. The field that makes an answer trustworthy: a model reading it cannot claim to have seen a video it only saw a third of.","properties":{"covered_seconds":{"type":"number"},"of_total":{"type":"number"},"is_full_video":{"type":"boolean","description":"True when this is second 0 to the end, nothing left out."},"gaps":{"type":"array","description":"Every second not covered.","items":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}}}},"truncated":{"type":"object","description":"Present when a level's window cap cut the request short. `next_start` is where to resume.","properties":{"requested_end":{"type":"number"},"next_start":{"type":"number"},"reason":{"type":"string"}}}}},"Technical":{"type":"object","description":"What a single decode measured. These are measurements, not judgements: `scene_cuts` are moments the picture changed past a threshold, which includes a fast camera move. That distinction travels with the number rather than being resolved here, because resolving it needs the frames — and the caller has them.","properties":{"scene_cuts":{"type":"array","items":{"type":"number"},"description":"Seconds at which the picture changed past the threshold."},"pacing":{"type":"object","properties":{"cuts":{"type":"integer"},"segments":{"type":"integer"},"median_segment_seconds":{"type":["number","null"]},"shortest_segment_seconds":{"type":["number","null"]},"longest_segment_seconds":{"type":["number","null"]},"cuts_per_minute":{"type":["number","null"]}}},"freezes":{"$ref":"#/components/schemas/SpanList"},"blacks":{"$ref":"#/components/schemas/SpanList"},"silence_gaps":{"$ref":"#/components/schemas/SpanList"},"skipped":{"type":"boolean","description":"True when the detectors never ran. Without this an empty list is ambiguous — a video with no cuts and a video nobody looked at produce identical output."}}},"SpanList":{"type":"array","items":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"},"duration":{"type":["number","null"]}}}},"Transcript":{"type":"object","properties":{"language":{"type":["string","null"]},"source":{"type":"string","enum":["captions","transcribed"],"description":"`captions` when the uploader wrote them, `transcribed` when we heard them. They differ in reliability and that is the caller's business."},"text":{"type":"string"},"segments":{"type":"array","items":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"},"text":{"type":"string"},"confidence":{"type":"number"},"words":{"type":"array","description":"Present when `words` was true.","items":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"},"word":{"type":"string"},"confidence":{"type":"number"}}}}}}},"speech":{"type":"object","description":"Whether the audio was speech at all. Music scored over a video transcribes into fluent sentences nobody said; this says so rather than handing back lyrics as dialogue.","properties":{"is_speech":{"type":"boolean"},"confidence":{"type":"number"},"reason":{"type":"string"}}},"low_confidence":{"type":"array","description":"Passages the model was unsure of. Settle these against the frames.","items":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"},"text":{"type":"string"},"confidence":{"type":"number"}}}}}},"FramesResult":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string"},"credits_charged":{"type":["number","null"]},"cache_hit":{"type":"boolean","description":"True when this window had already been paid for — billed at 20% of the original."},"video":{"$ref":"#/components/schemas/Video"},"detail":{"type":"string"},"fps":{"type":"number"},"fps_capped_to_source":{"type":"boolean","description":"True when the level's rate was above the source's own frame rate."},"window":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2},"coverage":{"$ref":"#/components/schemas/Coverage"},"geometry":{"type":"object","properties":{"grid":{"type":"array","items":{"type":"integer"}},"frames_per_sheet":{"type":"integer"},"tile":{"type":"array","items":{"type":"integer"}},"sheet":{"type":"array","items":{"type":"integer"}},"frame_interval_ms":{"type":["number","null"],"description":"Milliseconds between consecutive tiles. 33 at `motion`."}}},"total_frames":{"type":"integer"},"total_estimated_vision_tokens":{"type":"integer"},"sheets":{"type":"array","items":{"$ref":"#/components/schemas/Sheet"}},"transcript":{"oneOf":[{"$ref":"#/components/schemas/Transcript"},{"type":"null"}]},"technical":{"$ref":"#/components/schemas/Technical"},"signal":{"type":"object","description":"What happened between the frames on the sheets — one-frame inserts, flashes, overlays appearing, camera moves, and what the edit did (dissolves, wipes, whip-blurs). Absent when this call did not ask for it, which is not the same as nothing being found.","additionalProperties":true},"notes":{"type":"array","items":{"type":"string"},"description":"Anything the pipeline wants the caller to know, in words."}}},"TranscriptResult":{"allOf":[{"$ref":"#/components/schemas/Transcript"},{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string"},"credits_charged":{"type":["number","null"]},"cache_hit":{"type":"boolean"},"video":{"$ref":"#/components/schemas/Video"},"subtitles":{"type":"string","description":"Present when `format` was `srt` or `vtt`."}}}]},"UploadResult":{"type":"object","properties":{"upload_id":{"type":"string","description":"Pass this as `url` on any later call about the same video."},"video":{"$ref":"#/components/schemas/Video"},"size_bytes":{"type":"integer","description":"What is stored."},"received_bytes":{"type":"integer","description":"What was sent."}}},"UploadTicket":{"type":"object","properties":{"ticket":{"type":"string"},"upload_url":{"type":"string","format":"uri"},"expires_at":{"type":"string","format":"date-time"},"max_bytes":{"type":"integer"}}},"QueuedJob":{"type":"object","description":"What a call made with `sync: false` returns.","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["queued"]},"queue":{"type":"string"},"queued_at":{"type":"string","format":"date-time"}}},"Job":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["queued","running","succeeded","failed","cancelled"]},"operation":{"type":"string","enum":["frames","transcript"]},"detail":{"type":["string","null"]},"queue":{"type":"string"},"credits_charged":{"type":["number","null"]},"cache_hit":{"type":"boolean"},"queued_at":{"type":"string","format":"date-time"},"started_at":{"type":["string","null"],"format":"date-time"},"finished_at":{"type":["string","null"],"format":"date-time"},"error":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"type":"null"}]},"result":{"description":"Present only once `status` is `succeeded`.","oneOf":[{"$ref":"#/components/schemas/FramesResult"},{"$ref":"#/components/schemas/TranscriptResult"}]}}},"CreditsResult":{"type":"object","properties":{"org_id":{"type":"string"},"tier":{"type":"string"},"credits_balance":{"type":"number","description":"The monthly grant. Resets on `credits_renew_at`."},"credits_renew_at":{"type":["string","null"],"format":"date-time"},"topup_balance":{"type":"number","description":"Bought credit. Never resets."},"wallet":{"type":"string","enum":["plan","topup"],"description":"Which of the two pots this credential spends."},"plan":{"type":"object","properties":{"monthly_credits":{"type":"number"},"concurrency":{"type":"integer"},"retention_days":{"type":"integer"},"max_video_minutes":{"type":"integer"},"allows_motion":{"type":"boolean"}}},"ledger":{"type":"array","items":{"type":"object","properties":{"delta":{"type":"number"},"reason":{"type":"string"},"wallet":{"type":"string"},"job_id":{"type":["string","null"]},"balance_after":{"type":"number"},"at":{"type":"string","format":"date-time"}}}}}},"DetailLevelsResult":{"type":"object","properties":{"levels":{"type":"array","items":{"type":"object","properties":{"detail":{"type":"string"},"fps":{"description":"A number, or the adaptive ladder for `overview`.","oneOf":[{"type":"number"},{"type":"array","items":{"type":"array","items":{"type":"number"}}}]},"grid_16_9":{"type":"array","items":{"type":"integer"}},"grid_9_16":{"type":"array","items":{"type":"integer"}},"frames_per_sheet":{"type":"integer"},"max_window_seconds":{"type":["number","null"]},"on_exceed":{"type":"string","enum":["truncate","error"]},"credits":{"type":"object","properties":{"unit":{"type":"string","enum":["min","sec","flat"]},"rate":{"type":"number"}}},"max_sheets_per_call":{"type":"integer"},"purpose":{"type":"string"}}}},"plans":{"type":"object","additionalProperties":{"type":"object","properties":{"price_usd":{"type":"number"},"monthly_credits":{"type":"number"},"allows_motion":{"type":"boolean"}}}}}},"ApiError":{"type":"object","description":"Errors are written to be acted on, not just logged. Where a different call would have worked, `suggestion` holds it — ready to spread straight back into the request.","required":["type","message"],"properties":{"type":{"type":"string","description":"The machine-readable discriminator: `window_too_long_for_detail`, `source_unavailable`, `insufficient_credits`, `rate_limited`, `invalid_request`, `unauthorized`, `insufficient_scope`, `tier_limit`, `video_not_found`, `no_video_stream`, `processing_failed`."},"message":{"type":"string"},"suggestion":{"type":"object","description":"Arguments for a call that would work instead.","additionalProperties":true,"examples":[{"start":0,"end":3,"detail":"motion"}]},"alternative":{"type":"string","description":"A different approach, in prose, when no single call is the answer."},"max_window_seconds":{"type":"number"}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/ApiError"}}}}}}