Skip to content
IntroductionHow it works, guides, and what you can transcribe.
QuickstartSubmit a link, wait for the job, export the transcript. Three requests.
AuthenticationAPI keys, scopes, Idempotency-Key and X-Request-Id.
JobsCreate, wait for, list, cancel and retry jobs.
TranscriptsThe transcript object and txt, srt, vtt, json exports.
BatchesUp to 50 videos in one request.
UploadsTranscribe your own audio and video files.
WebhooksGet called when a job or batch finishes; verify the signature.
ErrorsEvery error code, what it means and what to do.
Rate limitsSubmits, jobs in progress and reads per plan; the 429 response.
Pricing and credits1 credit per caption transcript, 2 per minute of AI transcription. Plans and extra credits.
SourcesYouTube, TikTok, your files and direct links: accepted URLs and modes.
MCPFind and transcribe videos from Claude, Cursor, Windsurf or your own agent.
OpenAPI SpecificationThe OpenAPI 3.1 spec for codegen and typed clients.
Claude CodeOne command adds TranscriptDock to Claude Code.
Claude DesktopAdd TranscriptDock as an MCP server in Claude Desktop's configuration file.
CursorAdd TranscriptDock as an MCP server in Cursor.
WindsurfAdd TranscriptDock to Windsurf so Cascade can find and transcribe videos.
OpenClawConnect TranscriptDock to OpenClaw autonomous agents.
19 results
API

API reference

Endpoints, fields, responses, credits, limits and errors.


How it works#

Every transcript is a job. You submit a source, the job runs in the background, and when it succeeds it points at a transcript you can read or export as often as you like. Base URL: https://api.transcriptdock.com. All requests and responses are JSON.

# 1. Submit
curl -X POST https://api.transcriptdock.com/v1/jobs \
-H "Authorization: Bearer td_live_YOUR_KEY" \
-H "Idempotency-Key: video-7680721699171601694" \
-H "Content-Type: application/json" \
-d '{ "source": { "url": "https://www.tiktok.com/@tiktok/video/7680721699171601694" }, "mode": "auto" }'
 
# 2. Wait (returns as soon as the job finishes, up to 25 s per call)
curl "https://api.transcriptdock.com/v1/jobs/11111111-1111-4111-8111-111111111111?wait=25" \
-H "Authorization: Bearer td_live_YOUR_KEY"
 
# 3. Export
curl "https://api.transcriptdock.com/v1/transcripts/22222222-2222-4222-8222-222222222222/export?format=srt" \
-H "Authorization: Bearer td_live_YOUR_KEY"
Never call the API from a browser: your key would be visible to anyone. Call it from your server and keep the key in an environment variable.

Authentication#

Create keys on the API keys page. A key is shown once, looks like td_live_..., and goes in the Authorization header of every request. Keys carry scopes chosen at creation: jobs:read, jobs:write, transcripts:read, uploads:write. Create one key per integration so you can revoke it alone.

header
Authorization: Bearer td_live_YOUR_KEY

Idempotency-Key#

Required on POST /v1/jobs, /v1/batches, /v1/jobs/{id}/retry, /v1/video-discoveries and /v1/language-discoveries: any 8 to 128 printable ASCII characters. Sending the same key with the same body within 48 hours returns the original object, so a retried request never creates or charges twice. The same key with a different body returns 409 IDEMPOTENCY_CONFLICT. A good key is your own id for the thing you are transcribing.

X-Request-Id#

Every response carries one. Quote it when you contact support.

Endpoints#

