WhipScribe API

Audio and video to text, from your code

Send a link or a file. Get back a timestamped transcript as JSON, TXT, SRT, VTT or DOCX, plus a summary with key quotes and named speakers. It's the same service the website runs, in 99+ languages.

  • REST + JSON
  • One header: X-API-Key
  • MCP connector
  • Files up to 10 hours
  • Credits never expire
60-second quickstart

From a link to subtitles

  1. Get a key. Sign in and get API access: it comes with the 5,000-minute pack ($24, credits never expire) or the Workspace plan ($19/month). Then create a key at whipscribe.com/apis/keys. It's shown once, so put it in an environment variable straight away.
  2. Send a link. POST /v1/transcribe/url returns a job_id straight away.
  3. Wait, then fetch. Poll GET /v1/jobs/{job_id} every few seconds until status is done, then download ?format=srt.
export WHIPSCRIBE_API_KEY="paste-your-key"

# 1. Send a link (any public video, podcast episode or media file)
curl -s https://whipscribe.com/api/v1/transcribe/url \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.archive.org/download/testmp3testfile/mpthreetest.mp3"}'
# → {"job_id": "35f4be54-aa3e-4adc-85b7-b44f284d1fc3", "status": "queued"}

JOB_ID=35f4be54-aa3e-4adc-85b7-b44f284d1fc3   # the job_id you got back

# 2. Poll every few seconds until "status" is "done"
curl -s https://whipscribe.com/api/v1/jobs/$JOB_ID \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY"

# 3. Download subtitles (or format=json | txt | vtt | docx)
curl -s "https://whipscribe.com/api/v1/jobs/$JOB_ID/result?format=srt" \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY" -o transcript.srt

Call the API from your server or script, not from a web page. Browsers are blocked from calling it directly, which also keeps your key out of page source. To play audio in a browser, hand it the short-lived link from the playback endpoint.

Use it from

Pick the door that suits your stack

Recipes

What people build with it

These come from the projects developers have actually shipped on the API: call scorecards and CRM notes, meeting digests, subtitle pipelines, Drive and folder importers, and assistants that transcribe inside chat.

Calls and meetings to structured notes

most built

Upload the recording, wait for it to finish, then read two things: the summary (topics, key quotes, who said what) and the timestamped segments. Hand both to your own model or template to produce a scorecard, a CRM note, a decision log or a list of action items, and keep the timestamps so every claim links back to the moment it was said.

curl
# Upload a recording (multipart)
curl -s https://whipscribe.com/api/v1/transcribe \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY" \
  -H "Idempotency-Key: call-2026-10-04-acme" \
  -F "file=@sales-call.m4a" \
  -F "language=auto" \
  -F "word_timestamps=true"

# …poll GET /v1/jobs/$JOB_ID until done, then:
curl -s https://whipscribe.com/api/v1/jobs/$JOB_ID/insights -H "X-API-Key: $WHIPSCRIBE_API_KEY"
curl -s "https://whipscribe.com/api/v1/jobs/$JOB_ID/result?format=json" -H "X-API-Key: $WHIPSCRIBE_API_KEY"

The summary is written the first time you ask for it, so the first /insights call on a job can take a little longer; later calls return straight away.

Video and podcast links to subtitles

Send the page link (YouTube, Vimeo, an Apple Podcasts episode, an RSS feed, a Google Drive share link) or a direct media URL. When it's done, ask for format=srt or format=vtt and drop the file next to your video. Use format=txt for show notes and blog drafts.

curl
curl -s https://whipscribe.com/api/v1/transcribe/url \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.youtube.com/watch?v=VIDEO_ID", "language": "auto"}'

# when done:
curl -s "https://whipscribe.com/api/v1/jobs/$JOB_ID/result?format=vtt" \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY" -o captions.vtt

A link that is private, removed or behind a sign-in is refused up front with a clear message and nothing is queued. Download the file and upload it instead. See links we can fetch.

A folder of recordings

