{"openapi":"3.1.0","info":{"title":"transcribe.so API","version":"1.0.0","summary":"Transcribe audio and video at scale with multilingual ASR.","description":"The transcribe.so API takes audio or video — from YouTube, a hosted\nplatform URL (Apple Podcasts, Spotify, Vimeo, ...), a public media URL,\nor a file you upload — and returns full transcripts with speaker\nlabels, timestamped sentence-level segments, chapters, sections, Q&A,\nand citations. Live Q&A (`POST /transcriptions/{id}/ask`, `POST /ask`)\nanswers questions with cited timestamps and is metered by the same\nQ&A allowance as the app (never the wallet).\n\n## Authentication\n\nEvery request carries `Authorization: Bearer <token>`. The token is\neither an API key (`tsk_live_<secret>`, get one at\nhttps://transcribe.so/settings/api-keys; shown once, we store only\n`sha256(key)`) or an OAuth 2.1 access token issued to a registered\nclient (see `OAuth2` under securitySchemes and\nhttps://transcribe.so/.well-known/oauth-authorization-server). Webhook\nendpoints accept API keys only.\n\n## Rate limits\n\nEvery endpoint shares one limiter: 60 requests per minute per API key\n(per user for OAuth tokens), shared with the MCP endpoint. Exceeding it\nreturns 429 `rate_limited` with a `retry-after` header (seconds), even\non operations that don't list a 429 response explicitly.\n\n## Pricing\n\nSame per-minute rates as the dashboard. Wallet-funded: charges draw\nfrom your wallet balance (top-ups). No commits, no minimums. See\nhttps://transcribe.so/pricing. Captioned clip renders\n(`POST /transcriptions/{id}/clips`, currently up to 60 s per clip)\nare a flat $0.05 per started 60 s of clip, wallet-only: held at\nrequest, settled on completion, released on failure.\n\n## Errors\n\nAll errors return the same envelope: `{ \"error\": { \"code\", \"message\",\n\"request_id\", \"doc_url\" } }`. The `code` is stable; the `message`\ninlines an actionable URL where relevant; the `doc_url` points to the\ndocs section for that error.\n\n## Idempotency\n\nPOST endpoints accept `Idempotency-Key`. Cached for 24h per\n`(api_key, idempotency_key)` pair. See https://transcribe.so/developers/docs#idempotency.\n\n## Webhooks\n\nRegister a URL via `POST /webhooks` and we'll POST signed events when\ntranscriptions reach a terminal state. HMAC-SHA256 over `${t}.${body}`,\nsent as `X-Transcribe-Signature: t=<unix>,v1=<hex>`. Payloads, headers,\nand retry policy are documented in the top-level `webhooks` section.\nPer-request callbacks: pass `callback_url` on POST /transcriptions to\nreceive the same signed events for that one job without registering a\nwebhook (works for OAuth/MCP callers too); the signing secret comes back\nin the 202 as `callback.secret`. Clip renders fire `clip.completed` /\n`clip.failed` the same way (`callback_url` on POST /clips, secret in\n`callback_secret`; registered webhooks receive them for clips created\nwith their key).\n\n## MCP\n\nThe same account and transcription surface is exposed as an MCP server\n(Streamable HTTP) at https://transcribe.so/mcp for Claude, ChatGPT and\nother MCP clients. Same Bearer tokens, same rate limit. Discovery:\nhttps://transcribe.so/.well-known/mcp/server-card.json.\n","x-transcribe-mcp":{"url":"https://transcribe.so/mcp","transport":"streamable-http","server_card":"https://transcribe.so/.well-known/mcp/server-card.json","tools":["getAccount","listPipelines","listTranscriptions","getTranscription","getTranscriptionResult","waitForTranscription","getTranscriptionTimestamps","getTranscriptionWords","getSubtitles","getClip","getQuote","search","fetch","askTranscription","askLibrary","transcribe","createUpload","renderClip","regeneratePostingChapters","retryTranscription","deleteTranscription"]},"contact":{"name":"transcribe.so support","url":"https://transcribe.so/contact"},"license":{"name":"Proprietary","url":"https://transcribe.so/terms-of-service"}},"servers":[{"url":"https://transcribe.so","description":"Production"}],"externalDocs":{"description":"Full API reference","url":"https://transcribe.so/developers/docs"},"tags":[{"name":"Account","description":"User account, wallet balance, navigation links."},{"name":"Pipelines","description":"The transcription capability catalog and pricing."},{"name":"Transcriptions","description":"Submit, list, fetch, delete, retry transcriptions."},{"name":"Uploads","description":"Two upload modes: short-lived presigned S3 PUT for one-shot uploads,\nor resumable tusd uploads for large files / flaky networks.\n"},{"name":"Quotes","description":"Preview cost before committing."},{"name":"Clips","description":"Hosted captioned MP4 clip renders (audiogram style: audio segment,\nbranded background, word-by-word captions) of a completed\ntranscription, currently up to 60 s per clip. Flat $0.05 per\nstarted 60 s, wallet-only.\n"},{"name":"Webhooks","description":"Register and manage webhook delivery URLs."},{"name":"Realtime","description":"Live microphone transcription sessions (internal beta)."}],"security":[{"BearerAuth":[]},{"OAuth2":[]}],"paths":{"/api/v1/me":{"get":{"tags":["Account"],"summary":"Authenticated user","description":"Returns the authenticated user, wallet balance, subscription\ntier, effective plan `limits` (file duration cap, upload size cap,\nconcurrent jobs), a self-discovering `links` map (api keys,\nbilling, docs, support) and, for API-key auth, an `api_key` block\n(scopes, monthly cap, month-to-date spend).\n","operationId":"getMe","responses":{"200":{"description":"Account info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/pipelines":{"get":{"tags":["Pipelines"],"summary":"Capability catalog + pricing","description":"The standard transcription pipeline with current per-minute pricing,\nsupported languages, and timestamp capability metadata. Every\ntranscription includes speaker labels (diarization), timestamped\nsegments, and AI analysis in the base price. Same rates as /pricing.\n","operationId":"listPipelines","responses":{"200":{"description":"Pipelines","content":{"application/json":{"schema":{"type":"object","required":["pipelines"],"properties":{"pipelines":{"type":"array","items":{"$ref":"#/components/schemas/Pipeline"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/uploads":{"post":{"tags":["Uploads"],"summary":"Get a presigned S3 PUT URL","description":"One-shot upload. Returns a short-lived (900s) presigned URL. PUT the\nraw file body to it with the same `Content-Type`. Then call\n`POST /transcriptions` with `source: \"upload\"` and the returned\n`upload_id`.\n\nFor files over ~50 MB or unstable networks, prefer\n`POST /api/v1/uploads/tus` (resumable).\n","operationId":"createUpload","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadRequest"}}}},"responses":{"200":{"description":"Presigned URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/uploads/tus":{"post":{"tags":["Uploads"],"summary":"Start a resumable (tus) upload","description":"Returns a tusd endpoint URL + short-lived HMAC token. Use any\ntus 1.0 client; put the token in `Upload-Metadata` under\n`upload_metadata_key`. Then pass `upload_id=\"tus/<last-path-segment>\"`\nto POST /transcriptions. Max 1 GB. See /developers/docs#uploads.\n","operationId":"createTusUpload","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TusUploadRequest"}}}},"responses":{"200":{"description":"tusd endpoint + signed upload token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TusUploadResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/transcriptions":{"get":{"tags":["Transcriptions"],"summary":"List transcriptions","description":"Newest first, cursor-paginated.","operationId":"listTranscriptions","parameters":[{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"in":"query","name":"cursor","description":"ISO timestamp of the last item from the previous page.","schema":{"type":"string","format":"date-time"}},{"in":"query","name":"api_only","description":"When true, returns only transcriptions created via API.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Transcription list","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Transcription"}},"next_cursor":{"type":["string","null"],"format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Transcriptions"],"summary":"Submit a transcription","description":"Four input sources (`youtube`, `platform_url`, `external_url`,\n`upload`). Quote, wallet hold, enqueue. Returns 202 with the\njob's id and charge. Send `Idempotency-Key` to make retries safe.\nOptional `callback_url` gets this job's signed completed/failed\nwebhook (secret in `callback.secret`).\n","operationId":"createTranscription","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Up to 128 chars. Cached for 24h per (api_key, idempotency_key).\nReusing the same key with a different body returns 400.\n","schema":{"type":"string","maxLength":128}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTranscriptionRequest"}}}},"responses":{"202":{"description":"Job queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTranscriptionResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientFunds"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/transcriptions/{id}":{"get":{"tags":["Transcriptions"],"summary":"Get a transcription","operationId":"getTranscription","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}}],"responses":{"200":{"description":"Transcription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transcription"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Transcriptions"],"summary":"Delete a transcription","description":"Permanently deletes the row, all derived rows (segments, chapters,\nsections, qna), and any S3 objects (uploaded audio, generated text).\n","operationId":"deleteTranscription","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTranscriptionResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/transcriptions/{id}/result":{"get":{"tags":["Transcriptions"],"summary":"Get transcription result sections (chapters, sections, Q&A by default)","description":"Default `?include=chapters,sections,qna`. `chapters` = 8-30 LLM-curated\nnavigable items. `acts` = 3-15 narrative summaries for show notes.\n`segments` = verbatim. `posting_chapters` = per-variant cache.\nOnly meaningful once status=completed.\n","operationId":"getTranscriptionResult","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"include","in":"query","required":false,"description":"Comma-separated list of result sections to include. Valid\nvalues: `chapters`, `acts`, `sections`, `qna`, `segments`,\n`posting_chapters`. Pass `all` for every section. Default:\n`chapters,sections,qna` (skips segments + posting_chapters).\nUnknown values are silently ignored; an empty/invalid list\nfalls back to the default.\n\n`chapters` vs `acts`: `chapters` is the LLM-curated scannable\nnavigation list (8-30 items, default); `acts` is the per-act\nnarrative summary (3-15 items, each with a 3-5 sentence summary)\nused for show notes / RSS / podcast chapter export. Both share\nthe same wire shape; only the source row differs.\n","schema":{"type":"string","examples":["chapters,sections,qna","acts","segments","posting_chapters","all"]}}],"responses":{"200":{"description":"Result with the requested sections included","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/transcriptions/{id}/timestamps":{"get":{"tags":["Transcriptions"],"summary":"Platform-formatted chapter timestamps (paste-ready)","description":"Paste-ready chapter timestamps for the chosen `format` and\n`variant`. Enforces each platform's rules (char budget, spacing,\nHH:MM:SS). 409 not_ready means the curated cache isn't generated\nyet: POST /timestamps/regenerate once, then retry.\n","operationId":"getTranscriptionTimestamps","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"format","in":"query","required":false,"description":"Destination — where the caller will paste output. Picks the\nrenderer. Default `youtube`.\n\n- `youtube` / `spotify` / `apple_podcasts` — chapter list per\n  platform's rules (HH:MM:SS for Apple; ≥3 chapters / first\n  at 0:00 / ≥10s spacing for YouTube).\n- `markdown` — clickable `[mm:ss](url?t=<sec>)` for\n  blog/Discord/Slack/Chat.\n- `x` — X (Twitter) thread, 280 char/post. Response includes\n  `thread: string[]`.\n- `threads` — Meta Threads, 500 char/post. Response includes\n  `thread: string[]`.\n- `instagram_caption` — no URLs (IG kills clickability); CTA\n  points to bio link.\n- `plain` — raw `mm:ss`, no platform rules.\n\nIgnored for `variant=show_notes` and `variant=original`.\n","schema":{"type":"string","enum":["youtube","spotify","apple_podcasts","markdown","x","threads","instagram_caption","plain"],"default":"youtube"}},{"name":"variant","in":"query","required":false,"description":"Content — pick by user intent. Default `standard`.\n- `standard` — 8-30 chapter list (default outline/TOC).\n- `highlights` — 5-item TEXT outline for IG caption / TikTok pinned / X bio. NOT clips.\n- `clips` — 3 VIDEO clip ideas for Reels / TikTok / Shorts.\n- `quoted_sections` — 8-12 items with insight_title + verbatim quote.\n- `show_notes` — per-chapter Markdown summaries.\n- `original` — raw section list.\n","schema":{"type":"string","enum":["standard","highlights","clips","quoted_sections","show_notes","original"],"default":"standard"}},{"name":"style","in":"query","required":false,"deprecated":true,"description":"Deprecated. Accepted for back-compat and ignored: every\nvariant is generated in the single `balanced` voice, which\nthe worker pre-generates for all four cached variants. The\nresponse echoes `style: \"balanced\"` (null for `show_notes`\nand `original`, which don't use the curated cache).\n","schema":{"type":"string","enum":["balanced"],"default":"balanced"}}],"responses":{"200":{"description":"Paste-ready timestamps for the destination platform","content":{"application/json":{"schema":{"type":"object","properties":{"format":{"type":"string","description":"Echoes the requested destination format."},"variant":{"type":"string","description":"Echoes the requested content variant."},"style":{"type":["string","null"],"description":"Always `balanced` (the only voice generated).\n`null` when the variant doesn't use the curated\ncache (`show_notes`, `original`).\n"},"text":{"type":"string","description":"Paste this directly into the destination platform."},"thread":{"type":"array","items":{"type":"string"},"description":"Present only for `variant=quoted_sections` +\n`format=x` or `format=threads`. Array of pre-split\nposts (each ≤280 chars for X, ≤500 for Threads)\nfor sequential posting. The flat `text` field\njoins them with separators for clients that just\nwant one paste-able blob.\n"},"char_count":{"type":"integer"},"items_used":{"type":"integer"},"items_total":{"type":"integer"},"truncated":{"type":"boolean"},"ok_to_paste":{"type":"boolean","description":"False when the result violates a hard platform rule\nthe user can't fix client-side (e.g. <3 chapters\nfor YouTube). Text is still returned so callers can\nsurface the partial result.\n"},"warnings":{"type":"array","items":{"type":"string"}},"constraints":{"type":"object","description":"Platform constraints applied. Lets callers explain\nwhy titles got truncated or items were merged.\nOmitted for `variant=show_notes` and `variant=original`.\n"},"source":{"type":"object","properties":{"kind":{"type":"string","enum":["posting_chapters","chapters_summary","raw_sections"]},"generated_at":{"type":["string","null"],"format":"date-time"},"model":{"type":["string","null"],"description":"Public pipeline alias (`standard`) when the chapters were AI-generated, else null. Never an internal model identifier.\n","enum":["standard",null]},"regen_count":{"type":"integer"},"available_styles":{"type":"array","items":{"type":"string","enum":["balanced"]},"description":"Which styles are cached for this transcription.\nToday either `[]` (not generated yet) or\n`[\"balanced\"]`. Kept for wire compatibility.\n"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The curated `posting_chapters` cache for this variant hasn't\nbeen generated yet (legacy transcriptions, or generation still\nrunning). The error envelope includes `available_styles`\n(empty here) and the `variant`. Populate it with ONE POST to\n`/timestamps/regenerate`, then retry.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"error":{"type":"object","properties":{"available_styles":{"type":"array","items":{"type":"string","enum":["balanced"]}},"requested_style":{"type":"string","enum":["balanced"]},"variant":{"type":"string","enum":["standard","highlights","clips","quoted_sections","show_notes","original"]}}}}}]}}}}}}},"/api/v1/transcriptions/{id}/timestamps/regenerate":{"post":{"tags":["Transcriptions"],"summary":"(Re)generate the posting_chapters cache","description":"Runs the LLM curate+polish pipeline for all four cached variants\nwith an optional 200-char refine prompt, replacing the cached\n`balanced` entries. Synchronous, latency 30-90s. Capped at 10\nregens per transcription (resets on re-transcription).\n","operationId":"regeneratePostingChapters","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"style":{"type":"string","deprecated":true,"enum":["balanced"],"default":"balanced","description":"Deprecated. Accepted for back-compat and ignored;\ngeneration always uses the `balanced` voice.\n"},"refine_prompt":{"type":["string","null"],"maxLength":200,"description":"Free-text steer for the LLM. Examples: \"focus on\nthe case studies\", \"emphasize technical decisions\".\nValidated for prompt-injection markers; truncated\nto 200 chars. Replaces the cached entries.\n"}}}}}},"responses":{"200":{"description":"Newly generated posting_chapters","content":{"application/json":{"schema":{"type":"object","properties":{"regenerated":{"type":"boolean"},"regen_count":{"type":"integer"},"regen_limit":{"type":"integer"},"regen_remaining":{"type":"integer"},"style":{"type":"string","enum":["balanced"]},"refine_prompt":{"type":["string","null"]},"available_styles":{"type":"array","items":{"type":"string","enum":["balanced"]}},"posting_chapters":{"type":"object","description":"The standard variant, just-generated."},"posting_chapters_highlights":{"type":"object","description":"The highlights variant (5-item condensed text outline for tight surfaces), just-generated."},"posting_chapters_clips":{"type":"object"},"posting_chapters_quoted_sections":{"type":"object","description":"The quoted_sections variant, just-generated. Items\ncarry `{start_ms, insight_title, quote}` (richer\nthan the chapter-list variants).\n"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"Regeneration limit reached for this transcription.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/transcriptions/{id}/words":{"get":{"tags":["Transcriptions"],"summary":"Word-level timings (paginated, milliseconds)","description":"Paginated word timings in ms with the owning segment's speaker.\nAlways 200 when the row exists: `available:false` + `reason`\n(`not_completed` | `no_word_timestamps`) with empty `words`.\nPage with `offset`/`limit` until `has_more` is false.\n","operationId":"getTranscriptionWords","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"offset","in":"query","required":false,"description":"Zero-based index of the first word to return. Default 0.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"limit","in":"query","required":false,"description":"Page size. Default 2000, max 5000.","schema":{"type":"integer","minimum":1,"maximum":5000,"default":2000}}],"responses":{"200":{"description":"Word page. Note the casing: this endpoint is snake_case\n(`start_ms`), while `/subtitles?format=json` keeps the subtitle\nexporter's camelCase (`startMs`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordsResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/transcriptions/{id}/ask":{"post":{"tags":["Transcriptions"],"summary":"Ask a question about one transcription (live Q&A)","description":"Live LLM answer grounded in the transcript's sections (~10s,\nnon-streaming). Consumes the daily Q&A allowance shared with the\napp (tier-based, never the wallet); `no_answer` costs nothing.\nCached pairs stay free via /result?include=qna. Needs status completed.\n","operationId":"askTranscription","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskRequest"}}}},"responses":{"200":{"description":"Answer with citations. `remaining_*` are AFTER this call and null\nwhen unlimited; on `no_answer: true` nothing was persisted and no\nallowance was consumed.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Transcription is not `completed` yet (`not_ready`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/QnaQuotaExceeded"}}}},"/api/v1/transcriptions/{id}/subtitles":{"get":{"tags":["Transcriptions"],"summary":"Subtitle file (SRT / VTT / karaoke VTT / JSON)","description":"Returns the subtitle file as a RAW body (not the JSON envelope) so\nit can be piped to disk. Requires status `completed` (409\n`not_ready` otherwise). Errors keep the envelope. Cue metadata is\nin `x-transcribe-cue-*` response headers.\n","operationId":"getSubtitles","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"format","in":"query","required":false,"description":"Output format. Default `srt`.\n- `srt` (`application/x-subrip; charset=utf-8`)\n- `vtt` (`text/vtt; charset=utf-8`)\n- `vtt-karaoke` (`text/vtt; charset=utf-8`, inline `<hh:mm:ss.mmm>`\n  per-word tags: absolute WebVTT cue timestamps inside the cue, so\n  browsers' native `<track>` and players that implement cue\n  timestamps reveal words progressively; most video editors ignore\n  the tags and import the cue text. For per-word editing use\n  `mode=word` SRT or `/words` JSON)\n- `json` (`application/json`, the subtitle exporter's cue JSON,\n  camelCase: `startMs`, `endMs`, `words[].punctuationAfter`)\n","schema":{"type":"string","enum":["srt","vtt","vtt-karaoke","json"],"default":"srt"}},{"name":"preset","in":"query","required":false,"description":"Platform line-length / cues-per-second rules. Default `youtube`.\n`tiktok-shorts` and `instagram-reels` for short vertical video.\n`custom` is not accepted over the API.\n","schema":{"type":"string","enum":["youtube","tiktok-shorts","instagram-reels","netflix","podcast","broadcast"],"default":"youtube"}},{"name":"speaker_labels","in":"query","required":false,"description":"Prefix each cue with `[Speaker]`. Accepts true/false/1/0. Default false.","schema":{"type":"boolean","default":false}},{"name":"mode","in":"query","required":false,"description":"Cue timing. `auto` (default) = word-timed cues when word\ntimestamps exist, else sentence cues. `word` = word-timed cues\nunder the preset's line rules (NOT one word per cue; use\n`/words` for that); 400 `invalid_request` when the transcription\nhas no word timestamps. `sentence` = one cue per sentence.\nWhen `source=materialized` wins, `mode` is reported (from the\nstored cues, in `x-transcribe-cue-mode`), not honored.\n","schema":{"type":"string","enum":["auto","word","sentence"],"default":"auto"}},{"name":"source","in":"query","required":false,"description":"Which cues to serve. `auto` (default): the user's edited cues\nfrom the app when they exist AND `mode` is omitted/auto AND\n`preset` is omitted/youtube (you asked for nothing specific, so\nyou get what the app shows); any explicit `mode` or non-default\n`preset` regenerates. `generated` always regenerates.\n`materialized` requires edited cues (404 `not_found` otherwise);\nedited cues carry preset line-breaking only.\n","schema":{"type":"string","enum":["auto","generated","materialized"],"default":"auto"}}],"responses":{"200":{"description":"Subtitle file body. `Content-Disposition: inline;\nfilename=\"<slug>-<preset|sentence>.<ext>\"`.\n","headers":{"x-transcribe-cue-count":{"description":"Number of cues in the body.","schema":{"type":"integer"}},"x-transcribe-cue-source":{"description":"Where the cues came from.","schema":{"type":"string","enum":["generated","materialized"]}},"x-transcribe-cue-mode":{"description":"Resolved cue timing (never `auto`).","schema":{"type":"string","enum":["word","sentence"]}},"content-disposition":{"description":"`inline; filename=\"<slug>-<preset|sentence>.<ext>\"`","schema":{"type":"string"}}},"content":{"application/x-subrip":{"schema":{"type":"string"},"example":"1\n00:00:00,000 --> 00:00:02,400\nWelcome back to the show.\n"},"text/vtt":{"schema":{"type":"string"},"example":"WEBVTT\n\n00:00:00.000 --> 00:00:02.400\nWelcome back to the show.\n"},"application/json":{"schema":{"type":"object","description":"Subtitle exporter JSON (camelCase). `cues[].words` is\npresent for word-timed cues; each word carries `text`,\n`startMs`, `endMs`, `punctuationAfter`, `speaker`.\n"}}}},"400":{"description":"Invalid `format`/`preset`/`mode`/`source`/`speaker_labels`, or\n`mode=word` on a transcription without word timestamps\n(\"word timestamps not available for this transcription; use\nmode=sentence or auto\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Transcription not found / not yours; `source=materialized` with\nno edited cues; or a completed transcription with no segments.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_ready`: the transcription is not `completed` yet. The\nenvelope's `error.status` carries the current status. Wait via\n`/wait`, then retry.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"error":{"type":"object","properties":{"status":{"type":"string"}}}}}]}}}}}}},"/api/v1/transcriptions/{id}/wait":{"get":{"tags":["Transcriptions"],"summary":"Wait for transcription (long-poll, single tool call)","description":"Holds the connection up to `timeout` seconds (max 45) until the\ntranscription is `completed`/`failed`. Returns earlier if it\nfinishes. Use instead of polling. Pass `include=` to inline\nresult sections when completed (saves another call).\n","operationId":"waitForTranscription","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"timeout","in":"query","required":false,"description":"Seconds to hold the connection. Clamped to [1, 45].\nDefault 30. Tuned for ChatGPT GPT-Action tool-call timeout.\n","schema":{"type":"integer","minimum":1,"maximum":45,"default":30}},{"name":"include","in":"query","required":false,"description":"If completed within the timeout window, also inline\nresult sections. Same values as `/result`: comma-separated\nsubset of `chapters`, `acts`, `sections`, `qna`, `segments`,\n`posting_chapters`, or `all`. Omit to skip the inline\nresult and call /result separately.\n","schema":{"type":"string","examples":["chapters,sections,qna","all"]}}],"responses":{"200":{"description":"Transcription state. Adds `_timed_out: true` when the\ntimeout elapsed without reaching a terminal status.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/TranscriptionResult"},{"type":"object","properties":{"_timed_out":{"type":"boolean","description":"True if the timeout elapsed before the\ntranscription reached a terminal status.\nCaller may retry the same /wait endpoint.\n"}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/transcriptions/{id}/retry":{"post":{"tags":["Transcriptions"],"summary":"Retry a failed transcription","description":"Restart a transcription that reached `status: \"failed\"`. Charges\nrun again from scratch; failures don't refund the original hold.\n","operationId":"retryTranscription","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}}],"responses":{"202":{"description":"Retry queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientFunds"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/transcriptions/{id}/clips":{"post":{"tags":["Clips"],"summary":"Render a captioned clip (MP4)","description":"Queues a hosted captioned MP4 render of a 1-60 s range of a\ncompleted transcription with word timestamps. Flat $0.05 per\nstarted 60 s, held on the wallet now and settled on completion.\n202 with the queued clip; poll GET /clips/{clipId}?wait=45.\n","operationId":"createClip","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Up to 128 chars. Cached for 24h per (api_key, idempotency_key).\nReusing the same key with a different body returns 400.\n","schema":{"type":"string","maxLength":128}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClipRequest"}}}},"responses":{"202":{"description":"Clip render queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClipResponse"},"examples":{"queued":{"summary":"Queued clip","value":{"id":42,"transcription_id":4821,"status":"queued","start_seconds":12.5,"end_seconds":42.5,"duration_seconds":null,"aspect":"9:16","style":"karaoke","title":"Why the retry loop was wrong","charge_usd":0.05,"mp4_url":null,"mp4_url_expires_at":null,"error":null,"created_at":"2026-08-18T09:12:44.120Z","completed_at":null,"callback_secret":null}}}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientFunds"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`not_ready`: the transcription is not `completed` yet (wait via\n/wait) or has no stored audio to render from.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: request rate (60/min) or more than 10 clips\nqueued/rendering for this account; `error.message` says which.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Clips"],"summary":"List clips of a transcription","description":"Clips rendered from this transcription, newest first (max 50).\nCompleted clips carry a fresh presigned `mp4_url` (valid 1 hour).\n","operationId":"listClips","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}}],"responses":{"200":{"description":"Clip list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClipsListResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/transcriptions/{id}/clips/{clipId}":{"get":{"tags":["Clips"],"summary":"Get a clip (optionally long-poll until rendered)","description":"One clip. Pass `wait` (1-45 s) to hold the connection until the\nclip is `completed`/`failed`; the body then adds `_timed_out`.\nCompleted clips carry a presigned `mp4_url` (valid 1 hour; call\nagain for a fresh one).\n","operationId":"getClip","parameters":[{"name":"id","in":"path","required":true,"description":"Transcription identifier. Plain integer (e.g., `4821`).\n","schema":{"type":"integer","minimum":1,"examples":[4821]}},{"name":"clipId","in":"path","required":true,"description":"Clip identifier returned by POST /clips.","schema":{"type":"integer","minimum":1,"examples":[42]}},{"name":"wait","in":"query","required":false,"description":"Seconds to hold the connection until the clip is terminal.\nClamped to [1, 45]. Omit for an immediate snapshot.\n","schema":{"type":"integer","minimum":1,"maximum":45}}],"responses":{"200":{"description":"Clip state. Adds `_timed_out: true` when `wait` elapsed\nbefore the clip reached a terminal status.\n","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Clip"},{"type":"object","properties":{"_timed_out":{"type":"boolean","description":"Present only when `wait` was given."}}}]}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/quotes":{"post":{"tags":["Quotes"],"summary":"Preview transcription cost","description":"Returns the estimated cost without queueing. Same body shape as\n`POST /transcriptions`. Useful for showing \"this will cost $X\"\nconfirmation before committing.\n","operationId":"createQuote","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Up to 128 chars. Cached for 24h per (api_key, idempotency_key).\nReusing the same key with a different body returns 400.\n","schema":{"type":"string","maxLength":128}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTranscriptionRequest"}}}},"responses":{"200":{"description":"Quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quote"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/ask":{"post":{"tags":["Transcriptions"],"summary":"Ask a question across your library (live Q&A)","description":"Live LLM answer across the whole library or the given\n`transcription_ids` (~10s, non-streaming). Shares the SAME daily Q&A\nallowance as per-transcript Q&A and the app (never the wallet).\n`no_answer` costs nothing.\n","operationId":"askLibrary","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskLibraryRequest"}}}},"responses":{"200":{"description":"Answer with citations across transcripts. `saved: false` means\nnothing was persisted (no allowance consumed).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskLibraryResponse"}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/QnaQuotaExceeded"}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"Get the active webhook for this key","description":"API-key callers only (one webhook per key). OAuth tokens get 403.\n","operationId":"getWebhook","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Webhook (or null)","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"oneOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"null"}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Webhooks"],"summary":"Register or replace the webhook","description":"Replaces any existing active webhook on this key. The\n`signing_secret` is shown **once**; store it. API-key callers\nonly; OAuth tokens get 403. Unknown event names are dropped.\n","operationId":"createWebhook","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"HTTPS endpoint that will receive POSTs."},"events":{"type":"array","items":{"type":"string","enum":["transcription.completed","transcription.failed","clip.completed","clip.failed"]},"default":["transcription.completed","transcription.failed","clip.completed","clip.failed"]}}}}}},"responses":{"201":{"description":"Webhook created (signing secret returned once)","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","required":["signing_secret"],"properties":{"signing_secret":{"type":"string","description":"Shown only at creation. Store it. Used to verify\n`X-Transcribe-Signature` on incoming deliveries.\n"}}}]}}}},"400":{"$ref":"#/components/responses/InvalidRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"delete":{"tags":["Webhooks"],"summary":"Revoke the active webhook","description":"API-key callers only. OAuth tokens get 403.","operationId":"deleteWebhook","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"boolean","enum":[true]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/v1/webhooks/test":{"post":{"tags":["Webhooks"],"summary":"Enqueue a test event","description":"Enqueues a synthetic `webhook.test` delivery for the currently-\nregistered webhook. Useful to confirm your URL is reachable and\nsignature verification works before any real transcriptions run.\nAPI-key callers only; OAuth tokens get 403.\n","operationId":"testWebhook","security":[{"BearerAuth":[]}],"responses":{"202":{"description":"Test event enqueued","content":{"application/json":{"schema":{"type":"object","required":["delivery_id","enqueued"],"properties":{"delivery_id":{"type":"integer"},"enqueued":{"type":"boolean","enum":[true]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/realtime/sessions":{"post":{"tags":["Realtime"],"summary":"Start a realtime session (internal beta)","description":"Internal beta — not generally available. Holds 60 minutes of wallet\nbalance, creates a live transcription, and returns a single-use\n60-second WebSocket ticket. Unused held minutes are refunded when the\nsession ends.\n","operationId":"createRealtimeSession","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"language":{"type":"string","default":"auto","description":"Language hint for the ASR stream. `auto` (default) uses automatic language detection; otherwise a code from the realtime language allowlist (a subset of the languages in GET /pipelines). Unknown codes fail with 400 `unsupported_language`. Language validation runs before pipeline authorization, so `unsupported_language` takes precedence over `unsupported_pipeline`.\n"}}}}}},"responses":{"201":{"description":"Session created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RealtimeSession"}}}},"400":{"description":"Realtime is not enabled for this key (`unsupported_pipeline` — internal-beta access is granted per key), or the language hint is not supported (`unsupported_language`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"`insufficient_funds`: wallet balance can't cover the 60-minute upfront hold. `spend_cap_exceeded`: the API key's monthly spend cap would be passed: starting a session reserves the full 60-minute cap, so committed month spend plus that reserve must fit under the cap (a cap below one reserve blocks realtime for the key entirely); raise the cap at /settings/api-keys.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Session setup failed. Any held balance is released automatically; safe to retry.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/sessions/{id}":{"delete":{"tags":["Realtime"],"summary":"End a realtime session (internal beta)","description":"Internal beta. Requests session end; the service settles the held\nbalance for streamed minutes and closes the socket within one\nheartbeat. Returns 202 (accepted, not yet settled). Sessions 30s+\nthen run post-stop enrichment: completion (webhook and GET status)\nlags session end by 1-5 min, up to 10.\n","operationId":"endRealtimeSession","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Session id from POST /realtime/sessions."}],"responses":{"202":{"description":"End requested","content":{"application/json":{"schema":{"type":"object","required":["session_id","status"],"properties":{"session_id":{"type":"string"},"status":{"type":"string","enum":["ending"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/realtime/sessions/{id}/resume":{"post":{"tags":["Realtime"],"summary":"Resume a dropped realtime session (internal beta)","description":"Internal beta. After a socket drop, mints a fresh single-use ticket\nfor the SAME still-live session (same transcription, same hold).\n410 once the session has ended or gone stale.\n","operationId":"resumeRealtimeSession","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Session id from POST /realtime/sessions."}],"responses":{"201":{"description":"Fresh ticket for the same session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RealtimeSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"410":{"description":"Session already ended or stale.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"transcriptionCompleted":{"post":{"summary":"transcription.completed","description":"Sent when a transcription reaches `status: \"completed\"`.\n`data.transcription` is the row snapshot (note `completed_at`, not\n`processing_completed_at`). Fetch full content with GET\n/api/v1/transcriptions/{id}/result.\n","operationId":"onTranscriptionCompleted","parameters":[{"$ref":"#/components/parameters/WebhookSignatureHeader"},{"$ref":"#/components/parameters/WebhookEventHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx within 10s. Anything else is retried."}}}},"transcriptionFailed":{"post":{"summary":"transcription.failed","description":"Sent when a transcription reaches `status: \"failed\"`.\n`data.transcription.error` carries the failure reason.\n","operationId":"onTranscriptionFailed","parameters":[{"$ref":"#/components/parameters/WebhookSignatureHeader"},{"$ref":"#/components/parameters/WebhookEventHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx within 10s. Anything else is retried."}}}},"clipCompleted":{"post":{"summary":"clip.completed","description":"Sent when a clip render reaches `status: \"completed\"`. `data.clip`\nis the same shape as GET /clips/{clipId}; `mp4_url` is presigned\nfresh for every delivery attempt (valid 1 hour).\n","operationId":"onClipCompleted","parameters":[{"$ref":"#/components/parameters/WebhookSignatureHeader"},{"$ref":"#/components/parameters/WebhookEventHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx within 10s. Anything else is retried."}}}},"clipFailed":{"post":{"summary":"clip.failed","description":"Sent when a clip render reaches `status: \"failed\"`; the wallet\nhold was released. `data.clip.error` carries the failure reason.\n","operationId":"onClipFailed","parameters":[{"$ref":"#/components/parameters/WebhookSignatureHeader"},{"$ref":"#/components/parameters/WebhookEventHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx within 10s. Anything else is retried."}}}},"webhookTest":{"post":{"summary":"webhook.test","description":"Synthetic event enqueued by POST /api/v1/webhooks/test.\n`data` is `{ message, delivered_at }` (no transcription).\n","operationId":"onWebhookTest","parameters":[{"$ref":"#/components/parameters/WebhookSignatureHeader"},{"$ref":"#/components/parameters/WebhookEventHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Acknowledge with any 2xx."}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"tsk_live_<secret>","description":"Get a key at https://transcribe.so/settings/api-keys. Pass it as\n`Authorization: Bearer tsk_live_<secret>` on every request. Keys\ncarry `scopes`: `[\"read\",\"write\"]` (full access, default) or\n`[\"read\"]` (read-only: GET endpoints only; any POST/DELETE returns\n403 `scope_forbidden`, including quotes and uploads). A key may\nalso carry an optional monthly spend cap: once transcription jobs,\nclip renders or realtime sessions started with the key would pass\nit, POST /transcriptions, /retry, /clips and /realtime/sessions\nreturn 402 `spend_cap_exceeded` (a realtime session counts its full\n60-minute upfront reserve). GET /me echoes both under `api_key`.\n"},"OAuth2":{"type":"oauth2","description":"Per-user OAuth 2.1 (authorization code + PKCE S256) for agent\nclients (ChatGPT, Claude). The resulting access token is a JWT\nsent as `Authorization: Bearer <jwt>` on every request. Server\nmetadata: https://transcribe.so/.well-known/oauth-authorization-server\n(dynamic client registration supported). Webhook endpoints reject\nOAuth tokens with 403. OAuth tokens are always full access; for a\nread-only integration use a read-only API key instead.\n","flows":{"authorizationCode":{"authorizationUrl":"https://transcribe.so/oauth/authorize","tokenUrl":"https://transcribe.so/oauth/token","scopes":{"email":"Read the account email.","profile":"Read the account profile."}}}}},"parameters":{"WebhookSignatureHeader":{"name":"X-Transcribe-Signature","in":"header","required":true,"description":"`t=<unix-seconds>,v1=<hex>`; hex = HMAC-SHA256(signing_secret,\n`${t}.${rawBody}`). Reject when |now - t| > 300s.\n","schema":{"type":"string"}},"WebhookEventHeader":{"name":"X-Transcribe-Event","in":"header","required":true,"description":"Event name; equals the body's `event`.","schema":{"type":"string","enum":["transcription.completed","transcription.failed","clip.completed","clip.failed","webhook.test"]}}},"responses":{"Unauthorized":{"description":"Authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Authenticated but not allowed: `forbidden` (webhook endpoints called with an OAuth token, or a pipeline this account can't use), `entitlement_required` (plan lacks the feature), or `scope_forbidden` (read-only API key on a POST/DELETE endpoint; create a key with write access at /settings/api-keys).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InvalidRequest":{"description":"Malformed body / query / path parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The resource doesn't exist or isn't yours.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InsufficientFunds":{"description":"`insufficient_funds`: wallet balance can't cover the estimated charge. `spend_cap_exceeded`: the API key's monthly spend cap would be passed by this job (settled month spend + in-flight holds + this charge); raise the cap at /settings/api-keys.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Per-key request rate exceeded (60/min), or — on Pay as you go only — fair-use processing minutes are exhausted for the current 6h window (transcription-creating endpoints only); `error.message` says which. Request rate: back off and retry. Fair-use: wait for the window to roll or upgrade the plan. Subscription plans are never fair-use rejected; over-guideline jobs may queue at lower priority.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"QnaQuotaExceeded":{"description":"`qna_quota_exceeded`: the Q&A allowance is used up. `error.scope` is `transcript` (per-transcript cap, free plan) or `day` (rolling-24h daily cap, all plans); `error.retry_after` (seconds; also the `Retry-After` header) says when the daily window frees a slot. The allowance is shared with the transcribe.so app; upgrade for more. Cached Q&A pairs stay free via GET /result?include=qna.\n","headers":{"Retry-After":{"description":"Seconds until the daily window frees a slot (day scope only).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"error":{"type":"object","properties":{"scope":{"type":"string","enum":["transcript","day"]},"retry_after":{"type":"integer"}}}}}]},"examples":{"day":{"summary":"Daily allowance used up","value":{"error":{"code":"qna_quota_exceeded","message":"Daily Q&A allowance is used up (10 questions per rolling 24h on the free plan). The Q&A allowance is shared with the app and resets on a rolling 24h window; upgrade at https://transcribe.so/pricing for more. Cached Q&A pairs stay free via GET /api/v1/transcriptions/{id}/result?include=qna.","request_id":"req_01J8ZK3M2N4P5Q6R7S8T9V0W1X","doc_url":"https://transcribe.so/developers/docs#ask","scope":"day","retry_after":18342}}},"transcript":{"summary":"Per-transcript allowance used up (free plan)","value":{"error":{"code":"qna_quota_exceeded","message":"Q&A allowance for this transcript is used up (3 questions per transcript on the free plan).","request_id":"req_01J8ZK3M2N4P5Q6R7S8T9V0W1Y","doc_url":"https://transcribe.so/developers/docs#ask","scope":"transcript"}}}}}}}},"schemas":{"RealtimeSession":{"type":"object","required":["session_id","ws_url","ticket","cap_minutes"],"properties":{"session_id":{"type":"string","description":"Realtime session id (rts_...)."},"transcription_id":{"type":"integer","description":"The live transcription row (present on create)."},"ws_url":{"type":"string","description":"WebSocket endpoint to open with the ticket."},"ticket":{"type":"string","description":"Single-use ticket, valid 60 seconds, redeemed at WS handshake."},"cap_minutes":{"type":"integer","description":"Maximum session length backed by the wallet hold."},"language":{"type":"string","description":"The session's language hint as validated (`auto` or a realtime allowlist code). Present on create.\n"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","request_id","doc_url"],"properties":{"code":{"type":"string","description":"Stable, machine-readable error identifier.","enum":["unauthenticated","invalid_api_key","forbidden","entitlement_required","not_found","not_ready","invalid_request","unsupported_pipeline","unsupported_language","addon_not_supported","insufficient_funds","rate_limited","qna_quota_exceeded","scope_forbidden","spend_cap_exceeded","internal_error"]},"message":{"type":"string","description":"Human-readable. When there's an actionable URL we inline it\nso terminal users see it without parsing JSON.\n"},"request_id":{"type":"string","description":"Quote in support tickets."},"doc_url":{"type":"string","format":"uri","description":"Stable URL pointing to docs / dashboard for this error."}}}}},"Me":{"type":"object","required":["user_id","email","wallet_balance_usd","subscription_tier","limits","links"],"properties":{"user_id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"wallet_balance_usd":{"type":"number"},"subscription_tier":{"type":"string","description":"`free` is the Pay as you go plan; subscription plans are `starter`, `pro`, `business`, `enterprise`.\n","enum":["free","starter","pro","business","enterprise"],"examples":["free","pro"]},"limits":{"type":"object","description":"Effective plan limits, resolved live from the plan catalog.\n`max_file_minutes` is the cap the quote/start path enforces:\nyour plan's own cap, or the platform ceiling (12 hours) when\nthe plan has none.\n","required":["max_file_minutes","max_upload_bytes","max_concurrent_jobs"],"properties":{"max_file_minutes":{"type":"integer","description":"Single-file duration cap in minutes.","examples":[600,720]},"max_upload_bytes":{"type":"integer","description":"Single-file upload size cap in bytes (1 GiB).","examples":[1073741824]},"max_concurrent_jobs":{"type":"integer","description":"Transcription jobs allowed in flight at once.","examples":[1,5]}}},"links":{"type":"object","description":"Self-discovering navigation map. Surface in your own UI\ninstead of hardcoding URLs.\n","properties":{"dashboard":{"type":"string","format":"uri"},"api_keys":{"type":"string","format":"uri"},"billing":{"type":"string","format":"uri"},"docs":{"type":"string","format":"uri"},"support":{"type":"string","format":"uri"}}},"api_key":{"type":"object","description":"Present only when authenticated with a tsk_live_ API key.\n`scopes` is [\"read\"] (read-only) or [\"read\",\"write\"];\n`monthly_cap_usd` is the optional per-key spend cap (null =\nuncapped); `current_month_spent_usd` is settled spend this UTC\nmonth on transcription jobs started with the key;\n`current_month_committed_usd` = settled + active holds on\nin-flight jobs, which is what the cap compares against (active\nrealtime sessions count via their 60-minute hold; streamed\nminutes settle into spent).\n","required":["scopes","monthly_cap_usd","current_month_spent_usd","current_month_committed_usd"],"properties":{"scopes":{"type":"array","items":{"type":"string","enum":["read","write"]}},"monthly_cap_usd":{"type":["number","null"]},"current_month_spent_usd":{"type":"number"},"current_month_committed_usd":{"type":"number"}}}}},"Pipeline":{"type":"object","required":["code","name","retail_usd_per_min","retail_usd_per_hour","supported_languages"],"properties":{"code":{"type":"string","examples":["standard"]},"name":{"type":"string"},"description":{"type":["string","null"]},"retail_usd_per_min":{"type":"number"},"retail_usd_per_hour":{"type":"number"},"supported_languages":{"type":"array","description":"Each entry describes one supported language. `code` is the BCP-47\ntwo- or three-letter tag you pass to `language` on submit;\n`benchmark_error_rate_band` is the published accuracy band for\nthis pipeline on this language (null when no published result\nexists).\n","items":{"$ref":"#/components/schemas/PipelineLanguage"}},"word_timestamp_languages":{"type":["array","null"],"description":"Pipeline capability metadata: the subset of `supported_languages`\nfor which the engine produces word-level alignment internally.\nInformational only. No API v1 or MCP surface returns word-level\ntimestamps today; `segments` are sentence-level\n(`start_seconds` / `end_seconds` per segment). Null when the\npipeline has no word-level alignment for any language.\n","items":{"$ref":"#/components/schemas/PipelineLanguage"}},"timestamp_options":{"type":"array","description":"Capability metadata (`sentence`, `word`), not an export option.\nThe API returns sentence-level segments; there is no parameter\nto request word-level output yet.\n","items":{"type":"string","enum":["sentence","word"]}}}},"PipelineLanguage":{"type":"object","required":["code","label"],"properties":{"code":{"type":"string","description":"BCP-47-style language code, e.g. `en`, `zh`, `af`."},"label":{"type":"string","description":"English display name."},"native_name":{"type":["string","null"],"description":"Display name in the language itself."},"fleurs_wer":{"type":["number","null"],"deprecated":true,"description":"Deprecated. API v1 always returns null for this field; the key\nis retained for wire compatibility only. Use\n`benchmark_error_rate_band` instead.\n"},"benchmark_error_rate_band":{"type":["string","null"],"enum":["under_5_percent","5_to_under_15_percent","15_percent_or_higher",null],"description":"Published benchmark error-rate band for this pipeline on this\nlanguage — a coarsening of published FLEURS results (word error\nrate; character error rate for languages scored per character).\nNull when no published result exists.\n"}}},"UploadRequest":{"type":"object","required":["filename","content_type","file_size"],"properties":{"filename":{"type":"string"},"content_type":{"type":"string","description":"Allowed: audio/mpeg, audio/mp3, audio/wav, audio/m4a, audio/mp4,\naudio/x-m4a, audio/aac, audio/ogg, audio/webm, audio/flac,\nvideo/mp4, video/webm, video/quicktime, video/x-msvideo.\n"},"file_size":{"type":"integer","maximum":1073741824,"description":"Max 1 GB."}}},"UploadResponse":{"type":"object","required":["upload_id","upload_url","expires_in"],"properties":{"upload_id":{"type":"string","description":"Opaque token to pass back to POST /transcriptions."},"upload_url":{"type":"string","format":"uri","description":"PUT the file body here with the matching Content-Type."},"expires_in":{"type":"integer","description":"Seconds until upload_url expires (typically 900)."}}},"TusUploadRequest":{"type":"object","required":["filename","file_size"],"properties":{"filename":{"type":"string"},"file_size":{"type":"integer","maximum":1073741824,"description":"Max 1 GB. The token is bound to this size."}}},"TusUploadResponse":{"type":"object","required":["upload_endpoint","upload_token","upload_metadata_key","expires_in","max_file_size"],"properties":{"upload_endpoint":{"type":"string","format":"uri","description":"Public tusd endpoint. Initiate the upload here with a tus 1.0\nclient.\n"},"upload_token":{"type":"string","description":"Short-lived HMAC ticket. Place it in Upload-Metadata under\n`upload_metadata_key`. Tusd's pre-create hook validates it.\n"},"upload_metadata_key":{"type":"string","description":"Metadata key under which the token must appear. Always\n`\"upload-token\"` today; named so SDKs don't hard-code it.\n"},"expires_in":{"type":"integer","description":"Seconds until the token expires (typically 3600)."},"max_file_size":{"type":"integer","description":"Bytes; same 1 GB cap as the presigned-PUT flow."}}},"CreateTranscriptionRequest":{"type":"object","required":["source"],"description":"Submit a transcription. Four input sources selected by `source`:\n- `youtube`: provide `url` (a YouTube watch URL or short link)\n- `platform_url`: provide `url` (Apple Podcasts, Spotify episode,\n                  SoundCloud, Vimeo, Twitch VOD, Loom, ...)\n- `external_url`: provide `url` (any public direct audio/video URL;\n                  private/loopback/link-local/internal hosts rejected)\n- `upload`: provide `upload_id` and `duration_seconds`\n            (after POST /uploads presigned PUT, or POST /uploads/tus)\n\nField requirements per source:\n- `youtube`, `platform_url` and `external_url` require `url`.\n  `duration_seconds` optional; when omitted, we probe (slower).\n- `upload` requires `upload_id` and `duration_seconds`.\nServer-side validation rejects mismatched combinations with a\n400 + `error.code: invalid_request`.\n","properties":{"source":{"type":"string","enum":["youtube","external_url","platform_url","upload"],"description":"Input mode. `youtube` for any youtube.com / youtu.be URL.\n`external_url` for any public direct-media URL (mp3, mp4,\nDrive/Dropbox/iCloud share; private/loopback/link-local/internal\nhosts rejected). `platform_url` for hosted\nplatforms with a content URL (Apple Podcasts, Spotify\nepisode, SoundCloud track, Vimeo, Twitch VOD, Loom).\n`upload` for files uploaded via POST /uploads or /uploads/tus.\n"},"url":{"type":"string","format":"uri","description":"Required when `source` is `youtube`, `platform_url` or\n`external_url`. http(s) only; must be reachable without auth.\nFor `external_url` only: public URL, max 2048 chars; private,\nloopback, link-local and internal hosts, and URLs with\ncredentials, are rejected with 400 invalid_request; redirects\nare followed at most 10 hops, each checked the same way.\n","examples":["https://youtu.be/dQw4w9WgXcQ"]},"upload_id":{"type":"string","description":"Required when `source` is `upload`. The `upload_id` from\nPOST /uploads, or `tus/<last-path-segment>` for tus uploads.\n"},"original_filename":{"type":"string","description":"Optional. Only meaningful for `upload`. Stored as-is for\ndisplay in the dashboard.\n"},"duration_seconds":{"type":"number","description":"Audio/video duration in seconds. Required for `upload`\n(used to compute the wallet hold). Optional for URL sources;\nwe probe if omitted, but probing adds latency, so pass it\nwhen known.\n","minimum":0},"pipeline_code":{"type":"string","deprecated":true,"description":"**Deprecated — omit this field.** Every transcription runs on the\nstandard pipeline: timestamped segments, speaker labels, chapters,\nand Q&A are always included, and audio never leaves our\ninfrastructure for a third-party AI provider. Legacy values are\naccepted for backward compatibility and mapped to the current\npipeline; the response's `pipeline_code` echoes the effective\npipeline. Unknown codes are rejected with\n`error.code: unsupported_pipeline`.\n"},"language":{"type":"string","default":"auto","description":"BCP-47 language code (e.g., `en`, `ja`, `ko`) or `auto` for\nautomatic detection (the default).\n"},"addons":{"type":"array","maxItems":8,"items":{"type":"string"},"description":"Optional pipeline add-ons (post-quote audio augmentations). Each\nentry is the `code` of a row from the add-on catalog. Empty or\nunset == no add-ons (backward-compatible with pre-2026-05-28\ncallers). Validated against the catalog; unknown / incompatible\nentries return 400 with `error.code = \"addon_not_supported\"`.\n\nDiarization is included in the base price, so you do NOT need any\nadd-on to get speaker labels — every transcription returns them.\nHistorical diarization add-on codes are retired (folded into the\nbase price, $0 extra); passing one is accepted but changes\nnothing. Leave `addons` empty.\n","examples":[[]]},"callback_url":{"type":"string","format":"uri","maxLength":2048,"description":"Optional per-request webhook target. When set, this job's\n`transcription.completed` / `transcription.failed` event is\nPOSTed here with the same signed payload and headers as a\nregistered webhook (see the top-level `webhooks` section), in\naddition to any registered webhook, and regardless of whether\nthe caller uses an API key or an OAuth/MCP token. The signing\nsecret is returned in the 202 as `callback.secret` and is\ndeterministic per transcription (idempotent replays return the\nsame value); it is never returned by GET. Must be a public\nhttp(s) URL: private, loopback, link-local and internal\nhostnames are rejected with `invalid_request`. Failed\ndeliveries retry on the webhook backoff schedule (5 attempts)\nand never disable anything.\n","examples":["https://example.com/hooks/transcribe"]}}},"QuoteAddon":{"type":"object","required":["code","type","retail_usd_per_min","retail_usd_total"],"description":"Per-add-on price contribution. The top-level `retail_usd` already\nINCLUDES these — this array is informational, for receipts and the\ncost-preview UI.\n","properties":{"code":{"type":"string","description":"Add-on catalog code."},"type":{"type":"string","description":"Add-on family (`diarize`, `translate`, `vocab`, `enroll`, …)."},"retail_usd_per_min":{"type":"number"},"retail_usd_total":{"type":"number"}}},"Transcription":{"type":"object","required":["id","status","stage","pipeline_code","language","source"],"properties":{"id":{"type":"integer","description":"Plain integer. Doubles as the dashboard URL segment\n(`https://transcribe.so/transcriptions/<id>`).\n","examples":[4821]},"status":{"type":"string","description":"`pending`/`quoted` before start, `queued` while waiting for a\nconcurrency slot, `processing` while running, then `completed`,\n`failed`, or `cancelled`.\n","enum":["pending","quoted","queued","processing","completed","failed","cancelled"]},"stage":{"type":"string","examples":["queued","downloading","transcribing","sectionalizing","summarizing","completed","error"]},"progress":{"type":"integer","minimum":0,"maximum":100},"pipeline_code":{"type":"string"},"language":{"type":"string"},"detected_language":{"type":["string","null"]},"source":{"type":["string","null"],"description":"Input source. `realtime` marks rows created by the realtime\nsessions API (not submittable via POST /transcriptions).\n","enum":["youtube","external_url","platform_url","upload","realtime",null]},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"external_url":{"type":["string","null"],"description":"Set when `source` is `external_url`. Null otherwise.\n"},"youtube_url":{"type":["string","null"],"description":"Set when `source` is `youtube`. Null otherwise. NOTE: every\nchapter / section / qna citation / posting_chapters item\nalready carries a pre-computed `url` field with the canonical\ntimestamp deep-link — use those VERBATIM in markdown links\nrather than rebuilding from this base URL. This field exists\nfor cases where you need the bare video URL without a\ntimestamp, or when you want to direct the user to the source\nwithout anchoring to a specific moment. Canonical timestamp\nformat if you ever do build one yourself: append `?t=<sec>`\n(bare integer seconds, e.g.\n`https://youtu.be/RdAY3DjSr3A?t=1121`). If the base URL\nalready has a query string, use `&t=<sec>`. Avoid HH:MM:SS\n(`?t=00:18:41`), the legacy `&start=` parameter, and the\n`?t=Ns` suffix form — `?t=Ns` was documented as valid but\ndoes not reliably seek when the link opens through ChatGPT's\nredirector (verified 2026-05-25).\n"},"duration_seconds":{"type":["number","null"],"description":"Source media duration in seconds (server-probed via yt-dlp /\nffprobe / TUS-upload ffprobe — the caller-supplied value is\nadvisory only and replaced by the probe).\n"},"charge_usd":{"type":["number","null"],"description":"Final customer charge in USD (2dp). Server-derived from\nduration_seconds via ceil(N/60) × pipeline retail rate ×\ntier markup. Caller-supplied values are ignored. Note: on\nGET responses `billed_minutes` and `retail_usd` are NOT\npresent (only the 202 create/retry responses carry them);\nderive locally if needed:\n`billed_minutes = ceil(duration_seconds/60)`.\n"},"error":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"processing_started_at":{"type":["string","null"],"format":"date-time"},"processing_completed_at":{"type":["string","null"],"format":"date-time"},"created_via_api_key_id":{"type":["string","null"],"format":"uuid"},"callback_url":{"type":["string","null"],"format":"uri","description":"Per-request webhook target set at create time (see\n`CreateTranscriptionRequest.callback_url`). Null when none was\nsupplied. The signing secret is never returned here.\n"}}},"CreateTranscriptionResponse":{"type":"object","description":"202 body of POST /transcriptions. A slim acknowledgement, not the\nfull Transcription object: fetch that with GET /transcriptions/{id}.\n","required":["id","status","stage","pipeline_code","language","source","duration_seconds","billed_minutes","retail_usd","addons","callback"],"properties":{"id":{"type":"integer","examples":[4821]},"status":{"type":"string","enum":["processing","queued"]},"stage":{"type":"string","enum":["queued"]},"pipeline_code":{"type":"string","examples":["standard"]},"language":{"type":"string","description":"Requested language, `auto` when omitted."},"source":{"type":"string","enum":["youtube","external_url","platform_url","upload"]},"url":{"type":"string","description":"Echoed for URL sources."},"upload_id":{"type":"string","description":"Echoed when source is `upload`."},"duration_seconds":{"type":"number"},"billed_minutes":{"type":"number"},"retail_usd":{"type":"number","description":"Amount held against the wallet (USD)."},"addons":{"type":"array","description":"Add-ons applied to this job. Empty array when none were\nrequested (the common case).\n","items":{"$ref":"#/components/schemas/QuoteAddon"}},"callback":{"oneOf":[{"$ref":"#/components/schemas/TranscriptionCallback"},{"type":"null"}],"description":"Present (non-null) only when the request carried\n`callback_url`. Store `secret` to verify the signed callback.\n"}}},"TranscriptionCallback":{"type":"object","required":["url","secret"],"description":"Per-request callback details echoed on the 202. `secret` is the\n`whsec_...` HMAC key for this transcription's callback deliveries\n(same `X-Transcribe-Signature` scheme as registered webhooks). It is\ndeterministic per transcription and never returned by GET.\n","properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string","examples":["whsec_exampleexampleexampleexample"]}}},"RetryResponse":{"type":"object","description":"202 body of POST /transcriptions/{id}/retry.","required":["id","status","stage","billed_minutes","retail_usd"],"properties":{"id":{"type":"integer"},"status":{"type":"string","enum":["processing","queued"]},"stage":{"type":"string","enum":["queued"]},"billed_minutes":{"type":"number"},"retail_usd":{"type":"number"}}},"DeleteTranscriptionResponse":{"type":"object","required":["id","deleted"],"properties":{"id":{"type":"integer"},"deleted":{"type":"boolean","enum":[true]}}},"WebhookTranscription":{"type":"object","description":"Snapshot of the transcription carried in `transcription.*` events.\nUses `completed_at` (not `processing_completed_at`) and omits\n`progress`, `description`, `external_url`, `youtube_url`.\n","required":["id","status","stage"],"properties":{"id":{"type":"integer"},"status":{"type":"string","enum":["completed","failed"]},"stage":{"type":"string"},"pipeline_code":{"type":"string"},"language":{"type":["string","null"]},"detected_language":{"type":["string","null"]},"source":{"type":["string","null"],"enum":["youtube","external_url","platform_url","upload","realtime",null]},"title":{"type":["string","null"]},"duration_seconds":{"type":["number","null"]},"charge_usd":{"type":["number","null"]},"error":{"type":["string","null"]},"created_at":{"type":["string","null"],"format":"date-time"},"processing_started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}}},"WebhookEvent":{"type":"object","description":"Body of every webhook delivery. Verify `X-Transcribe-Signature`\nover the raw bytes before parsing.\n","required":["id","event","created","data"],"properties":{"id":{"type":"string","description":"Delivery id, `evt_<delivery_id>`. Stable across retries; use it to dedupe.","examples":["evt_1042"]},"event":{"type":"string","enum":["transcription.completed","transcription.failed","clip.completed","clip.failed","webhook.test"]},"created":{"type":"integer","description":"Unix seconds when this delivery attempt was sent."},"data":{"type":"object","description":"For `transcription.*` events: `{ transcription: WebhookTranscription | null }`.\nFor `clip.*` events: `{ clip: Clip | null }` (mp4_url presigned per attempt).\nFor `webhook.test`: `{ message: string, delivered_at: date-time }`.\n","properties":{"transcription":{"oneOf":[{"$ref":"#/components/schemas/WebhookTranscription"},{"type":"null"}]},"clip":{"oneOf":[{"$ref":"#/components/schemas/Clip"},{"type":"null"}]},"message":{"type":"string"},"delivered_at":{"type":"string","format":"date-time"}}}}},"CreateClipRequest":{"type":"object","required":["start_seconds","end_seconds"],"description":"Body of POST /transcriptions/{id}/clips. The transcription must be\n`completed` with word timestamps and the range must contain speech.\n","properties":{"start_seconds":{"type":"number","minimum":0,"description":"Clip start within the transcription, seconds.","examples":[12.5]},"end_seconds":{"type":"number","description":"Clip end, seconds. Length (end - start) must be 1-60 s and end\nmust not exceed the transcription duration (+1 s tolerance).\n","examples":[42.5]},"aspect":{"type":"string","enum":["9:16","1:1","16:9"],"default":"9:16","description":"Output aspect ratio (720x1280, 720x720, 1280x720)."},"style":{"type":"string","enum":["captions","karaoke","minimal"],"default":"captions","description":"Caption style: `captions` (pill + word pop-in), `karaoke`\n(active word highlighted), `minimal` (plain bottom text).\n"},"title":{"type":["string","null"],"maxLength":120,"description":"Optional title rendered at the top of the clip."},"callback_url":{"type":"string","format":"uri","maxLength":2048,"description":"Optional public http(s) URL that receives this clip's signed\n`clip.completed` / `clip.failed` webhook. Same rules as\nCreateTranscriptionRequest.callback_url.\n"}}},"Clip":{"type":"object","description":"A captioned clip render. `mp4_url` is a presigned download URL\n(valid until `mp4_url_expires_at`, 1 hour) present only when\n`status` is `completed`; every GET presigns a fresh one.\n","required":["id","transcription_id","status","start_seconds","end_seconds","duration_seconds","aspect","style","title","charge_usd","mp4_url","mp4_url_expires_at","error","created_at","completed_at"],"properties":{"id":{"type":"integer","examples":[42]},"transcription_id":{"type":"integer","examples":[4821]},"status":{"type":"string","enum":["queued","rendering","completed","failed"]},"start_seconds":{"type":"number"},"end_seconds":{"type":"number"},"duration_seconds":{"type":["number","null"],"description":"Rendered length in seconds once completed."},"aspect":{"type":"string","enum":["9:16","1:1","16:9"]},"style":{"type":"string","enum":["captions","karaoke","minimal"]},"title":{"type":["string","null"]},"charge_usd":{"type":"number","description":"Flat charge, $0.05 per started 60 s of clip.","examples":[0.05]},"mp4_url":{"type":["string","null"],"format":"uri"},"mp4_url_expires_at":{"type":["string","null"],"format":"date-time"},"error":{"type":["string","null"],"description":"Failure reason when `status` is `failed`."},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}}},"CreateClipResponse":{"allOf":[{"$ref":"#/components/schemas/Clip"},{"type":"object","required":["callback_secret"],"properties":{"callback_secret":{"type":["string","null"],"description":"`whsec_...` HMAC key for this clip's callback deliveries when\n`callback_url` was given (deterministic per clip, never\nreturned by GET); null otherwise.\n","examples":["whsec_exampleexampleexampleexample"]}}}]},"ClipsListResponse":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Clip"}}}},"Section":{"type":"object","description":"Fine-grained section of a transcript (sub-chapter unit).\n","properties":{"id":{"type":"integer"},"section_index":{"type":"integer"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"start_seconds":{"type":["number","null"]},"end_seconds":{"type":["number","null"]},"segment_count":{"type":["integer","null"]},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link to the source platform at\nthis section's start time. For YouTube sources:\n`https://youtu.be/<id>?t=<sec>` (bare integer, matches\nYouTube's share-dialog format). Null for sources without\ntimestamp deep-link support (uploads, generic external_url).\nRender markdown links VERBATIM as `[mm:ss](<url>)`; never\nrebuild it from start_seconds + youtube_url yourself — LLM-\nassembled URLs are inconsistent (mixing `?t=Ns`,\n`?t=H:MM:SS`, and `&start=N` variants, none of which reliably\nseek through ChatGPT's link redirector).\n"}}},"PostingChaptersStyleEntry":{"type":"object","description":"One style-keyed entry inside a chapter-list variant cache\n(`standard`, `highlights`, `clips`). The parent object is keyed\nby style; only `balanced` is generated today.\n","properties":{"items":{"type":"array","items":{"type":"object","properties":{"start_ms":{"type":"integer"},"title":{"type":"string"},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link to the source\nplatform at start_ms. See Section.url for shape +\nrendering rules. Render VERBATIM.\n"}}}},"generated_at":{"type":"string","format":"date-time"},"style":{"type":"string","enum":["balanced"]}}},"QuotedSectionsStyleEntry":{"type":"object","description":"One style-keyed entry inside the `quoted_sections` variant cache.\nItems carry an editorialized `insight_title` plus a verbatim\n`quote` instead of a plain `title`.\n","properties":{"items":{"type":"array","items":{"type":"object","properties":{"start_ms":{"type":"integer"},"insight_title":{"type":"string"},"quote":{"type":"string","description":"Verbatim transcript quote."},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link at start_ms. See\nSection.url. Render VERBATIM.\n"}}}},"generated_at":{"type":"string","format":"date-time"},"style":{"type":"string","enum":["balanced"]}}},"TranscriptionResult":{"allOf":[{"$ref":"#/components/schemas/Transcription"},{"type":"object","properties":{"included":{"type":"array","description":"Echoes which result sections were included in this\nresponse. Lets clients tell at a glance whether they got\nthe trimmed default (`[\"chapters\",\"sections\",\"qna\"]`)\nor opted in to verbatim segments + posting_chapters.\nSorted alphabetically.\n","items":{"type":"string","enum":["chapters","acts","sections","qna","segments","posting_chapters"]}},"segments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"segment_index":{"type":"integer"},"start_seconds":{"type":"number"},"end_seconds":{"type":"number"},"start_ms":{"type":"integer","description":"Integer milliseconds — same clock as `start_seconds`,\nno rounding loss.\n"},"end_ms":{"type":"integer"},"text":{"type":"string"},"speaker":{"type":["string","null"]}}}},"chapters":{"type":"array","description":"User-facing scannable chapter list (8-30 LLM-curated\nitems by default). Each item carries a pre-computed\n`url` field — the canonical clickable deep-link to the\nsource platform at the chapter's start time. Render\nmarkdown as `[mm:ss](<url>)` using `url` VERBATIM.\n","items":{"type":"object","properties":{"id":{"type":"integer"},"chapter_index":{"type":"integer"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"start_seconds":{"type":["number","null"]},"end_seconds":{"type":["number","null"]},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link. See Section.url.\nRender VERBATIM as `[mm:ss](<url>)`.\n"}}}},"acts":{"type":"array","description":"Per-act narrative summaries (3-15 items, ~3-5 min each)\nfrom `transcript_chapters`. Used for show notes / RSS\nexport. Same wire shape as `chapters`. Opt-in via\n`?include=acts`.\n","items":{"type":"object","properties":{"id":{"type":"integer"},"chapter_index":{"type":"integer"},"title":{"type":["string","null"]},"summary":{"type":["string","null"]},"start_seconds":{"type":["number","null"]},"end_seconds":{"type":["number","null"]},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link. See Section.url.\n"}}}},"sections":{"type":"array","description":"Fine-grained section-level entries (8–50 per video).\nSub-chapter unit in the book hierarchy (Chapter → Section).\n","items":{"$ref":"#/components/schemas/Section"}},"posting_chapters":{"type":"object","description":"LLM-curated paste-ready chapter cache, keyed by variant\nthen by style. Present only when `?include=posting_chapters`\nwas requested. Only the `balanced` style is generated; a\nvariant is null until the worker (or a regenerate) fills it.\n","properties":{"standard":{"type":"object","description":"8-30 item navigable chapter list (the default for \"give me the chapters / outline / TOC\").","additionalProperties":{"$ref":"#/components/schemas/PostingChaptersStyleEntry"}},"highlights":{"type":"object","description":"5-item condensed TEXT outline for ultra-tight surfaces\n(Instagram caption first line, TikTok pinned comment,\nX/Twitter bio — 150-280 char budgets). Titles ≤25 chars.\nNOT for video clips — use the `clips` variant for that.\n","additionalProperties":{"$ref":"#/components/schemas/PostingChaptersStyleEntry"}},"clips":{"type":"object","description":"3 VIDEO clip ideas for short-form video (Instagram Reels /\nTikTok / YouTube Shorts). Each item is a self-contained\nhook-driven moment a creator would clip into a 30-60s\nshort. Default for \"give me clip ideas / Reel ideas /\nTikTok ideas / Shorts ideas / viral moments\".\n","additionalProperties":{"$ref":"#/components/schemas/PostingChaptersStyleEntry"}},"quoted_sections":{"type":"object","description":"8–12 social-post items with editorialized insight\ntitle + verbatim quote. Item shape differs from the\nchapter-list variants — each item carries\n`{start_ms, insight_title, quote, url}` instead of\n`{start_ms, title, url}`. Pre-generated for `balanced`.\n","additionalProperties":{"$ref":"#/components/schemas/QuotedSectionsStyleEntry"}}}},"qna":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"question":{"type":"string"},"answer":{"type":["string","null"]},"citations":{"type":"array","description":"Sections the question was grounded against (the\nretrieval context). Use `start_seconds` for\ntimestamp deep-links. Q&A rows do not carry\ntime anchors of their own.\n","items":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":["string","null"]},"start_seconds":{"type":["number","null"]},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link. See\nSection.url. Render VERBATIM as\n`[mm:ss](<url>)`.\n"}}}},"answer_citations":{"type":"array","description":"Sections the answer text actually drew from (subset\nof `citations` after the LLM picks). Same shape.\n","items":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":["string","null"]},"start_seconds":{"type":["number","null"]},"url":{"type":["string","null"],"description":"Pre-computed clickable deep-link. See\nSection.url. Render VERBATIM as\n`[mm:ss](<url>)`.\n"}}}},"created_at":{"type":"string","format":"date-time"}}}}}}]},"ApiWord":{"type":"object","required":["text","start_ms","end_ms","segment_id","word_index","speaker","punctuation"],"description":"One word with millisecond timings. snake_case (unlike the\n`/subtitles?format=json` exporter, which is camelCase).\n","properties":{"text":{"type":"string","examples":["Welcome"]},"start_ms":{"type":"integer","examples":[0]},"end_ms":{"type":"integer","examples":[420]},"segment_id":{"type":"integer","description":"Owning `transcript_segments` row.","examples":[88213]},"word_index":{"type":"integer","description":"Position of the word within its segment.","examples":[0]},"speaker":{"type":["string","null"],"description":"Speaker label of the owning segment, when diarized.","examples":["A"]},"punctuation":{"type":["string","null"],"description":"Trailing punctuation attached to the word, if any.","examples":[","]}}},"WordsResponse":{"type":"object","required":["id","status","granularity","available","reason","language","count","offset","limit","has_more","words"],"properties":{"id":{"type":"integer","examples":[4821]},"status":{"type":"string","examples":["completed"]},"granularity":{"type":["string","null"],"description":"Raw `timestamp_granularity` set at creation (`word` | `segment` |\nnull). Metadata only; `available` is the source of truth.\n","examples":["word"]},"available":{"type":"boolean","description":"True when the transcription has word timestamps."},"reason":{"type":["string","null"],"enum":["not_completed","no_word_timestamps",null],"description":"Why `available` is false; null when available."},"language":{"type":["string","null"],"examples":["en"]},"count":{"type":"integer","description":"Total words for the transcription (0 when unavailable).","examples":[1832]},"offset":{"type":"integer"},"limit":{"type":"integer"},"has_more":{"type":"boolean"},"words":{"type":"array","items":{"$ref":"#/components/schemas/ApiWord"}}},"examples":[{"id":4821,"status":"completed","granularity":"word","available":true,"reason":null,"language":"en","count":1832,"offset":0,"limit":2,"has_more":true,"words":[{"text":"Welcome","start_ms":0,"end_ms":420,"segment_id":88213,"word_index":0,"speaker":"A","punctuation":null},{"text":"back","start_ms":420,"end_ms":640,"segment_id":88213,"word_index":1,"speaker":"A","punctuation":null}]}]},"AskRequest":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":1,"maxLength":500,"description":"The question (any language; the answer follows the transcript's language).","examples":["What did the guest say about pricing?"]},"top_k":{"type":"integer","minimum":10,"maximum":100,"default":60,"description":"Hybrid-retrieval candidates before rerank."}}},"AskCitation":{"type":"object","required":["marker","section_id","transcription_id","title","start_seconds","url","quote"],"description":"One cited transcript section. `marker` matches the inline `[N]` in\n`answer`. `url` is the deep-link to the source at that timestamp\n(YouTube `?t=<sec>` etc.) or the transcribe.so dashboard link with\n`?t=<sec>` when the source has no per-second anchor.\n","properties":{"marker":{"type":"integer","examples":[1]},"section_id":{"type":"integer","examples":[88213]},"transcription_id":{"type":"integer","examples":[4821]},"title":{"type":["string","null"],"description":"Section title (per-transcript ask) or source transcript title (library ask)."},"start_seconds":{"type":["number","null"],"examples":[1121.5]},"url":{"type":["string","null"],"examples":["https://youtu.be/dQw4w9WgXcQ?t=1121"]},"quote":{"type":"string","description":"Short verbatim excerpt of the cited content."}}},"AskResponse":{"type":"object","required":["answer","no_answer","citations","qna_id","remaining_per_transcript","remaining_per_day","model"],"properties":{"answer":{"type":"string","description":"Markdown answer with inline `[N]` markers matching `citations[].marker`."},"no_answer":{"type":"boolean","description":"True when no relevant context was found. Nothing persisted, no allowance consumed."},"citations":{"type":"array","items":{"$ref":"#/components/schemas/AskCitation"}},"qna_id":{"type":["integer","null"],"description":"Persisted Q&A row id (also visible via /result?include=qna); null on `no_answer`."},"remaining_per_transcript":{"type":["integer","null"],"description":"Questions left on this transcript AFTER this call; null = unlimited (paid plans)."},"remaining_per_day":{"type":["integer","null"],"description":"Questions left in the rolling 24h window AFTER this call; null = unlimited."},"model":{"type":["string","null"],"description":"Public pipeline alias of the answering model; null on `no_answer`.","examples":["standard"]}}},"AskLibraryRequest":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":1,"maxLength":500,"examples":["Across my interviews, what pricing objections came up?"]},"transcription_ids":{"type":"array","maxItems":50,"items":{"type":"integer","minimum":1},"description":"Restrict retrieval to these transcription ids. Omit for the whole library."}}},"AskLibraryResponse":{"type":"object","required":["answer","no_answer","citations","qna_id","remaining_today","saved"],"properties":{"answer":{"type":"string"},"no_answer":{"type":"boolean"},"citations":{"type":"array","items":{"$ref":"#/components/schemas/AskCitation"}},"qna_id":{"type":["integer","null"],"description":"library_qna row id; null when not saved."},"remaining_today":{"type":["integer","null"],"description":"Questions left in the rolling 24h window AFTER this call; null = unlimited."},"saved":{"type":"boolean","description":"False when nothing was persisted (no_answer, or the history insert failed); no allowance consumed then."}}},"Quote":{"type":"object","required":["transcription_id","billed_minutes","retail_usd","expires_at"],"properties":{"transcription_id":{"type":"integer","description":"Identifies the quoted row only. The subsequent\ncreateTranscription operation returns a DIFFERENT id, and only\nthat id is valid for getTranscription / waitForTranscription /\ngetTranscriptionResult. Do not pass this quote id to those\noperations.\n","examples":[4821]},"duration_seconds":{"type":"number"},"billed_minutes":{"type":"number"},"retail_usd":{"type":"number"},"retail_usd_per_min":{"type":"number"},"retail_usd_per_hour":{"type":"number"},"subscription_tier":{"type":"string"},"pipeline_code":{"type":"string"},"language":{"type":"string"},"title":{"type":["string","null"]},"detected_language":{"type":["string","null"]},"expires_at":{"type":"string","format":"date-time"},"addons":{"type":"array","description":"Add-on contributions to this quote. Empty array when no add-ons\nwere requested. Top-level `retail_usd` already includes these.\n","items":{"$ref":"#/components/schemas/QuoteAddon"}}}},"Webhook":{"type":"object","required":["id","url","events","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["transcription.completed","transcription.failed","clip.completed","clip.failed"]}},"consecutive_failures":{"type":"integer"},"disabled_at":{"type":["string","null"],"format":"date-time"},"last_delivery_at":{"type":["string","null"],"format":"date-time"},"last_success_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}}}}