MethodPathWhat it doesCredits
POST/v1/jobsTranscribe one video, file or link1 per caption transcript, 2 per minute of AI transcription
GET/v1/jobs/{id}Job status, waits up to 25 sFree
GET/v1/jobsJob historyFree
POST/v1/jobs/{id}/cancelCancel a queued jobFree
POST/v1/jobs/{id}/retryRetry a failed jobAs a new job
POST/v1/batchesTranscribe up to 50 videosPer item, as above
GET/v1/batches/{id}Batch statusFree
GET/v1/transcripts/{id}Transcript JSONFree
GET/v1/transcripts/{id}/exporttxt, srt, vtt or json fileFree
DELETE/v1/transcripts/{id}Delete a transcriptFree
POST/v1/uploadsGet a URL to upload a file toFree
POST/v1/uploads/{id}/completeMark the upload finishedFree
POST/v1/video-discoveriesSearch YouTube, list a channel, playlist or TikTok profile1 per page
GET/v1/video-discoveries/{id}Discovery resultFree
POST/v1/language-discoveriesList a YouTube video's caption languages1
GET/v1/language-discoveries/{id}Language listFree
POST/v1/webhook-endpointsRegister a webhook URLFree
GET/v1/webhook-endpointsList webhook URLsFree
DELETE/v1/webhook-endpoints/{id}Disable a webhook URLFree
GET/v1/usageCredits and planFree
GET/v1/capabilitiesWhat your key can doFree

Create a job#

POST/v1/jobs
source.urlstringOptional
A public video: YouTube (watch?v=, youtu.be, shorts, live) or TikTok (tiktok.com/@user/video/…, vm.tiktok.com share links). Any other https link to an audio or video file is treated as a direct media link (AI transcription). One of url or upload_id is required.
source.upload_iduuidOptional
A file you uploaded (see Uploads). Always AI transcription.
mode"captions_only" | "auto" | "transcribe"Required
captions_only: the video's own captions, 1 credit; fails with NO_CAPTIONS if there are none. auto: captions when they exist (1 credit), otherwise AI transcription. transcribe: always AI transcription of the audio, 2 credits per started minute, word timings. AI transcription needs a paid plan.
caption_languagesstring[]Optional
Preferred caption languages in order, e.g. ["en", "es"], up to 5. Default: the video's default track. captions_only and auto only.
caption_preference"prefer_creator" | "creator_only" | "automatic_only"Optional
Whether to accept captions the creator uploaded, YouTube's automatic captions, or either (default prefer_creator: creator first).
languagestringOptional
Spoken language hint for AI transcription (BCP 47). Default: detect.
max_creditsintegerOptional
Spend cap for AI transcription. The media is measured first; if it would cost more, the job fails with BUDGET_EXCEEDED and nothing is charged. Default: enough for 2 hours.
webhook_endpoint_iduuidOptional
Receive job.succeeded / job.failed at this webhook.
metadataobjectOptional
Up to 10 string values (keys ≤ 64, values ≤ 256 characters). Echoed back in webhook events. Does not affect caching.

Returns 202 with the job. If your workspace already holds a transcript for the same video and options, it returns 200 with a succeeded job for free (billing.kind: "cached").

202 response
{
"id": "11111111-1111-4111-8111-111111111111",
"status": "queued",
"stage": "resolve",
"result_id": null,
"error": null,
"billing": { "credits_reserved": 2, "credits_charged": 0, "kind": "captions" },
"source": { "platform": "tiktok", "media_id": "7680721699171601694", "canonical_url": "https://www.tiktok.com/@tiktok/video/7680721699171601694", "title": null, "upload_id": null },
"options": { "mode": "auto", "language": null, "caption_preference": "prefer_creator" },
"created_at": "2026-09-16T10:00:00.000Z"
}

The job object#

iduuidOptional
Job id.
statusstringOptional
queued → processing (→ awaiting_provider during AI transcription) → succeeded, failed or cancelled. The last three are final.
stagestring | nullOptional
Where a running job is: resolve, captions, acquire_audio, submit_asr, wait_asr, finalize. Informational.
result_iduuid | nullOptional
The transcript, once succeeded.
errorobject | nullOptional
On failure: code, message, retryable, optional details.detail. Same codes as Errors.
billingobjectOptional
credits_reserved (held while running, 0 when done), credits_charged (final), kind: captions, ai_transcription or cached.
sourceobjectOptional
platform, media_id, canonical_url, title, upload_id.
optionsobjectOptional
mode, language, caption_preference as accepted.
created_atdate-timeOptional
UTC.

