API docs
A small HTTP surface for transcribing audio & video. Drop a file or a URL, poll the job, pull the transcript in the format you want. Same engine as the web app.
Auth
Every request carries an API key. Optional user-identity headers attach the job to a specific account so it shows up in "Your files", gets the longer retention window on a paid plan, and survives re-installs.
| Header | Required? | Notes |
|---|---|---|
X-API-Key: <key>required |
on every request | Identifies the calling app + tier. Create one yourself at /apis/keys โ sign in, add credit, mint the key. The API has no free tier: a key needs a positive balance ($50 minimum, spendable on transcription). |
Authorization: Bearer <Firebase idToken>optional |
signed-in users | Identifies a user signed into whipscribe.com. Jobs submitted with this header are tied to the user's email; retention + "Your files" follow the user, not the key. |
X-User-Email: you@example.comoptional |
paid accounts without Firebase | Look-up hint for the credit ledger. Prefer Authorization when the caller is a Firebase-authed browser; use X-User-Email for server-to-server flows. |
X-Guest-Token: <token>optional |
one-off guest purchase | Returned by the credit-purchase flow for anonymous buyers. |
X-Claim-Token: <token>optional |
guest submissions | Proves ownership of a job submitted without a signed-in user. The submit response includes claim_token; store it client-side and send it on subsequent reads for the same job. See Claim a guest job. |
Anonymous calls (no user-identity headers) are allowed on the free tier and subject to per-IP rate limits. Keys in doc examples are always placeholders โ don't share yours.
Base URL & CORS
All paths below are relative to this root. The server accepts application/json for URL submits and multipart/form-data for file uploads. CORS is open for GET and POST from browser origins; server-to-server calls have no origin restriction.
Credits & billing
Credit is metered in audio-hours. A 30-minute audio file spends 0.5h; a 60-second clip spends 60s from the same balance. Jobs that fail don't consume credit. Submits over your balance return HTTP 402 with an upgrade_url. See /pricing for tiers, or the /credits dashboard to refresh your server-side balance on a new device.
Submit a file
Upload an audio or video file as multipart/form-data.
| Field | Type | Notes |
|---|---|---|
filerequired | file | mp3, m4a, wav, mp4, mov, ogg, webm, flac. Up to 10 hours per file. |
languageoptional | string | ISO code (en, es, fr, โฆ). Auto-detected if omitted. |
diarizeoptional | boolean | Speaker labels. Default true. |
word_timestampsoptional | boolean | Per-word offsets. Default true. |
sourceoptional | enum | upload | url | recording | api. Defaults to upload for this endpoint. See Source field. |
# curl example curl https://whipscribe.com/api/v1/transcribe \ -H "X-API-Key: $WHIPSCRIBE_KEY" \ -F "file=@episode-412.mp3" \ -F "language=en" \ -F "source=api"
// 202 Accepted { "job_id": "35f4be54-aa3e-4adc-85b7-b44f284d1fc3", "status": "queued", "tier": 2, "claim_token": "e0610c19..." // only when no user identity was supplied }
Submit a URL
Same pipeline, but we fetch the media for you. Only Creative Commons-licensed YouTube URLs are currently accepted. Accepts the same language, diarize, word_timestamps, and source fields as the multipart endpoint. Defaults source to url.
curl https://whipscribe.com/api/v1/transcribe/url \ -H "X-API-Key: $WHIPSCRIBE_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://www.youtube.com/watch?v=...", "language":"en", "source":"url"}'
Bulk & Google Drive folders
Two ways to run a whole folder, both keyed to your API key โ connectors are per-user tenants, never shared.
Bulk from your own storage
Loop your files through POST /v1/transcribe (or the presigned
POST /v1/uploads/init flow for large files), passing
Idempotency-Key per file so retries never double-submit. Group the results
yourself, or use the library folder endpoints to mirror your source folder:
# 1. create (or reuse) the destination folder
curl -X POST https://whipscribe.com/api/v1/library/folders \
-H "Authorization: Bearer <firebase-id-token>" \
-H "Content-Type: application/json" -d '{"name": "Podcast Season 2"}'
# 2. after each submit, file the job into it
curl -X POST https://whipscribe.com/api/v1/library/folders/<folder_id>/items \
-H "Authorization: Bearer <firebase-id-token>" \
-H "Content-Type: application/json" \
-d '{"type": "transcript", "job_id": "<job_id>", "title": "ep-014.mp3"}'
No code at all: whipscribe.com/bulk does the same thing from the browser โ pick a local folder, and every file lands in a same-named WhipScribe folder.
Google Drive connector (early access)
Connect once over OAuth, then batch-transcribe picked files or whole folders. Each picked Drive folder is mirrored as a matching WhipScribe library folder and every job from it is filed there automatically. Drive bytes stream through our server โ your Drive credentials never reach a worker.
# start OAuth (returns the Google authorize URL to open)
POST /v1/connectors/drive/start {"nickname": "work-drive"}
# your connectors
GET /v1/connectors/drive
# expand + enqueue: file & folder IDs from the Drive Picker
POST /v1/connectors/drive/<id>/transcribe
{"file_ids": ["<drive-file-or-folder-id>", ...], "language": "auto"}
# โ {"batch_id", "queued", "total", "failures", "folders":[{"name","folder_id"}]}
# batch progress
GET /v1/batches/<batch_id>
Every file in a batch is a normal job โ poll it, fetch results, and receive webhooks exactly as documented above. Rollout is per-account during early access: leave your email on /bulk and we enable yours.
List your jobs
Most recent first. Scoped to the caller's identity (Firebase user email, X-User-Email, or the API key). Returns an array of the same row shape as the status endpoint.
// 200 OK [ { "job_id": "35f4be54-...", "status": "done", "filename": "episode-412.mp3", "audio_duration_seconds": 967, "language": "en", "source": "upload", "created_at": 1776620389.79 }, ... ]
Poll job status
Lightweight poll. Recommended cadence: 3 seconds while status โ {queued, processing}.
// 200 OK { "job_id": "35f4be54-...", "status": "processing", // queued | processing | done | failed "progress": 0.42, // 0.0โ1.0 (best-effort) "audio_duration_seconds": 967, "language": "en", "source": "upload", // upload | url | recording | api "speech_detected": true, // see "No-speech detection" below "speech_ratio": 0.78, // 0.0โ1.0 โ fraction classified as speech by the VAD pre-flight "error": null }
No-speech detection
Every submission runs through a Silero VAD pre-flight before reaching Whisper. If the audio doesn't contain transcribable speech (music, ambient noise, near-silence), the job completes successfully with speech_detected: false instead of feeding non-speech audio to Whisper, which would otherwise hallucinate confident-looking text in random languages.
The job's status stays done โ the system worked, the audio just didn't contain what we transcribe. The result document carries:
// GET /v1/jobs/{id}/result?format=json โ VAD-rejected file { "text": "", "language": null, "segments": [], "speech_detected": false, "speech_ratio": 0.02, "suggestion": "This file appears to be music or ambient audio. Transcription requires spoken content." }
Branch your client on speech_detected === false rather than parsing the suggestion text. No usage minutes are charged for VAD-rejected jobs. The threshold is operator-tunable via WHIPSCRIBE_VAD_MIN_SPEECH_RATIO (server-side only).
Get the transcript
Returns application/json for format=json, plain text for the rest. format=json gives you the richest payload โ text, segments, speaker labels, word timestamps.
// GET .../result?format=json { "text": "Welcome back to the show. Today we're talking about...", "language": "en", "segments": [ { "start": 0.0, "end": 16.3, "speaker": "SPEAKER_00", "text": "Welcome back to the show...", "words": [ { "start": 0.0, "end": 0.4, "text": "Welcome" }, ... ] }, ... ] }
Playback URL
Returns a URL your <audio> element (or any HTTP client that supports Range) can stream the original audio from. Keeps your API key off the playback path.
// 200 OK { "url": "https://audio.del1.vultrobjects.com/โฆ/episode-412.mp3", "storage": "vultr", // "vultr" (direct CDN/presigned) | "disk" (backend-relative, prepend /api) "expires_in": 600, // seconds; refetch on <audio>.error or before playback "retention_days": 30 // only on 410; see Retention }
If storage is "disk", the returned path is backend-relative โ prepend your base URL's /api prefix before handing it to the browser. The URL is short-lived; the audio.addEventListener('error', refetchAndResume) pattern is supported.
Claim a guest job
Transfer ownership of jobs that were submitted anonymously (no user identity on submit) to a signed-in user. The submit response for anonymous jobs includes claim_token; keep it client-side until the user signs in, then call this endpoint once to attach every pending token to their email. Extends each claimed job's retention to the claiming user's tier window.
curl https://whipscribe.com/api/v1/jobs/claim \ -H "X-API-Key: $WHIPSCRIBE_KEY" \ -H "Authorization: Bearer $FIREBASE_ID_TOKEN" \ -H "Content-Type: application/json" \ -d '{"claim_tokens": ["e0610c19...", "2ace5e8d..."]}'
// 200 OK { "claimed": 2 }
Cancel or delete a job
Cancels in-flight jobs or removes completed jobs from the server. Deleting a completed job does not return the credits its transcription consumed. Returns 204 No Content.
Make a clip
Render a vertical 9:16 MP4 of any [start_s, end_s] window (3–180 s) from a finished job. Uploaded and recorded files only — jobs submitted by URL return 403 CLIPS_UPLOAD_ONLY. Clips are metered like audio: the clip's length is spent from the same free daily minutes or paid hours as transcription. Responds 202 immediately; poll GET /api/v1/clips/{clip_id} until status is done (then video_url is set) or failed.
curl https://whipscribe.com/api/v1/jobs/$JOB_ID/clips \ -H "X-API-Key: $WHIPSCRIBE_KEY" \ -H "Content-Type: application/json" \ -d '{"start_s": 312.4, "end_s": 358.0, "title": "Why we killed the free tier", "caption_style": "bold-yellow"}' # โ 202 {"clip_id":"โฆ","status":"rendering","duration_s":45.6,"billed":false,"poll":"/v1/clips/โฆ"} curl https://whipscribe.com/api/v1/clips/$CLIP_ID -H "X-API-Key: $WHIPSCRIBE_KEY" # โ {"clip":{"status":"done","video_url":"https://โฆ/clip.mp4","duration_s":45.6, โฆ}}
| Field | Type | Notes |
|---|---|---|
start_s, end_s | number | Seconds into the source. Must lie inside the source duration; end_s - start_s must be 3–180. |
titleoptional | string | Shown in your Shorts library. Max 120 chars. |
caption_styleoptional | string | Burned-in caption preset: bold-yellow, rounded-white, bold-bg, karaoke. Omit for no captions; unknown names return 400 INVALID_CAPTION_STYLE with the valid list. |
Your clips are listed at GET /api/v1/shorts; re-cut one with POST /api/v1/clips/{clip_id}/retrim (not metered again); remove one with DELETE /api/v1/clips/{clip_id}.
Find moments
Runs the one-time analysis (sentence index, energy, silences, speaker turns) that the discovery endpoints read. Idempotent; returns 202. Until it finishes, the endpoints below return 409 FEATURES_NOT_READY — usually 10–60 s.
Duration, speaker turns, silence breaks and a short summary of the recording.
High-signal sentences with timestamps. kind is one of hook, question, number, speaker_change, high_energy. Use them as start_s seeds, then widen to a sentence boundary.
Returns {"matches": [...]} of sentences containing q. Pass start_s and end_s instead of q to list every sentence in a range when tightening a clip's bounds.
curl "https://whipscribe.com/api/v1/jobs/$JOB_ID/clips/candidates?kind=hook&limit=5" -H "X-API-Key: $WHIPSCRIBE_KEY" # โ {"sentences":[{"start_s":312.4,"end_s":318.9,"text":"Here's the part nobody tells youโฆ"}, โฆ]}
Who am I
Returns what the server sees when you call. Use this instead of hardcoding the retention window in your client โ the number is authoritative and will stay in sync if the policy ever shifts.
// 200 OK โ signed-in user on the free plan { "email": "you@example.com", "tier": "free", // guest | free | paid โ stable public enum "retention_days": 30, "signed_in": true }
// 200 OK โ anonymous caller (no Authorization / X-User-Email) { "email": null, "tier": "guest", "retention_days": 3, "signed_in": false }
Treat tier as the stable public field. Any additional fields on this response are implementation detail and may change โ don't branch on them.
Retention
Uploaded audio is kept for the window below after the job completes, then auto-deleted. Transcripts stick around until you delete them.
| Tier | Audio retention | Who |
|---|---|---|
guest | 3 days | anonymous submissions (no signed-in user) |
free | 30 days | signed-in users on the free plan |
paid | 365 days | any paid plan |
Query GET /api/v1/me for the exact retention_days that applies to the caller. When the window has elapsed, GET /jobs/{id}/audio/url returns 410 AUDIO_EXPIRED with the original window in the response body.
Source field
Every job carries a source enum so clients can surface how a transcript was created. Server validates on submit; invalid values return 422 BAD_SOURCE.
| Value | Meaning | Default for |
|---|---|---|
upload | multipart file upload | POST /transcribe |
url | fetched from a URL | POST /transcribe/url |
recording | captured from browser / extension mic | โ |
api | programmatic caller (scripts, SDKs, CI, MCP) | โ |
Callers can override the default by sending source in the submit body.
Idempotency-Key
Submit endpoints (POST /v1/transcribe, POST /v1/transcribe/url, POST /v1/uploads/init) accept an optional Idempotency-Key: <string> header. Retries carrying the same key for the same API key return the original job's response (HTTP 200, with an X-Idempotent-Replay: true header) instead of creating a duplicate job โ matching the Stripe pattern. Keys are scoped per API key, may contain [A-Za-z0-9_.:/-], and must be โค255 chars with no whitespace; invalid keys return 400 BAD_IDEMPOTENCY_KEY. The key becomes reusable once the original job ages past its retention window.
# First submit curl https://whipscribe.com/api/v1/transcribe/url \ -H "X-API-Key: $WHIPSCRIBE_KEY" \ -H "Idempotency-Key: job-2026-04-19-abc123" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/audio.mp3"}' # โ 202 {"job_id":"โฆ","status":"queued",โฆ} # Retry after a dropped connection โ same key, same response curl https://whipscribe.com/api/v1/transcribe/url \ -H "X-API-Key: $WHIPSCRIBE_KEY" \ -H "Idempotency-Key: job-2026-04-19-abc123" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/audio.mp3"}' # โ 200 {"job_id":"<same>","status":"queued",โฆ} (X-Idempotent-Replay: true)
Error codes
Every error response is JSON-shaped as {"error": "<human sentence>", "code": "<machine enum>"}. Some codes carry extra fields โ noted inline.
| Status | Code | Meaning |
|---|---|---|
400 | BAD_ID | Malformed job_id (not a valid UUID). |
400 | BAD_URL | Submitted URL isn't http(s). |
401 | MISSING_API_KEY | No X-API-Key header. |
401 | AUTHENTICATION_REQUIRED | The job belongs to a signed-in user (or carries a pending claim token) but the request didn't carry user-identity proof โ pass Authorization: Bearer <Firebase idToken> or X-Claim-Token: <token>. Distinct from NOT_FOUND: 401 means "you forgot the headers", 404 means "wrong job_id (or auth provided but doesn't match โ anti-enumeration)". |
402 | NO_CREDITS | Insufficient credit. Response includes credits snapshot and upgrade_url. |
404 | NOT_FOUND | Unknown job id, or exists but not yours. (We deliberately don't distinguish โ don't leak existence.) |
410 | AUDIO_EXPIRED | Audio retention window has elapsed. Body includes retention_days โ the policy that applied. Transcript is still readable. |
410 | AUDIO_MISSING | Audio was deleted or never persisted. |
413 | FILE_TOO_LARGE | Multipart upload exceeds the max size. |
415 | BAD_MIME | File's detected MIME type is unsupported. |
422 | BAD_SOURCE | Submitted source isn't one of the allowed enum values. |
400 | BAD_IDEMPOTENCY_KEY | Submitted Idempotency-Key header failed validation (empty, whitespace, > 255 chars, control chars, or disallowed punctuation). See Idempotency-Key. |
403 | CLIPS_UPLOAD_ONLY | Clips are rendered from uploaded or recorded files only; URL jobs are transcript-only. |
409 | FEATURES_NOT_READY | Call POST /jobs/{id}/clips/preprocess first, then retry. |
409 | TRANSCRIPT_NOT_DONE | Clip endpoints need a finished job. Body includes the current status. |
429 | QUOTA_EXCEEDED | Free daily minutes spent (transcription and clips share the bucket). Includes upgrade_url. |
429 | CLIP_RATE_LIMITED | Daily clip count for your tier reached; resets at UTC midnight. |
429 | RATE_LIMITED | Too many submits in a short window; retry with backoff. |
502 | BACKEND_ERROR | Upstream transcription service error. Retry safely. |
502 | BACKEND_UNREACHABLE | We couldn't reach the transcription backend at all. Retry. |
Versioning & surface stability
Only paths under /api/v1/* are considered public and will be deprecated with notice before removal. Anything outside that prefix is internal and may change at any time โ don't integrate against it.
Within /v1, response objects are additive: new fields may appear, existing field names and types won't change under the same version. Error code strings are stable; HTTP status + code together uniquely identify a failure mode.
Contact
Enterprise tier (signed webhooks, 120 req/min), higher rate limits, SSO/SAML, on-prem, or feature requests โ email contact@neugence.ai. We usually reply within a day.