Loop over the files, submit each one with an Idempotency-Key made from the file's name or hash, and poll the list endpoint rather than every job. If your script crashes halfway, run it again: files already sent return their original job instead of being transcribed twice.

Python
import hashlib, os, pathlib, time, requests

API = "https://whipscribe.com/api/v1"
HEADERS = {"X-API-Key": os.environ["WHIPSCRIBE_API_KEY"]}
folder = pathlib.Path("recordings")
out = pathlib.Path("transcripts"); out.mkdir(exist_ok=True)

jobs = {}
for path in sorted(folder.glob("*.mp3")):
    key = "batch-" + hashlib.sha256(path.read_bytes()).hexdigest()[:32]
    with path.open("rb") as f:
        r = requests.post(f"{API}/transcribe", headers={**HEADERS, "Idempotency-Key": key},
                          files={"file": (path.name, f)}, data={"language": "auto"})
    r.raise_for_status()
    jobs[r.json()["job_id"]] = path

while jobs:
    time.sleep(10)
    for job_id, path in list(jobs.items()):
        job = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS).json()
        if job["status"] == "done":
            txt = requests.get(f"{API}/jobs/{job_id}/result", headers=HEADERS, params={"format": "txt"})
            (out / (path.stem + ".txt")).write_text(txt.text, encoding="utf-8")
            del jobs[job_id]
        elif job["status"] == "failed":
            print(path.name, job["error"]); del jobs[job_id]

An account allows 60 requests a minute across its keys, so a slow poll (every 10 seconds or more) leaves plenty of room for big folders. No code? The Library does the same from the browser, folder for folder.

Large files

For long recordings, skip the multipart form: ask for an upload link, send the bytes straight to it, then commit. It's faster and copes better with slow connections.

Python
path = pathlib.Path("town-hall.mp4")

# 1. Ask for an upload ticket
ticket = requests.post(f"{API}/uploads/init", headers=HEADERS, json={
    "filename": path.name, "content_type": "video/mp4",
    "size_bytes": path.stat().st_size, "language": "auto",
}).json()

# 2. Send the bytes exactly as the ticket says (method + headers)
with path.open("rb") as f:
    requests.request(ticket["method"], ticket["upload_url"],
                     headers=ticket.get("headers", {}), data=f).raise_for_status()

# 3. Commit: echo object_key and options back unchanged
job = requests.post(f"{API}/uploads/{ticket['job_id']}/commit", headers=HEADERS, json={
    "object_key": ticket["object_key"], "options": ticket["options"],
    "original_filename": path.name,
}).json()   # → {"job_id": …, "status": "queued"}; poll as usual

The upload link expires after expires_in seconds; ask for a new ticket if you miss it.

Transcript beside the audio

For a player where clicking a line jumps to that moment, fetch format=json (each segment has start and end in seconds) and a playback link. The link is short-lived and carries no key, so it's safe to give to the browser; fetch a fresh one when it expires.

JavaScript (your server, then the page)
// server: GET /v1/jobs/{id}/audio/url → { "url": "https://…", "expires_in": 3600 }
const { url } = await (await fetch(`${API}/jobs/${jobId}/audio/url`, { headers })).json();

// page: one line per segment; click to seek
audio.src = url;
for (const seg of transcript.segments) {
  const line = document.createElement("p");
  line.textContent = seg.text;
  line.onclick = () => { audio.currentTime = seg.start; audio.play(); };
  list.append(line);
}

Inside Claude and other MCP clients

The MCP connector gives an assistant tools to transcribe links and files, read transcripts and summaries, list your recent jobs, and organise your library into folders. Ask in plain words: “Transcribe this episode and give me the five best quotes with timestamps.”

  • claude.ai and Claude Desktop: Settings → Connectors → Add custom connector, and paste https://whipscribe.com/mcp. Sign in to WhipScribe when asked.
  • Claude Code: run the command below, then /mcp to sign in.
  • Clients that run a local command (Cursor, Windsurf, Claude Desktop config): use the whipscribe-mcp package with your key in WHIPSCRIBE_API_KEY.
  • Your own MCP client: connect over Streamable HTTP and either complete the sign-in flow or send Authorization: Bearer <your API key>. On the MCP endpoint the key goes in Authorization, not X-API-Key.
