transcribe.so
Find who said what, and exactly when, in any video, podcast or meeting.
Community: Submitted by a user or imported; check the owner before granting accessOnlineNo sign-inGlobalFreeRead-only
What it can do
- GetAccount: Returns the user's wallet balance, subscription tier, effective plan limits (single-file duration cap, upload size cap, concurrent jobs), and dashboard/billing/docs links. Call this when t
- ListPipelines: Returns the transcription capability catalog: the standard pipeline with per-minute and per-hour pricing in USD, supported languages (each with a published accuracy band in `benchmark_e
- ListTranscriptions: Lists the user's transcriptions, newest first. Cursor-paginated: pass `next_cursor` from the previous response back as `cursor` to fetch the next page. `limit` defaults to 50, max
What data it sees
Do you need an account
No: the server works without sign-in
Find who said what, and exactly when, in any video, podcast or meeting. Transcribe YouTube links, podcast links, direct file URLs or uploads into speaker-labelled, timestamped transcripts. Then search across transcripts, ask cited questions, and export chapters, subtitles (SRT/VTT), social captions and captioned clips. Quotes are free, and your agent shows the price and waits for your approval before starting a paid job. Sign in with OAuth, or use an API key.
Server tool list (23)
Raw names from tools/list. Only developers need these.
| getAccount | Returns the user's wallet balance, subscription tier, effective plan limits (single-file duration cap, upload size cap, concurrent jobs), and dashboard/billing/docs links. Call this when the user asks about their balance, subscription, plan limits, or where to top up. The `links.billing` URL is the same one returned on insufficient_funds errors — surface it proactively if balance is low. |
| listPipelines | Returns the transcription capability catalog: the standard pipeline with per-minute and per-hour pricing in USD, supported languages (each with a published accuracy band in `benchmark_error_rate_band`), and timestamp capability metadata (`word_timestamp_languages`, `timestamp_options` — pipeline capabilities; the API/MCP result surfaces return sentence-level segments today). Every transcription includes speaker labels (diarization), timestamped segments, and AI analysis in the base price. Informational only — `transcribe` requires no pipeline selection. |
| listTranscriptions | Lists the user's transcriptions, newest first. Cursor-paginated: pass `next_cursor` from the previous response back as `cursor` to fetch the next page. `limit` defaults to 50, max 200. Set `api_only=true` to filter to only transcriptions created via API (excludes dashboard-created ones). Returns `data[]` (transcription summaries — id, status, title, duration, etc.) and `next_cursor` (null if last page). |
| getTranscription | Returns a single transcription by id (status, stage, progress, title, duration, source URL). Does NOT include chapters/acts/sections/qna/segments — use `getTranscriptionResult` for those, or `waitForTranscription` to long-poll until completion. Errors with `not_found` if the id doesn't belong to the authenticated user. |
| getTranscriptionResult | Returns the full result for a completed transcription: the transcription row PLUS the requested sections. Default `include` is chapters+sections+qna. `chapters` returns the scannable chapter list (8-30 LLM-curated items — the same list rendered in the web UI and the canonical answer to 'what are the chapters'); for the long-form per-act narrative summaries used in show notes / RSS, request `acts` instead (3-15 items). Pass `["all"]` for everything including verbatim segments (large — may be 100KB+). Every chapter / section / qna citation / posting_chapters item carries a pre-computed `url` field — the canonical clickable deep-link to the source platform at that timestamp. Render markdown links as `[mm:ss](<url>)` using `url` VERBATIM; never assemble URLs yourself. Use `qna[].citations[].start_seconds` and `sections[].start_seconds` for accurate timestamps; never fabricate them. Every pipeline produces speaker labels (diarization is universal), so each section normally carries `primary_speaker` (majority-vote speaker label — host vs guest) regardless of pipeline, plus `is_ad` (true for detected sponsor reads). Omit `is_ad: true` sections from summaries / quotes unless the user explicitly asks about ads or sponsors. |
| waitForTranscription | Long-polls until the transcription reaches a terminal state (`completed` or `failed`) OR the wait window elapses. The public /mcp endpoint sits behind Cloudflare, whose ~120s proxy-read timeout severs any longer hold with a 524 — so each call waits at most ~100s (default 90s), well under that wall. That CF read timeout, NOT Claude's 300s tool-call ceiling, is the binding constraint. On completion AND if `include` is set, the result sections are inlined so the caller skips a second tool call. If `_timed_out: true`, the job is still processing: call `waitForTranscription` again to keep waiting (each call covers another ~100s window) — this is the expected loop for jobs longer than the window, NOT an error, so don't treat it as a failure or retry blindly. For long jobs (after a few windows / a couple minutes of total wait), hand the user the dashboard link and let the completion email finish the job instead of polling indefinitely. |
| getTranscriptionTimestamps | Returns ready-to-paste chapter timestamps formatted for a destination platform. The `text` field is the paste-ready output — render it inside a fenced code block in your response so the user can one-click copy. NEVER assemble timestamp text yourself when the user wants something paste-ready; this endpoint enforces each platform's first-party rules (char budget, min spacing, HH:MM:SS for Apple). Output carries no transcribe.so promotion unless you pass `cta: true`. Always relay `warnings[]` — it names every quote or title that had to be shortened. On `not_ready`, check `reason`: `transcription_processing` means wait and retry; `transcription_failed`, `transcription_cancelled` and `not_started` are terminal or never-started and are NOT retryable — report them; `artifact_missing` means call `regeneratePostingChapters` once, then retry; never loop. |
| getTranscript | Returns the transcript of a completed transcription as one text blob, built from every segment — the source is never capped the way getTranscriptionResult include=segments is windowed. The INLINE `content` is a different matter: it is truncated at this tool's response budget, and when `truncated` is true what you hold is a PREFIX, not the transcript — only `download_url` (the same bytes via GET /api/v1/transcriptions/{id}/transcript, your Bearer key) is complete, so never summarize or quote-check a truncated blob as if it were the whole recording. `format=txt` is plain text, one paragraph per speaker turn, no title and no table of contents; `format=md` is Markdown with the title, chapter headings and speaker turns. `speaker_labels` and `timestamps` are independent booleans (both default true). The result carries `char_count` and `segment_count` so you can tell the user how much you are holding, and `truncated` plus `download_url` when the text is larger than this tool's inline budget — fetch that URL with the same Bearer key for the rest. Requires status `completed`: otherwise `not_ready` with `reason`. Only `transcription_processing` is worth waiting on (wait via waitForTranscription, then retry). `transcription_failed`, `transcription_cancelled` and `not_started` (priced but never started) are terminal for this call — report them, do not poll. `artifact_missing` = the transcription stored no segments. |
| searchSegments | Answers "who said X, and when". Case-insensitive substring search over the transcript segments of ALL the user's completed transcriptions (or only `transcription_ids` when given) — not a recent-N sample, so an old recording is found too. Each hit is the segment itself: `{transcription_id, title, segment_id, speaker, start_ms, end_ms, text, url}`. Quote and attribute from that hit's own `text` and `speaker`; a `speaker` of null means unknown, and labels are recording-local, never identified people. Page with `limit` / `offset` until `has_more` is false; `total` is null (no exact count is computed — `has_more` is the signal, not a page count). `query` must be at least 2 characters and `offset` at most 1000. Read-only and never billed. This is the segment-level tool — the separate `search` / `fetch` pair is the ChatGPT connector's document-level contract. |
| getTranscriptionWords | Paginated word-level timings (milliseconds) for a completed transcription, each with the owning segment's speaker. Always succeeds when the transcription exists: `available:false` with `reason` (`not_completed` | `no_word_timestamps`) and an empty `words` array when timings are not there. Page with `offset` / `limit` until `has_more` is false; `count` is the total. Use for karaoke / word-highlight captions, Remotion / video overlays, or precise quote timing. For a ready-made subtitle file call `getSubtitles` instead. |
| getSubtitles | Returns a subtitle file for a completed transcription as `content` (SRT, VTT, karaoke VTT with per-word timing, or JSON cues), plus `mime_type`, `filename`, and `download_url` (the REST endpoint with the same query; needs the same Bearer key). `content` is capped at 60,000 characters: when `truncated` is true, hand the user `download_url` or page through `getTranscriptionWords`. `preset` controls line length / cues-per-second rules (youtube default; tiktok-shorts and instagram-reels for short vertical video). `mode` auto = word-timed cues when word timestamps exist, else sentence cues; `mode=word` errors when no word timestamps. `source` auto = the user's edited cues from the app when they exist AND no preset/mode was asked for; any explicit preset or mode regenerates. Requires status `completed` (`not_ready` otherwise; wait with `waitForTranscription`). |
| getQuote | Prices a transcription WITHOUT starting it or charging anything. Same inputs as `transcribe`; returns `duration_seconds`, `billed_minutes`, `retail_usd`, per-minute/per-hour rates, the detected `title` / `detected_language`, and a `transcription_id` in quoted state (informational — `transcribe` creates and starts its own row; the quote row expires on its own). Use when the user asks 'how much would this cost' or wants a preview (title / length) before committing. For URL sources the server probes the media, so this can take a few seconds. Creates a quote row in your library (expires; not billed) so it can be started later with transcribe. |
| search | Keyword search over the user's own library: matches transcription titles and transcript text (case-insensitive substring; not semantic). Returns `results[]` of `{id, title, url}` where `id` is the transcription id as a string — pass it to `fetch`, or to the numeric-id tools after `Number(id)`. Transcript-text hits carry a `url` with `?t=<sec>` at the matching segment. This tool + `fetch` implement the ChatGPT connectors / deep-research contract; for structured chapters / sections / Q&A / timestamps prefer `getTranscriptionResult` and `getTranscriptionTimestamps`. |
| fetch | Returns one transcription as a single document `{id, title, text, url, metadata}` — the ChatGPT connectors / deep-research contract (pair with `search`). `text` is the section text when AI sections exist, else the speaker-labelled segments joined as `[mm:ss] Speaker: text` lines, capped at 40k characters (`metadata.truncated: true` when cut — call `getTranscriptionResult` with `include=["segments"]` or `["all"]` for the full structured result). `id` is the string id from `search` (a plain integer as string is also fine). |
| getClip | Returns one captioned clip render created by `renderClip` (status queued | rendering | completed | failed, the range, aspect, style, title, charge_usd, error). Once `completed` it carries `mp4_url`, a presigned download link valid for one hour (`mp4_url_expires_at`; call again for a fresh one). Pass `wait_seconds` (1-45) to long-poll until the clip is completed/failed or the window elapses (`_timed_out: true` means still rendering: call again). Renders take several times the clip length on the production fleet (a 60 s clip can take 5-8 minutes), so expect to call again more than once. Errors with `not_found` when the clip or transcription is not the user's. |
| transcribe | Starts a transcription and charges the wallet. Collapses the two-step quote+start dance from /api/v1/* into a single call (call `getQuote` first if the user wants the price before committing). `source` must be one of: `youtube` (pass `url`), `external_url` (direct media file URL; pass `url`, optionally `duration_seconds` to skip the server-side probe), `platform_url` (Apple Podcasts / SoundCloud / Vimeo / Twitch / Loom and similar page URLs; pass `url`), or `upload` (pass `upload_id` from `createUpload` + `duration_seconds`). Every transcription runs on the standard pipeline and includes speaker labels (diarization), timestamped segments, and AI analysis — nothing to select. Returns `id` immediately; follow up with `waitForTranscription` for short clips, or just hand off the dashboard URL for long ones (transcripts also email the user when done). On `insufficient_funds` the response includes a /billing URL — surface it to the user. Local files: `createUpload` -> PUT bytes -> `transcribe` source=upload; chat-only clients that cannot PUT should send the user to a public URL or the dashboard at https://transcribe.so/transcriptions. Optional `callback_url` (public http(s) endpoint the caller controls): on completion or failure transcribe.so POSTs a signed `transcription.completed` / `transcription.failed` webhook there (same payload/headers as a registered webhook; the per-transcription signing secret comes back in the result as `callback.secret`), so an agent with its own endpoint can be notified instead of polling. Retries: pass `idempotency_key` and re-send the SAME key with the SAME arguments to replay the first result instead of charging twice. Without a key this tool is NOT idempotent — call waitForTranscription/listTranscriptions before retrying a timed-out call. |
| createUpload | Step 1 of the two-step local-file flow, for agents that can run HTTP requests from a shell or runtime (Claude Code, scripts, desktop agents). Returns a short-lived presigned S3 PUT URL: `{upload_id, upload_url, expires_in}` (expires_in is seconds, ~15 min). Then PUT the raw file bytes to `upload_url` with `Content-Type: <content_type>` (e.g. `curl -X PUT -H 'Content-Type: audio/mpeg' --data-binary @file.mp3 '<upload_url>'`), and finally call `transcribe` with `source=upload`, the same `upload_id`, and `duration_seconds` (probe the file locally, e.g. ffprobe). Chat-only clients that cannot PUT bytes should not call this — send the user to the dashboard instead. Same limits as POST /api/v1/uploads: 1 GiB max; content_type must be an allowlisted audio/video MIME (mpeg/mp3, wav, m4a/x-m4a, mp4, aac, ogg, webm, flac, video/mp4, video/webm, quicktime, x-msvideo). |
| renderClip | Renders a hosted, shareable MP4 clip of a range of a completed transcription: the audio segment over a dark branded background with word-by-word captions (audiogram style, no source video). Requires a `completed` transcription with word timestamps (check `getTranscriptionWords` available:true); the range must contain speech and be 1-60 seconds long. Charges the wallet a flat $0.05 per started 60 seconds of clip (a hold is taken now and settled when the render completes; released on failure). Returns the queued clip (`id`, `status: queued`, `charge_usd`); follow up with `getClip` + `wait_seconds` (renders take several times the clip length: a 60 s clip can take 5-8 minutes, so poll more than once) to obtain `mp4_url`. Good inputs come from `getTranscriptionTimestamps` variant=clips or quoted_sections (start_seconds/end_seconds of a moment). `aspect`: 9:16 (default, Reels/TikTok/Shorts), 1:1, 16:9. `style`: captions (default, pill + word pop-in), karaoke (active word highlighted), minimal (plain bottom text). Optional `title` (<= 120 chars) shows at the top. Optional `callback_url` receives a signed clip.completed / clip.failed webhook (secret in `callback_secret`). Retries: pass `idempotency_key` and re-send the SAME key with the SAME arguments to replay the first result instead of rendering (and charging) twice; without a key, call getClip before retrying a timed-out call. Errors: not_ready (transcription not completed), invalid_request (bad range / no words in range), insufficient_funds (top up the wallet), rate_limited (more than 10 clips in flight). |
| regeneratePostingChapters | (Re)generates the LLM-curated posting_chapters cache. Use when (a) the user supplies a free-text steer like 'focus on the case studies', or (b) the transcription predates the feature and `getTranscriptionTimestamps` returned `not_ready`. Latency 30–90 seconds — tell the user 'regenerating, ~1 minute' BEFORE calling. Capped at 10 regenerations per transcription. Do NOT call more than once per user request. |
| retryTranscription | Re-runs a previously failed transcription against the user's wallet. Only `failed` transcriptions can be retried; the original source URL/file and pipeline are reused. This is a NEW paid attempt: the ceiling you sent to `transcribe` does NOT carry over, so pass `max_charge_usd` again if the user authorized a budget. Returns the same shape as `transcribe` (id, status=processing or queued — `queued` means the account was at its concurrency limit and the job starts automatically when a slot frees, stage=queued, billed_minutes, retail_usd). Errors: `not_found` (wrong id or wrong owner), `invalid_request` (status is not failed), `insufficient_funds` (top up at /billing), `max_charge_exceeded` (the retry would charge more than `max_charge_usd`; nothing was charged). |
| askTranscription | Live LLM answer to a question about ONE completed transcription, grounded in its transcript sections (~10 seconds, non-streaming). Returns `answer` (markdown with inline [N] markers), `citations[]` ({marker, section_id, transcription_id, title, start_seconds, url, quote} — render timestamps with the given `url`), `no_answer` (true when nothing relevant was found: nothing saved, nothing consumed), `qna_id`, `remaining_per_transcript` / `remaining_per_day` (null = unlimited) and `model`. Each answered question consumes the user's DAILY Q&A ALLOWANCE, which is shared with the transcribe.so app (tier-based; not the wallet) — check `getTranscriptionResult` with include=["qna"] first for free, already-cached Q&A pairs. `qna_quota_exceeded` carries `scope` (transcript | day) and `retry_after`. Requires status `completed` (`not_ready` otherwise). |
| askLibrary | Live LLM answer to a question across the user's WHOLE transcript library, or across `transcription_ids` when given (~10 seconds, non-streaming). Same output shape as `askTranscription` — `answer` with [N] markers, `citations[]` ({marker, section_id, transcription_id, title, start_seconds, url, quote}; `title` is the source transcript's title), `no_answer`, `qna_id`, `remaining_today` (null = unlimited) and `saved` (false when nothing was persisted). Each saved answer consumes the user's DAILY Q&A ALLOWANCE, shared with per-transcript Q&A and the transcribe.so app (tier-based; not the wallet) — prefer `getTranscriptionResult` include=["qna"] when the question is about one known transcription and cached pairs may exist. `qna_quota_exceeded` carries `scope` and `retry_after`. |
| deleteTranscription | Deletes a transcription. The row disappears from every listing immediately; it is retained server-side for a short window (about 7 days) before the transcript, its derived artifacts, and stored audio are permanently purged. There is no API to undo a delete (the ~15-minute undo exists only in the web app), so treat this as destructive: ALWAYS confirm with the user before calling — quote the title or duration back to them so they're sure of which one. Returns `{id, deleted: true}` on success or `not_found` if the id doesn't belong to the authenticated user or is already deleted. |