askLibrary
ChatGPTLive 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, segment_start_seconds, segment_end_seconds, speaker}; title is the source transcript's title; the three segment fields are null on library answers), 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 and included with the account. 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.
askTranscription
ChatGPTLive 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, segment_start_seconds, segment_end_seconds, speaker}; segment_*_seconds locate the quoted line itself, speaker is its raw diarization label, all three null when unresolved; 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 and included with the account. Check getTranscriptionResult with include=["qna"] first for already-cached Q&A pairs, which do not consume the allowance. qna_quota_exceeded carries scope (transcript | day) and retry_after. Requires status completed (not_ready otherwise).
createUpload
ChatGPTStep 1 of the two-step local-file flow, for agents that can run HTTP requests from a shell or runtime (coding agents, 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).
deleteTranscription
ChatGPTDeletes 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.
fetch
ChatGPTReturns 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).
getAccount
ChatGPTReturns the user's account status: remaining balance, account tier, effective limits (single-file duration cap, upload size cap, concurrent jobs), and links to the dashboard, docs and support. Call it only when the user asks about their account, balance or limits; do not bring up the balance or tier unprompted.
getClip
ChatGPTReturns 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.
getClipQuote
ChatGPTPreviews a clip render WITHOUT rendering or charging anything: returns charge_usd, the amount renderClip would draw from the balance already on the user's account for this range, plus the normalized start_seconds, end_seconds and clip_seconds. Runs the same checks as renderClip (a completed transcription the user owns, a range 1-60 seconds long that contains word timestamps), so it doubles as a dry run of the range. Read-only: nothing is created, held or queued. Call it before renderClip, tell the user the charge_usd and wait for a go-ahead unless it is within a limit the user already gave, then pass the agreed amount to renderClip as max_charge_usd. It does not check the account balance or how many clips are already in flight; renderClip enforces those when it runs. Errors: not_found (not the user's transcription), not_ready (transcription not completed), invalid_request (bad range / no words in range).
getQuote
ChatGPTPreviews a transcription WITHOUT starting it or charging anything. Same inputs as transcribe; returns duration_seconds, billed_minutes, retail_usd (what the job would draw from the balance already on the user's account; 0 when the account's tier includes it), the applicable 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 wants to know the length, title or amount before committing. For URL sources the server probes the media, so this can take a few seconds. A 5xx result (e.g. internal_error or source_busy with retry_after) means nothing was started or charged: wait retry_after seconds and retry once; if it is refused the same way again, or you cannot wait that long, stop and tell the user. Creates a quote row in your library (expires; not billed) so it can be started later with transcribe.
getSubtitles
ChatGPTReturns 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).
getTranscript
ChatGPTReturns 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 (previewed but never started) are terminal for this call; report them, do not poll. artifact_missing = the transcription stored no segments.
getTranscription
ChatGPTReturns 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
ChatGPTReturns 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.
getTranscriptionTimestamps
ChatGPTReturns 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 link or mention 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.
getTranscriptionWords
ChatGPTPaginated 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.
listPipelines
ChatGPTReturns the transcription capability catalog: the standard pipeline with the per-minute rate applied to jobs on this account (informational; the amount a specific job would draw comes from getQuote), 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; transcribe requires no pipeline selection.
listTranscriptions
ChatGPTLists 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). Pass client_reference to find the jobs you tagged with that id at create time (exact match). Returns data[] (transcription summaries — id, status, title, duration, etc.) and next_cursor (null if last page).
regeneratePostingChapters
ChatGPT(Re)generates the LLM-curated posting_chapters cache. Replaces the previously cached chapters for that transcription. Use when (a) the user supplies a short 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.
renderClip
ChatGPTRenders 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. Paid action: draws on the balance already on the user's account for each started 60 seconds of clip (a hold is taken now and settled when the render completes; released on failure); the amount comes back as charge_usd. Before calling: call getClipQuote for the same range, tell the user its charge_usd and wait for a go-ahead unless it is within a limit the user already gave, then pass the agreed amount here as max_charge_usd; if the amount is above that ceiling the call is refused with max_charge_exceeded before anything is held, so nothing is drawn from the account and nothing is rendered. 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. Error results below 500 are replayed under that key for 24 hours, so use a NEW idempotency_key only after a refusal you actually received that says nothing was drawn, once its cause is fixed (e.g. max_charge_exceeded or insufficient_funds after the user resolved it, not_ready once the transcription has completed, rate_limited once clips already in flight have finished; check with getClip); sending CHANGED arguments after such a refusal is a new request and takes a new key. A not_ready that says a request with this key is already in flight is different: keep retrying the SAME key. If that answer carries stale: true (no result recorded after 120 seconds), a clip may already have been rendered and drawn: do NOT send a new key on your own; tell the user (the REST list GET /api/v1/transcriptions/{id}/clips shows the clips; getTranscription does not). A not_ready saying the earlier request just completed with an error means nothing was recorded under the key: retry, the same key is fine. If the same key is rejected because the arguments differ and you never got an answer to the first call, the first call may have gone through: re-send the SAME key with the original arguments to get that result back, or check getClip, before rendering again, and never switch to a new key just to get past that rejection. Errors: not_ready (transcription not completed), invalid_request (bad range / no words in range), insufficient_funds (the account balance does not cover the clip; nothing was charged), max_charge_exceeded (the clip amount is above max_charge_usd; nothing was charged; the error carries charge_usd and max_charge_usd; do not raise the ceiling yourself, report the amount to the user and ask), rate_limited (more than 10 clips in flight).
retryTranscription
ChatGPTRe-runs a previously failed transcription; the retry draws on the balance already on the user's account like a new job. Only failed transcriptions can be retried; the original source URL/file and pipeline are reused. This is a NEW attempt that draws on the account again: 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 (the balance does not cover the retry; nothing was charged), max_charge_exceeded (the retry would charge more than max_charge_usd; nothing was charged), queue_full (accounts with a configured backlog limit only: too many jobs already waiting to start; wait retry_after seconds).
search
ChatGPTKeyword 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.
searchSegments
ChatGPTAnswers "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.
transcribe
ChatGPTStarts a transcription; the job draws on the balance already on the user's transcribe.so account (0 when the account's tier includes it). Collapses the two-step quote+start dance from /api/v1/* into a single call (call getQuote first if the user wants to see the length and amount 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 nothing was started or charged: tell the user their current transcribe.so balance does not cover this job and stop; do not point them at a payment page. On queue_full (accounts with a configured backlog limit only) too many jobs are already waiting to start: nothing was created or charged; wait retry_after seconds, then call again. 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. Optional client_reference: your own id for the job, returned on every read and webhook and searchable via listTranscriptions. Retries: pass idempotency_key and re-send the SAME key with the SAME arguments to replay the first result instead of charging twice. Error results below 500 are replayed under that key for 24 hours, so use a NEW idempotency_key only after a refusal you actually received that says nothing was started or charged, once its cause is fixed (e.g. insufficient_funds or max_charge_exceeded after the user resolved it, or a validation error such as unsupported_language after you corrected the input); sending CHANGED arguments after such a refusal is a new request and takes a new key. queue_full is not stored under the key: after waiting retry_after seconds, call again with the same key. If the same key is rejected because the arguments differ and you never got an answer to the first call, the first call may have gone through: check listTranscriptions / getTranscription before transcribing again, and never switch to a new key just to get past that rejection. A 5xx result (e.g. internal_error or source_busy with retry_after) is never stored: wait retry_after seconds and retry with the same key; if a refusal saying reading the source took longer than 45 seconds comes back again on that retry, stop and tell the user (for external_url, pass duration_seconds if you know the length). source_busy means the video's host (YouTube) is refusing requests for this video right now: retry once with the same key after retry_after seconds; if it is refused the same way again, or you cannot wait that long, stop and tell the user. If a same-key call answers not_ready saying the earlier call is already in flight, that call is still running; the server ends its preview within 45 seconds, so keep retrying the SAME key. Only when that answer carries stale: true (no result recorded after 120 seconds; in_flight_seconds gives the age), check listTranscriptions with the client_reference you sent: a job listed there in ANY status, including quoted, counts as found, so do not create another: follow it with getTr…
waitForTranscription
ChatGPTLong-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 the client's 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. If the result carries reason: "not_started" (status quoted: previewed but never started, e.g. only getQuote ran or the start failed), it returns within a few seconds instead of waiting: the job will never progress, so do not call waitForTranscription again; follow next_step (call transcribe again to create a new job). If you just called transcribe on this id, wait for that response before acting on not_started.