claude mcp add --transport http whipscribe https://whipscribe.com/mcp

More on the connector, with example prompts: whipscribe.com/claude.

Reference

Base URL and authentication

https://whipscribe.com/api/v1

Every path below sits under this base, so /v1/jobs means https://whipscribe.com/api/v1/jobs. There's no separate API hostname.

Send your key on every request in one header:

X-API-Key: <your key>

The key belongs to your account. Jobs you create with it show up in your WhipScribe library, and usage comes out of the same balance as the website. You can hold up to 10 active keys, for example one per environment. Rotating a key at /apis/keys keeps the old one working for 24 hours so a rolling deploy doesn't break. Treat a key like a password: never put it in a web page or a public repo.

Bodies are JSON (Content-Type: application/json), except file uploads, which use multipart/form-data.

Endpoints

CallWhat it does
POST /v1/transcribeUpload a file (multipart, field file) and start a job.
POST /v1/transcribe/urlStart a job from a link. JSON body with url.
POST /v1/uploads/initGet a short-lived upload link for a large file. See large files.
POST /v1/uploads/{job_id}/commitStart the job once the bytes are uploaded.
GET /v1/jobsYour jobs, newest first. limit (1–100, default 20), offset, status. Returns {"jobs": [...], "count", "total", "limit", "offset"}.
GET /v1/jobs/{job_id}Status of one job. Poll every 3 seconds or slower. Supports If-None-Match (304).
DELETE /v1/jobs/{job_id}Cancel a queued job, or delete a finished one and its files. Returns 204. Time already used isn't refunded.
GET /v1/jobs/{job_id}/resultThe transcript. format=json (default), txt, srt, vtt or docx.
GET /v1/jobs/{job_id}/insightsSummary, key quotes, topics and named speakers.
GET /v1/jobs/{job_id}/audio/urlA short-lived link to stream the original audio.
GET /v1/meWho the server thinks you are, and how long your audio is kept.
GET /v1/creditsTranscription time left on the account behind the key.
POST /v1/checkoutGet a pay link for a pack or the plan, tied to your account. See pay links.
GET /v1/checkout/{id}Whether that link is pending, paid or expired.

The same list, with every field, is in the OpenAPI spec.

Submit options

The file upload, the link submit and the upload ticket all accept these fields: form fields for /v1/transcribe, JSON for the others.

FieldNotes
filerequired on /v1/transcribeThe audio or video file. Most formats work: mp3, m4a, wav, aac, flac, ogg, opus, mp4, mov, webm, mkv and more. Up to 10 hours per file.
urlrequired on /v1/transcribe/urlA public link to a page or a media file. See links we can fetch.
languageoptionalAn ISO 639-1 code (en, es, hi, …) or auto (default) to detect it. Setting it helps on short or noisy clips.
word_timestampsoptionaltrue to get a start and end time for every word in format=json. Default false.
diarizeoptionaltrue to ask for speaker separation. Default false. Speakers are identified and named in the summary, key quotes and chapters. Don't rely on a per-line speaker label in the transcript.
sourceoptionalA label for how the recording was made: upload, url, recording or api. Defaults to upload for files and url for links.

A submit answers 202 with {"job_id", "status": "queued"}. Keep the job_id; everything else hangs off it.

The job object

What GET /v1/jobs/{job_id} returns. New fields may appear over time; ignore the ones you don't use.

