{"openapi":"3.1.0","info":{"title":"TranscriptFetch API","version":"1.0.0","description":"Fetch video transcripts from YouTube, TikTok, and Instagram, plus podcasts (Spotify, Apple Podcasts, or an RSS feed) and direct media file URLs. Videos without a caption track are transcribed automatically. YouTube additionally resolves playlists, channels, and keyword searches into video lists. Successful (`ok`) caption fetches and video lists cost 1 credit each; audio transcription costs 1 credit per started minute of audio (minimum 1 credit). Requests that fail or return no transcript are free."},"servers":[{"url":"https://transcriptfetch.com","description":"Production"}],"externalDocs":{"description":"Documentation","url":"https://transcriptfetch.com/docs"},"security":[{"bearerAuth":[]}],"tags":[{"name":"Transcripts","description":"Transcript and video-list endpoints"},{"name":"System","description":"Service health and metadata"}],"paths":{"/api/v1/transcripts/video":{"post":{"tags":["Transcripts"],"summary":"Fetch a transcript (YouTube, TikTok, Instagram, podcast, or file URL)","description":"Returns a transcript - text plus timestamped segments. Accepts YouTube, TikTok, and Instagram URLs (or a bare TikTok video id), direct media file URLs, and podcast links (Spotify, Apple Podcasts, or an RSS feed), which resolve to the episode's audio automatically and come back with a podcast block naming the show and episode. Podcast transcriptions may include best-effort speaker diarization: when labels were produced, each segment carries a `speaker` integer (0, 1, ...) identifying who is talking and a top-level `diarized: true` marks the transcript. Speaker ids are hints from voice separation, not named identification, and non-podcast sources never carry them. When no captions exist the audio is transcribed automatically: when we can determine the media length, media under 20 minutes simply waits (the request is held open for up to 45 seconds) and returns the finished transcript, so no polling is needed; when the length cannot be determined, only short-form platforms (TikTok and Instagram) are held inline. Longer media, or a transcription still running when the 45-second hold expires, returns 202 with a job to poll instead - the work continues either way, so the same request is safe to retry and will hit the cache once it finishes. Supply callback_url to have the finished transcript POSTed to you instead of polling. Every failure carries an ai_fallback block saying whether captions were definitively unavailable and whether retrying would work.","operationId":"fetchVideoTranscript","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoRequest"},"examples":{"example":{"value":{"video":"dQw4w9WgXcQ"}}}}}},"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","data":{"kind":"transcript","video_id":"dQw4w9WgXcQ","platform":"youtube","title":"Example video","thumbnailUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg","segments":[{"start":0,"duration":3.5,"text":"We're no strangers to love"}]},"usage":{"credits_spent":1,"balance":99,"bytes":14233}}}}}}},"202":{"description":"Accepted - audio transcription started. Poll poll_url for the result, or supply callback_url to have it delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAcceptedEnvelope"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/transcripts/channel":{"post":{"tags":["Transcripts"],"summary":"List a channel's videos","description":"Resolve a channel into a paginated list of videos (metadata only), newest first. Pass since_video_id to get back only the uploads newer than one you've already seen - a page with nothing new is free, which makes this safe to poll on a schedule.","operationId":"fetchChannelVideos","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelRequest"},"examples":{"example":{"value":{"channel":"@lexfridman","limit":10}}}}}},"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","data":{"kind":"video_list","source":"channel_videos","videos":[{"videoId":"dQw4w9WgXcQ","title":"Example video","thumbnailUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg","duration":212,"channel":"Example Channel"}],"next_cursor":"eyJvIjoxMH0"},"usage":{"credits_spent":1,"balance":98,"bytes":0}}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/transcripts/playlist":{"post":{"tags":["Transcripts"],"summary":"List a playlist's videos","description":"Resolve a playlist into a paginated list of videos (metadata only).","operationId":"fetchPlaylistVideos","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaylistRequest"},"examples":{"example":{"value":{"playlist":"PLrAXtmRdnEQy6nuLMt9H1aZIuhcGOcZQ4","limit":10}}}}}},"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","data":{"kind":"video_list","source":"playlist","videos":[{"videoId":"dQw4w9WgXcQ","title":"Example video","thumbnailUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg","duration":212,"channel":"Example Channel"}],"next_cursor":"eyJvIjoxMH0"},"usage":{"credits_spent":1,"balance":98,"bytes":0}}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/transcripts/search":{"post":{"tags":["Transcripts"],"summary":"Search YouTube for videos","description":"Resolve a keyword search into a paginated list of videos (metadata only).","operationId":"searchVideos","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"},"examples":{"example":{"value":{"query":"how transformers work","limit":10}}}}}},"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","data":{"kind":"video_list","source":"search","videos":[{"videoId":"dQw4w9WgXcQ","title":"Example video","thumbnailUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg","duration":212,"channel":"Example Channel"}],"next_cursor":"eyJvIjoxMH0"},"usage":{"credits_spent":1,"balance":98,"bytes":0}}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/api/v1/transcripts/jobs/{jobId}":{"get":{"tags":["Transcripts"],"summary":"Poll an audio-transcription job","description":"Returns the state of a transcription job created when a request escalated to audio. 200 for any job your account owns (an unknown id, or a job belonging to someone else, is a 404): read the status field, which is \"processing\" while the job is running, \"completed\" with the transcript in data, or \"failed\" with an error. The job_id and a ready-to-use poll_url come back in the 202 that started it. Free to poll on our side - credits are charged once, on delivery (1 credit per started minute of audio, minimum 1). NOTE for RapidAPI subscribers: every poll is still a billable request on your Hub plan, so prefer callback_url, or simply re-send the original request once the job has had time to finish (the result is cached, so it returns instantly and the delivery is charged once, not twice). Most transcriptions never reach this endpoint at all: when we can determine the media length, media under 20 minutes is returned synchronously, and short-form platforms (TikTok and Instagram) are held inline even when we cannot.","operationId":"getTranscriptJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","status":"completed","job_id":"asr_m3k1x9qz4vb2p7","data":{"kind":"transcript","video_id":"dQw4w9WgXcQ","title":"Example video","text":"We're no strangers to love …","segments":[{"start":0,"duration":3.5,"text":"We're no strangers to love"}]},"usage":{"credits_spent":1,"balance":942,"bytes":18244}}}}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/transcripts/batch":{"post":{"tags":["Transcripts"],"summary":"Fetch up to 500 transcripts in one call (plan-dependent)","description":"Fetch a batch of transcripts concurrently. Batch size follows your plan: up to 50 entries on the free tier, Basic and Pro, and up to 500 on Mega and Scale (a request over your plan's cap fails whole with a clear error before anything is fetched or charged). Accepts the same inputs as /transcripts/video - YouTube, TikTok and Instagram URLs as well as bare YouTube IDs. Charges 1 credit per successfully fetched transcript; failed videos are free. Entries with no caption track are transcribed from audio by default: those come back with outcome \"processing\" and a job_id, cost nothing on this call, and are charged on delivery at the audio rate. Re-send the same batch once they have finished and the text is returned normally - polling is optional. Send mode: \"captions\" to keep the old behaviour and have captionless entries fail as no_transcript instead.","operationId":"fetchTranscriptsBatch","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"},"examples":{"example":{"value":{"video_ids":["dQw4w9WgXcQ","https://www.tiktok.com/@user/video/7137723462233555205"],"mode":"auto"}}}}}},"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","data":{"kind":"transcript_batch","results":[{"video_id":"dQw4w9WgXcQ","outcome":"ok","reason":null,"message":null,"title":"Example video","text":"Full transcript text …","segments":[{"start":0,"duration":3.5,"text":"Full transcript …"}],"bytes":14233},{"video_id":"9bZkp7q19f0","outcome":"processing","reason":null,"message":"No caption track; transcribing the audio. Re-request this video to collect the text, or poll poll_url.","job_id":"asr_…","poll_url":"/api/v1/transcripts/jobs/asr_…","title":null,"text":null,"segments":null,"bytes":0},{"video_id":"jNQXAC9IVRw","outcome":"no_transcript","reason":"no_captions","message":"No caption track (manual or auto-generated) is available. (Seen with mode: \"captions\"; the default transcribes instead.)","title":null,"text":null,"segments":null,"bytes":0}]},"usage":{"credits_spent":1,"balance":97}}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/me":{"get":{"tags":["System"],"summary":"Validate your key & check balance","description":"Validates the API key and returns the account's remaining credit balance. Free - never billed. Useful for programmatic balance checks and as a credential test in integrations.","operationId":"me","externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"example":{"value":{"ok":true,"request_id":"req_…","data":{"kind":"me","user_id":"user_…","credits":250},"usage":{"credits_spent":0,"balance":250,"bytes":0}}}}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/api/v1/health":{"get":{"tags":["System"],"summary":"Health check","description":"Public liveness probe for uptime monitoring. Returns 200 whenever the API is serving. No authentication required and no credits used.","operationId":"healthCheck","security":[],"externalDocs":{"description":"Full API reference: fields, response shape, and error codes","url":"https://transcriptfetch.com/docs/endpoints"},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"examples":{"example":{"value":{"status":"ok","service":"transcriptfetch-api","version":"1.0.0","time":"2026-06-16T00:00:00.000Z"}}}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Send your API key as `Authorization: Bearer `."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Unique key (≤255 chars) to safely retry a request without being charged twice. Replays return the original response with `Idempotent-Replayed: true`. Reusing a key with a different body is a 409 conflict.","schema":{"type":"string","maxLength":255}}},"schemas":{"VideoRequest":{"type":"object","required":["video"],"properties":{"video":{"type":"string","minLength":1,"maxLength":2000,"description":"A video URL or 11-character YouTube video ID. Accepts YouTube (watch, youtu.be, /shorts/), TikTok, and Instagram URLs, plus direct media file URLs (mp4/mp3/wav/…). Also accepts podcast links: a Spotify or Apple Podcasts episode URL, or a podcast RSS feed URL, which is resolved to that episode's audio automatically. Videos without captions fall back to AI transcription."},"mode":{"type":"string","enum":["captions","audio","auto"],"default":"auto","description":"Where the text may come from. \"captions\" reads an existing caption track and fails if there is none, which is the only way to avoid audio transcription. \"audio\" skips captions and transcribes the audio. \"auto\" (the default) tries captions first and transcribes the audio when there are none. Short media may finish inline after a wait of up to 45 seconds; longer work returns 202 with a job to poll or deliver by callback. Audio is charged only on delivery: 1 credit per started minute of audio, minimum 1 (a 20-minute video is 20; the 4-hour cap is 240). Caption fetches are always 1 credit."},"timestamps":{"default":true,"oneOf":[{"type":"boolean"},{"type":"string","enum":["none","segment"]}],"description":"Which form the transcript comes back in. true (the default) returns the `segments` array, each with start, duration and text (podcast episodes transcribed from audio may also carry a `speaker` id per segment - see the podcast notes). false returns a single joined `text` string instead. On the single-transcript 200 exactly one of the two is present, never both, since segments already contain every word the joined text does; job results and batch entries carry both text and segments. The older strings \"segment\" and \"none\" mean the same two things and are still accepted."},"callback_url":{"type":"string","format":"uri","maxLength":2000,"description":"Where to POST the finished transcript when a request escalates to audio transcription, instead of polling the job. The delivery body is a trimmed envelope - {ok, status, job_id, data} on success, {ok, status, job_id, error} on failure - without the usage and request_id the poll URL adds. When a signing secret is configured on our side, the body is signed with HMAC-SHA256 over the exact bytes and sent as an X-TranscriptFetch-Signature: sha256= header so you can verify it came from us. Must be a public https URL on the standard port; the URL is checked again at delivery time, so an unreachable or private address still gets a 202 but never receives a delivery. The job stays pollable either way, so a missed delivery is never a lost transcript."},"ai_fallback":{"type":"boolean","default":false,"deprecated":true,"description":"Legacy alias for \"mode\", still supported. true is identical to \"mode\": \"audio\"; omitted or false is \"mode\": \"auto\". Send one or the other, not both. Note that false never disabled the fallback - audio was still transcribed when no captions existed - which is why the field was replaced."}}},"ChannelRequest":{"type":"object","required":["channel"],"properties":{"channel":{"type":"string","minLength":1,"maxLength":2000,"description":"Channel @handle, /channel/UC… URL, or UC… ID."},"limit":{"type":"integer","minimum":1,"maximum":50,"default":5,"description":"Max videos to return per page. Defaults to 5."},"cursor":{"type":"string","maxLength":256,"description":"Opaque pagination cursor from a previous response's next_cursor (max 256 characters). Omit for the first page. Cursors encode a list offset capped at 2000, so pagination cannot reach past the first 2000 items."},"since_video_id":{"type":"string","minLength":1,"maxLength":2000,"description":"Newest video ID you have already seen. The response is trimmed to videos newer than it, and a page with nothing newer costs no credits - so you can poll a channel for new uploads for free."}}},"PlaylistRequest":{"type":"object","required":["playlist"],"properties":{"playlist":{"type":"string","minLength":1,"maxLength":2000,"description":"Playlist URL or playlist ID."},"limit":{"type":"integer","minimum":1,"maximum":50,"default":5,"description":"Max videos to return per page. Defaults to 5."},"cursor":{"type":"string","maxLength":256,"description":"Opaque pagination cursor from a previous response's next_cursor (max 256 characters). Omit for the first page. Cursors encode a list offset capped at 2000, so pagination cannot reach past the first 2000 items."}}},"SearchRequest":{"type":"object","required":["query"],"properties":{"query":{"type":"string","minLength":1,"maxLength":2000,"description":"Keyword search query."},"limit":{"type":"integer","minimum":1,"maximum":50,"default":5,"description":"Max videos to return per page. Defaults to 5."},"cursor":{"type":"string","maxLength":256,"description":"Opaque pagination cursor from a previous response's next_cursor (max 256 characters). Omit for the first page. Cursors encode a list offset capped at 2000, so pagination cannot reach past the first 2000 items."}}},"BatchRequest":{"type":"object","required":["video_ids"],"properties":{"video_ids":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Video URLs or bare YouTube IDs, from any supported platform. Up to your plan's batch cap: 50 on free/Basic/Pro, 500 on Mega/Scale. Also accepted as the legacy key videoIds (send one or the other, not both)."},"mode":{"type":"string","enum":["auto","captions"],"description":"Where each entry's text may come from. \"auto\" (the default) reads captions and transcribes the audio when there are none. \"captions\" reads an existing caption track only, so a captionless video fails as no_transcript - the behaviour batch had before audio fallback existed. Note \"audio\" is not accepted here: skipping captions for 50 videos at once is a job queue, not a batch fetch."}}},"Health":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"service":{"type":"string"},"version":{"type":"string"},"time":{"type":"string","format":"date-time"}}},"Usage":{"type":"object","properties":{"credits_spent":{"type":"integer"},"balance":{"type":["integer","null"],"description":"Remaining balance, or null for unlimited (admin) accounts."},"bytes":{"type":"integer"}}},"Segment":{"type":"object","properties":{"start":{"type":"number","description":"Start time in seconds."},"duration":{"type":"number","description":"Cue duration in seconds."},"text":{"type":"string"},"speaker":{"type":"integer","description":"Speaker id (0, 1, …) on diarized podcast transcripts only; absent everywhere else."}}},"Video":{"type":"object","properties":{"videoId":{"type":"string"},"title":{"type":"string"},"thumbnailUrl":{"type":"string"},"duration":{"type":["number","null"]},"channel":{"type":["string","null"]}}},"SuccessEnvelope":{"type":"object","required":["ok","request_id","data","usage"],"properties":{"ok":{"type":"boolean","enum":[true]},"request_id":{"type":"string"},"data":{"oneOf":[{"$ref":"#/components/schemas/TranscriptData"},{"$ref":"#/components/schemas/VideoListData"},{"$ref":"#/components/schemas/MeData"}],"discriminator":{"propertyName":"kind"}},"usage":{"$ref":"#/components/schemas/Usage"}}},"TranscriptData":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["transcript"]},"video_id":{"type":"string"},"title":{"type":["string","null"]},"thumbnailUrl":{"type":["string","null"],"description":"Poster image for the video. TikTok and Instagram serve signed, expiring URLs, so copy the image rather than hotlinking it."},"diarized":{"type":"boolean","description":"True when speaker diarization produced labels (podcast transcriptions only); segments then carry `speaker` ids."},"text":{"type":["string","null"]},"segments":{"type":["array","null"],"items":{"$ref":"#/components/schemas/Segment"}},"podcast":{"type":"object","description":"Present only when the input was a podcast link. Spotify and Apple do not host podcast audio; both read the publisher's RSS feed, so the link is resolved to that feed and the episode's own audio file. These fields say which show and episode were matched, so you can verify the resolution was correct.","properties":{"show":{"type":["string","null"]},"episode":{"type":["string","null"]},"published_at":{"type":["string","null"],"format":"date-time"},"feed_url":{"type":["string","null"],"format":"uri"},"audio_url":{"type":"string","format":"uri"},"resolved_via":{"type":"string","enum":["spotify","apple","rss","direct"]}}}}},"VideoListData":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["video_list"]},"source":{"type":"string"},"videos":{"type":"array","items":{"$ref":"#/components/schemas/Video"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `cursor` for the next page; null when exhausted."}}},"MeData":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["me"]},"user_id":{"type":"string"},"credits":{"type":["integer","null"],"description":"Remaining balance, or null for unlimited (admin) accounts."}}},"TranscriptJobData":{"type":"object","required":["kind"],"description":"Placeholder data block on a 202: identifies what is being transcribed while the job runs.","properties":{"kind":{"type":"string","enum":["transcript_job"]},"video_id":{"type":"string"},"platform":{"type":"string"}}},"JobAcceptedEnvelope":{"type":"object","required":["ok","request_id","status","job_id","poll_url"],"description":"Returned with HTTP 202 when a request escalates to audio transcription. Poll poll_url (or wait for the callback_url delivery); credits are charged on delivery, not here.","properties":{"ok":{"type":"boolean","enum":[true]},"request_id":{"type":"string"},"status":{"type":"string","enum":["processing"]},"job_id":{"type":"string"},"poll_url":{"type":"string","description":"Path of the job endpoint to poll."},"data":{"description":"Either a transcript_job placeholder, or a transcript block with null text/segments, depending on how the request escalated.","oneOf":[{"$ref":"#/components/schemas/TranscriptJobData"},{"$ref":"#/components/schemas/TranscriptData"}]},"ai_fallback":{"type":"object","description":"Why transcription engaged and what delivery will cost, e.g. {engaged, captions_unavailable, message, cost_credits}."},"usage":{"$ref":"#/components/schemas/Usage"}}},"JobStatusEnvelope":{"type":"object","required":["ok","request_id","status","job_id"],"description":"Response of the job poll endpoint. data holds the transcript once status is completed and is null while processing (usage is absent then); a failed job has ok:false and an error block instead.","properties":{"ok":{"type":"boolean"},"request_id":{"type":"string"},"status":{"type":"string","enum":["processing","completed","failed"]},"job_id":{"type":"string"},"data":{"description":"The finished transcript; null until the job completes.","oneOf":[{"$ref":"#/components/schemas/TranscriptData"},{"type":"null"}]},"usage":{"$ref":"#/components/schemas/Usage"},"error":{"type":"object","description":"Present when status is failed.","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}},"BatchEnvelope":{"type":"object","required":["ok","request_id","data","usage"],"properties":{"ok":{"type":"boolean","enum":[true]},"request_id":{"type":"string"},"data":{"type":"object","properties":{"kind":{"type":"string","enum":["transcript_batch"]},"results":{"type":"array","items":{"$ref":"#/components/schemas/BatchResult"}}}},"usage":{"$ref":"#/components/schemas/Usage"}}},"BatchResult":{"type":"object","properties":{"video_id":{"type":"string"},"outcome":{"type":["string","null"],"enum":["ok","no_transcript","processing","error",null]},"reason":{"type":["string","null"],"description":"Structured failure reason (no_captions, unavailable, upstream_error, …); null on success."},"job_id":{"type":["string","null"],"description":"Set on outcome \"processing\": the audio-transcription job started for a captionless entry. Re-send the batch later or poll poll_url."},"poll_url":{"type":["string","null"],"description":"Set on outcome \"processing\": path to poll for the finished transcript."},"message":{"type":["string","null"],"description":"Human-readable explanation of the failure; null on success."},"title":{"type":["string","null"]},"text":{"type":["string","null"]},"segments":{"type":["array","null"],"items":{"$ref":"#/components/schemas/Segment"}},"bytes":{"type":"integer"}}},"AiFallback":{"type":"object","description":"Attached to transcript failures. Distinguishes 'this video has no captions' from 'we could not check', and says whether transcribing the audio would still work.","required":["available","captions_unavailable","message"],"properties":{"available":{"type":"boolean","description":"True when retrying the same request with ai_fallback:true would actually start a transcription."},"captions_unavailable":{"type":"boolean","description":"True only when we reached the video and confirmed no caption track exists. False means we could not check, NOT that captions exist."},"unavailable_reason":{"type":"string","enum":["unsupported_input","no_speech_to_transcribe","content_inaccessible","retry_captions_first","insufficient_credits"],"description":"Present when available is false: why AI transcription cannot be used."},"message":{"type":"string"},"retry_with":{"type":"object","description":"Merge into the original request body to trigger the fallback.","properties":{"ai_fallback":{"type":"boolean","enum":[true]}}},"cost_credits":{"type":"integer","description":"Credits charged on successful delivery. Failures are free. AI transcription bills by audio length, so this is an estimate from the measured media length when cost_estimated is true, and otherwise the one-block minimum."},"cost_estimated":{"type":"boolean","description":"True when cost_credits came from the media's real duration; false when it is only the minimum, because the length was not known at that point."},"balance":{"type":["integer","null"],"description":"Remaining balance; null for admins."}}},"ErrorEnvelope":{"type":"object","required":["ok","request_id","error"],"properties":{"ok":{"type":"boolean","enum":[false]},"request_id":{"type":"string"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_credits","rate_limited","idempotency_conflict","unsupported_platform","upstream_unavailable","internal_error","live_stream","was_live","no_audio_stream","audio_too_long","no_speech","captions_disabled","no_captions","age_restricted","members_only","private","unavailable","bot_gate","rate_limited","ip_blocked","region_blocked","timeout","upstream_error","proxy_unavailable","connection","parse_error","invalid_input","drm_protected","unknown"]},"message":{"type":"string"},"issues":{"type":"array","description":"Field-level validation problems, when applicable.","items":{"type":"object"}}}},"reason":{"type":"string","description":"Structured failure reason on transcript endpoints (no_captions, captions_disabled, age_restricted, rate_limited, …)."},"ai_fallback":{"$ref":"#/components/schemas/AiFallback"}}}},"responses":{"Error":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}