transcribe.so
Find who said what, and exactly when, in any video, podcast or meeting.
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.
How to connect
How to connect transcribe.so to Claude
The server address to paste into Claude settings:
https://transcribe-so--sh-sunmoon-lee.run.tools
- Open Claude (claude.ai or the desktop app).
- Go to Settings → Connectors.
- Click Add custom connector.
- Paste the server address copied below into Remote MCP server URL and click Add.
- In a chat, click + → Connectors and switch the new connector on.
Custom connectors are available on Free, Pro, Max, Team and Enterprise plans (Free is limited to one). On Team and Enterprise an organization Owner adds the connector first under Organization settings → Connectors.
Authorization
After you click Add, a sign-in window for the service opens. Sign in with your own account and approve access. Claude never sees your password.
How to connect transcribe.so to ChatGPT
The server address to paste into ChatGPT settings:
https://transcribe-so--sh-sunmoon-lee.run.tools
- Open ChatGPT in a browser (chatgpt.com). A Plus, Pro, Business, Enterprise or Edu plan is required.
- Turn on developer mode once: Settings → Apps → Advanced settings → Developer mode.
- Open Settings → Connectors and click Create.
- Fill in the form: Name (anything), Description (one line about what the service does), MCP server URL (copy it below).
- Under Authentication choose OAuth if the service requires sign-in, otherwise None. Click Create.
- In a new chat open + → Apps/Connectors and enable the connector.
OpenAI has renamed this section before (Connectors → Apps/Plugins). If the label differs, search settings for "developer mode". On Business/Enterprise workspaces an admin must allow custom connectors first.
Authorization
On first use ChatGPT opens the service's sign-in window. Sign in and approve access.
How to connect transcribe.so to Cursor
The server address to paste into Cursor settings:
https://transcribe-so--sh-sunmoon-lee.run.tools
Fastest: click Open in Cursor below and confirm the prompt.
Manually:
- In Cursor open Settings → Cursor Settings → MCP and click Add new global MCP server.
- Paste the JSON copied below into
~/.cursor/mcp.json(per project:.cursor/mcp.jsonin the repo root). - Save the file. The server appears in the MCP list; authorize it there if asked.
Authorization
If the service needs sign-in, an authorize button appears next to the server in the MCP list.
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. |