200 OK
{
  "job_id": "35f4be54-aa3e-4adc-85b7-b44f284d1fc3",
  "status": "done",                 // queued | processing | done | failed
  "filename": "sales-call",
  "audio_duration_seconds": 967.4,
  "source": "upload",
  "speech_detected": true,
  "created_at": 1791158901.4,         // Unix seconds
  "completed_at": 1791158934.4,
  "locked": false,                  // see Billing
  "error": null                       // a readable sentence when status is "failed"
}
  • status is the field to branch on. progress is a rough hint for a progress bar, not a promise.
  • locked is separate from status. A job can be done and locked at once. Check it before downloading.
  • No speech, no charge. Music, noise or silence finishes as done with speech_detected: false and an empty transcript, and no time is used. Branch on that field, not on the message text.
  • Poll every 3 seconds or slower, and use GET /v1/jobs?status=processing when you're waiting on many.

Transcript formats

GET /v1/jobs/{job_id}/result?format=json|txt|srt|vtt|docx
FormatUse it for
jsonBuilding on the transcript: full text, segments with start and end times, words when you asked for them.
txtPlain text for notes, search, or a prompt.
srt / vttSubtitles and captions for video players and editors.
docxA Word document to share or edit.
format=json
{
  "text": "Welcome back to the show. Today we're talking about pricing.",
  "language": "en",
  "duration": 967.4,
  "speech_detected": true,
  "segments": [
    {
      "start": 0.0,
      "end": 4.2,
      "text": "Welcome back to the show.",
      "words": [ { "start": 0.0, "end": 0.4, "text": "Welcome" } ]   // with word_timestamps=true
    }
  ]
}

Summary and insights

GET /v1/jobs/{job_id}/insights

A summary of the recording with its topics, key quotes and the people speaking, identified and named. It's written the first time you ask and stored after that. A job that isn't finished returns 409 NOT_READY; a recording with too little speech to summarise returns 409 NO_SPEECH.

200 OK (shortened)
{
  "insights": {
    "summary": "The team agrees to move the launch to March…",
    "topics": [ … ],
    "quotes": [ … ],
    "speakers": [ … ]
  }
}

Read the fields you need and ignore the rest. The object grows as we add to the summary.

Billing and locked transcripts

Usage is measured in minutes of audio and comes out of your account balance, the same one the website uses. API access, meaning the right to create keys, comes with the 5,000-minute pack ($24, credits never expire) or the Workspace plan ($19 a month: unlimited transcription through the site and your own keys, with no daily cap, fair use applies). The $6 and $12 packs work on the website and do not include API keys; once you have API access, any pack tops the balance up. Asking for a key without it answers 402 API_ACCESS_REQUIRED with an upgrade_url. GET /v1/credits tells you what's left.

  • Failed jobs and recordings with no speech aren't charged.
  • If a job finishes when the balance has run out, it's kept and marked "locked": true with an unlock_url. You can still read a free preview: format=json returns the opening part. Other formats and /insights answer 402 transcript_locked until the account has credit, and then the full transcript opens.
  • Deleting a finished job doesn't refund the time it used.

A script or an agent can't type a card, and shouldn't. Ask for a pay link instead: it opens card checkout for your account, a person pays it, and the credit lands on the account that asked, whoever pays.

curl
# 1. Ask for a link (sku: mini-4, pro-monthly, team-monthly or workspace-monthly)
curl -s https://whipscribe.com/api/v1/checkout \
  -H "X-API-Key: $WHIPSCRIBE_API_KEY" -H "Content-Type: application/json" \
  -d '{"sku": "team-monthly"}'
# → {"id": "txn_…", "status": "pending", "checkout_url": "https://…", "sku": "team-monthly",
#    "amount_usd": 24.0, "minutes": 5000, "expires_at": 1791391955.7}