Get a job#

GET/v1/jobs/{id}?wait=25

Returns the job. With wait (0 to 25 seconds) the request stays open and returns as soon as the job is final, so one call replaces a polling loop. Reads have their own limit of 120 per minute.

List jobs#

GET/v1/jobs?limit=20&cursor=

Newest first, limit 1 to 100. Pass the response's next_cursor back as cursor for the next page. Add platform and source_id (the source.media_id of a job) together to see only the jobs for one video, in any mode. That is how a client checks for a transcript the workspace already owns before submitting again.

Cancel a job#

POST/v1/jobs/{id}/cancel

Cancels a job that has not started AI transcription yet; the hold is released. Later than that it returns 409 CANCELLATION_NOT_ALLOWED and the job finishes.

Retry a job#

POST/v1/jobs/{id}/retry

Creates a new job from a failed one whose error was retryable. Needs a new Idempotency-Key; optional body { "max_credits": 40 }. Priced like a new job. Failures that are final (private video, no captions) return 409 JOB_NOT_RETRYABLE: fix the input and submit again.

The transcript#

GET/v1/transcripts/{id}
iduuidOptional
Same as the job's result_id.
sourceobjectOptional
platform, media_id, canonical_url, title.
source_originstringOptional
creator_captions, platform_captions (YouTube automatic) or speech_recognition (AI transcription).
languagestring | nullOptional
BCP 47 tag of the text.
textstringOptional
The whole transcript as plain text.
segmentsarrayOptional
Caption-sized pieces: { start, end, text } in seconds.
wordsarray | nullOptional
{ start, end, text, confidence } per word. AI transcription only.
timing_granularity"word" | "segment" | "none"Optional
Finest timing available.
duration_secondsnumber | nullOptional
Media length.
recognitionobject | nullOptional
AI transcription only: model, profile, quality_status (validated_language: we benchmarked it; provider_supported: the model lists it; experimental_language: quality may vary).
created_atdate-timeOptional
UTC.
200 response
{
"id": "22222222-2222-4222-8222-222222222222",
"source": { "platform": "youtube", "media_id": "dQw4w9WgXcQ", "canonical_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "title": "Never Gonna Give You Up" },
"source_origin": "creator_captions",
"language": "en",
"text": "Never gonna give you up. Never gonna let you down.",
"segments": [
{ "start": 0, "end": 2.4, "text": "Never gonna give you up" },
{ "start": 2.4, "end": 4.9, "text": "Never gonna let you down" }
],
"words": null,
"timing_granularity": "segment",
"duration_seconds": 212.0,
"recognition": null,
"extraction_version": "td-options-v2",
"created_at": "2026-09-16T10:00:14.000Z"
}

Transcripts are kept for your plan's retention period (7 days on the trial, 30 on Starter, 90 on Pro and Scale). DELETE /v1/transcripts/{id} removes one earlier.

Export a transcript#

GET/v1/transcripts/{id}/export?format=srt
formatYou get
txtPlain text, one segment per line.
srtSubRip subtitles.
vttWebVTT subtitles.
jsonThe transcript object above.

Free, unlimited. srt and vtt need timings; a transcript with none returns 422 TIMESTAMPS_UNAVAILABLE.

Batches#

POST/v1/batches

One request, many videos. Each item has the same fields as Create a job; the whole batch is accepted or rejected together. Items per batch: 1 on the trial, 10 on Starter, 25 on Pro, 50 on Scale.

itemsJobRequest[]Required
1 to 50 job requests.
webhook_endpoint_iduuidOptional
Receives one batch.completed when every item is final.
metadataobjectOptional
Echoed in the batch.completed event.
POST /v1/batches
curl -X POST https://api.transcriptdock.com/v1/batches \
-H "Authorization: Bearer td_live_YOUR_KEY" \
-H "Idempotency-Key: playlist-2026-09-16" \
-H "Content-Type: application/json" \
-d '{ "items": [
{ "source": { "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }, "mode": "auto" },
{ "source": { "url": "https://www.tiktok.com/@tiktok/video/7680721699171601694" }, "mode": "auto" }
] }'

The batch object has status (queued, processing, succeeded, partial_success, failed, cancelled), counts (total_items, succeeded_items, failed_items, cancelled_items) and items, each with its job_id. Read it with GET /v1/batches/{id}; every item is a normal job.

Uploads#

Your own audio or video (mp3, wav, m4a, ogg, aac, mp4, webm; up to 250 MB and 2 hours). Reserve a slot, PUT the file to the signed URL, mark it complete, then submit it as a job with source.upload_id. Uploads always use AI transcription.

upload and transcribe
# 1. Reserve
curl -X POST https://api.transcriptdock.com/v1/uploads \
-H "Authorization: Bearer td_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "interview.mp3", "content_type": "audio/mpeg", "bytes": 48213920 }'
# -> { "id": "33333333-...", "signed_upload_url": "https://...", "expires_at": "...", "status": "pending" }
 
# 2. Upload the bytes (same Content-Type you declared)
curl -X PUT "SIGNED_UPLOAD_URL" -H "Content-Type: audio/mpeg" --data-binary @interview.mp3
 
# 3. Complete
curl -X POST https://api.transcriptdock.com/v1/uploads/33333333-3333-4333-8333-333333333333/complete \
-H "Authorization: Bearer td_live_YOUR_KEY"
 
# 4. Transcribe it
curl -X POST https://api.transcriptdock.com/v1/jobs \
-H "Authorization: Bearer td_live_YOUR_KEY" \
-H "Idempotency-Key: interview-01" \
-H "Content-Type: application/json" \
-d '{ "source": { "upload_id": "33333333-3333-4333-8333-333333333333" }, "mode": "transcribe" }'
filenamestringRequired
Up to 255 characters.
content_typestringRequired
The file's MIME type, e.g. audio/mpeg or video/mp4. Send the same value on the PUT.
bytesintegerRequired
Exact file size. The PUT must match.

The signed URL is valid for 24 hours; reserve a new slot after that.

Webhooks#

POST/v1/webhook-endpoints

Register an https URL and pass its id as webhook_endpoint_id when you submit. We POST an event when the job or batch finishes. The response includes the signing secret once.

urlstringRequired
https URL, up to 2048 characters.
descriptionstringOptional
A label for your own reference.

Events#

job.succeeded
{
"event_id": "44444444-4444-4444-8444-444444444444",
"type": "job.succeeded",
"created_at": "2026-09-16T10:00:14.000Z",
"job_id": "11111111-1111-4111-8111-111111111111",
"batch_id": null,
"result_id": "22222222-2222-4222-8222-222222222222",
"metadata": { "order": "8812" },
"error": null
}
  • job.succeeded: fetch or export result_id.
  • job.failed: error carries the code.
  • batch.completed: every item is final; read the batch for per item results.

Respond with any 2xx within a few seconds. Deliveries that fail are retried with backoff and can be resent from the dashboard. An event can arrive more than once: dedupe on event_id.

Verify the signature#

Headers X-TranscriptDock-Event-Id, X-TranscriptDock-Timestamp and X-TranscriptDock-Signature. The signature is HMAC-SHA256 (hex) of {event_id}.{timestamp}.{raw_body} with your secret. Reject anything older than five minutes.

import crypto from "node:crypto";
 
export function verify(eventId: string, timestamp: string, rawBody: string, signature: string, secret: string): boolean {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${eventId}.${timestamp}.${rawBody}`).digest("hex");
return expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Find videos#

POST/v1/video-discoveries

Search YouTube or list a channel, playlist or TikTok profile, then feed the URLs into jobs. A discovery runs in the background like a job: read it with GET /v1/video-discoveries/{id} until status is succeeded. 1 credit per page; results are kept for 24 hours.

kindstringRequired
youtube_search, youtube_channel_videos, youtube_channel_search, youtube_playlist_videos or tiktok_user_videos.
querystringOptional
Search text (youtube_search, youtube_channel_search).
channelstringOptional
@handle, channel id or channel URL (channel kinds).
playliststringOptional
Playlist id or URL (youtube_playlist_videos).
userstringOptional
@user or profile URL (tiktok_user_videos).
limitintegerOptional
Videos per page, 1 to 50, default 20. TikTok: at most 10.
cursorstringOptional
next_cursor from the previous page. YouTube only.
include_detailsbooleanOptional
TikTok only: also fetch likes, comments, hashtags and caption languages per video.
search YouTube
curl -X POST https://api.transcriptdock.com/v1/video-discoveries \
-H "Authorization: Bearer td_live_YOUR_KEY" \
-H "Idempotency-Key: search-async-rust-1" \
-H "Content-Type: application/json" \
-d '{ "kind": "youtube_search", "query": "async rust", "limit": 5 }'
200 response
{
"id": "55555555-5555-4555-8555-555555555555",
"kind": "youtube_search",
"status": "succeeded",
"videos": [
{
"platform": "youtube",
"video_id": "wXtngLBkK4Q",
"url": "https://www.youtube.com/watch?v=wXtngLBkK4Q",
"title": "Async Rust explained in 20 minutes",
"channel_title": "Let's Get Rusty",
"duration_s": 1155,
"published_text": "4 months ago",
"view_count": 76296,
"thumbnail_url": "https://i.ytimg.com/vi/wXtngLBkK4Q/hq720.jpg"
}
],
"profile": null,
"next_cursor": "opaque-token",
"error": null,
"expires_at": "2026-09-17T12:00:00.000Z"
}

Caption languages of a video#

POST/v1/language-discoveries

Body { "source": { "url": "https://www.youtube.com/watch?v=..." } } with an Idempotency-Key (YouTube only, 1 credit). Read GET /v1/language-discoveries/{id} for the list of caption tracks with their codes, whether they are creator or automatic, and the default. Use the codes in caption_languages.

Usage and capabilities#

GET/v1/usage
200 response
{
"plan": "pro",
"period_end": "2026-10-16T00:00:00.000Z",
"credits_remaining": 5840,
"credits_total": 6000,
"credits_reserved": 4,
"prepaid_credits": 0,
"transcribe_credits_per_minute": 2,
"minimum_billable_seconds": 60
}

credits_remaining already excludes credits_reserved (holds for running jobs). prepaid_credits are bought credits, which never expire.

GET/v1/capabilities

What your key can do right now: per source (youtube, tiktok, upload, direct) which modes are available, plus limits and prices. Read this instead of hardcoding; the trial, for example, reports auto and transcribe as false.

200 response
{
"youtube": { "enabled": true, "captions_only": true, "auto": true, "transcribe": true },
"tiktok": { "enabled": true, "captions_only": true, "auto": true, "transcribe": true },
"upload": { "enabled": true, "captions_only": false, "auto": true, "transcribe": true },
"direct": { "enabled": true, "captions_only": false, "auto": true, "transcribe": true },
"max_duration_ms": 7200000,
"max_media_bytes": 262144000,
"export_formats": ["txt", "json", "srt", "vtt"],
"transcribe_credits_per_minute": 2,
"minimum_billable_seconds": 60,
"free_caption_credits": 50
}

Rate limits#

PlanSubmits / minJobs in progressReads / min
Trial1010120
Starter60100120
Pro120500120
Scale3002,000120

Over a limit you get 429 RATE_LIMITED with a retry-after header (seconds) and details.detail naming the limit. Nothing is charged. Webhooks and ?wait=25 keep you far from the read limit.

Errors#

Every error has the same shape. retryable: true means the same request can succeed later: wait retry-after seconds if present, otherwise back off a few seconds, and reuse the same Idempotency-Key so nothing is duplicated. Any other error needs a change on your side.

402 response
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Not enough credits. Buy credits or upgrade your plan to continue.",
"retryable": false,
"details": { "detail": "This job needs more credits than you have left. Buy credits or upgrade your plan." },
"doc_url": "https://transcriptdock.com/docs/api-reference#error-codes"
},
"request_id": "66666666-6666-4666-8666-666666666666"
}

A job that fails after it was accepted is still 200 on GET /v1/jobs/{id}, with status: "failed" and the same error object in error. Nothing is charged for a failed job.

Codes#

CodeHTTPRetryMeaning and what to do
INVALID_REQUEST422NoA field is missing or has the wrong shape. details.detail names it.
INVALID_URL422NoThe URL is not a valid https link (max 2048 characters).
UNSUPPORTED_SOURCE422NoThe link is not a YouTube or TikTok video URL.
UNSAFE_URL422NoThe direct media link points at a private or blocked network address.
INVALID_CURSOR422NoThe cursor expired or is malformed. Start again from the first page.
CAPABILITY_UNAVAILABLE422NoThis mode is not available for this source. GET /v1/capabilities shows what is.
IDEMPOTENCY_CONFLICT409NoThis Idempotency-Key was already used with a different body. Use a new key.
UNAUTHENTICATED401NoNo valid API key in the Authorization header.
FORBIDDEN403NoThe key lacks the scope for this endpoint, or the workspace is disabled.
EMAIL_UNVERIFIED403NoVerify the account email before using the API.
NOT_FOUND404NoNo such object in this workspace.
INSUFFICIENT_BALANCE402NoNot enough credits for this job. Buy credits or upgrade.
PLAN_REQUIRED402NoThis needs a paid plan (AI transcription, larger batches) or the trial credits are used up.
BUDGET_EXCEEDED402NoThe measured cost is above max_credits. Nothing was charged; raise the cap or skip the media.
JOB_NOT_RETRYABLE409NoThe failure was final (private video, no captions). Fix the input and submit a new job.
CANCELLATION_NOT_ALLOWED409NoAI transcription already started; the job will finish.
RATE_LIMITED429YesOver a rate or queue limit. Wait retry-after seconds; details.detail says which limit.
SOURCE_NOT_FOUND422NoThe video does not exist or was removed.
SOURCE_PRIVATE422NoThe video is private. Only public videos work.
SOURCE_AUTH_REQUIRED422NoThe video needs a login, age check or membership.
SOURCE_REGION_RESTRICTED422NoThe video is blocked in the regions we fetch from.
NO_CAPTIONS422NoNo captions on this video and mode was captions_only. Use auto or transcribe.
LANGUAGE_UNAVAILABLE422NoNone of caption_languages exists on this video. Drop the list to take the default track.
LANGUAGE_UNSUPPORTED422NoAI transcription does not support the requested language.
NO_SPEECH422NoAI transcription found no speech in the audio.
NO_AUDIO422NoThe media has no audio track.
INVALID_MEDIA422NoThe file could not be read as audio or video.
UNSUPPORTED_MEDIA422NoNot an audio or video file type.
DURATION_LIMIT_EXCEEDED413NoLonger than 2 hours.
FILE_TOO_LARGE413NoLarger than 250 MB.
TIMESTAMPS_UNAVAILABLE422NoThis transcript has no timings, so srt and vtt exports are unavailable. Use txt or json.
PROVIDER_REJECTED422NoThe AI transcription model could not process this audio.
SOURCE_RATE_LIMITED503YesThe source platform is throttling us. The job retries automatically.
SOURCE_BLOCKED503YesThe source platform blocked the fetch. The job retries automatically.
SOURCE_TIMEOUT503YesThe source platform timed out. The job retries automatically.
SOURCE_CHANGED503YesThe source changed while we read it. The job retries automatically.
PROVIDER_UNAVAILABLE503YesAI transcription is temporarily unavailable. The job retries automatically.
STORAGE_UNAVAILABLE503YesFile storage is temporarily unavailable. Retry the request.
INTERNAL_ERROR500YesOur fault. Retry with the same Idempotency-Key; quote X-Request-Id if it persists.