# 2. Give checkout_url to the person who pays, then poll until "paid"
curl -s https://whipscribe.com/api/v1/checkout/txn_… -H "X-API-Key: $WHIPSCRIBE_API_KEY"
  • The packs are one-time purchases: mini-4 (500 minutes, $6), pro-monthly (2,000, $12) and team-monthly (5,000, $24, includes API access). workspace-monthly is the $19-a-month plan. An unknown sku answers 400 UNKNOWN_SKU with the current list in sellable.
  • A link works for 48 hours. Asking again for the same sku while one is open returns the same link; an account can make six new links an hour.
  • When a request is refused for lack of credit (402 NO_CREDITS, INSUFFICIENT_CREDITS_FOR_DURATION, transcript_locked, API_ACCESS_REQUIRED) and you called with your own key, the answer already carries a checkout_url for the pack that covers it. Hand it over, wait for paid, and send the same request again.

Idempotency

Send Idempotency-Key: <your string> on POST /v1/transcribe, /v1/transcribe/url or /v1/uploads/init. A retry with the same key (for the same API key) returns the original job with 200 and X-Idempotent-Replay: true instead of starting a second one. Keys are up to 255 characters from A–Z a–z 0–9 _ . : / -; anything else returns 400 BAD_IDEMPOTENCY_KEY. Use one per file or per link, for example a hash of the file.

Limits and retention

Requests60 per minute per account, shared by its keys. Past that you get 429 RATE_LIMITED with a Retry-After header.
File lengthUp to 10 hours per file. On the Workspace plan, no per-file cap.
KeysUp to 10 active keys per account.
At onceWorkspace runs up to 10 recordings at once per account; API submits beyond that get 429 CONCURRENCY_LIMIT with Retry-After.
TranscriptsKept until you delete them.
Original audioKept until you delete it (DELETE /v1/jobs/{job_id}). GET /v1/me reports this as "retention_days": 36500. One exception: media fetched from a Kick VOD link is removed a day after you submit it. Once audio is gone, /audio/url answers 410 and the transcript is still there.

Need more throughput, signed webhooks or an invoice? Tell us what you're building.

Errors

Errors are JSON with a readable sentence and a stable code: {"error": "…", "code": "NOT_FOUND"}. Branch on code and show error to people. A request missing a required field answers 422 with a detail list instead.

StatusCodeWhat to do
400BAD_IDThe job id isn't a valid UUID.
400BAD_URLThat isn't a link. Send the full address starting with https://.
400UNSUPPORTED_HOST, SOURCE_GONEWe can't fetch audio from that link, or it's gone. Upload the file instead.
400BAD_IDEMPOTENCY_KEYFix the Idempotency-Key format (see above).
401MISSING_API_KEY, INVALID_API_KEYSend a valid key in X-API-Key.
400UNKNOWN_SKUPick a sku from the sellable list in the answer.
402transcript_lockedAdd credit at unlock_url, then fetch again.
402NO_CREDITS, INSUFFICIENT_CREDITS_FOR_DURATIONThe balance doesn't cover the request. Pay the checkout_url in the answer (see pay links), then send it again.
404NOT_FOUNDNo such job on your account. We don't say which, on purpose.
409NOT_READY, NO_SPEECHWait for done, or there's nothing to summarise.
410AUDIO_EXPIRED, AUDIO_MISSINGThe original audio is past its retention window or gone. The transcript is still readable.
413FILE_TOO_LARGE, FILE_TOO_LONGUse direct upload, or split the recording.
415BAD_MIME, BAD_EXTThat file type isn't audio or video we can read.
422BAD_SOURCEsource must be one of the four values above.
429RATE_LIMITEDWait for Retry-After seconds, then retry.
429CONCURRENCY_LIMITTen recordings are already running on this Workspace account. Wait for Retry-After seconds, then submit again.
5xxanyOur side. Retry with backoff; with an Idempotency-Key a retried submit can't double up.

Versioning

Everything under /api/v1 on this page is the public API. Within v1, changes are additive: new fields and new endpoints may appear, and existing fields and error codes keep their names and types. Anything removed gets notice first. Paths that aren't on this page or in the spec are internal and can change without warning, so don't build on them.

Support

Stuck, found a mismatch between these docs and what the API does, or need something it doesn't do yet? Email contact@whipscribe.com with the job_id if you have one. We usually reply within a day.