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. Submitcurl -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. Exportcurl "https://api.transcriptdock.com/v1/transcripts/22222222-2222-4222-8222-222222222222/export?format=srt" \-H "Authorization: Bearer td_live_YOUR_KEY"
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.
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#
| Method | Path | What it does | Credits |
|---|---|---|---|
| POST | /v1/jobs | Transcribe one video, file or link | 1 per caption transcript, 2 per minute of AI transcription |
| GET | /v1/jobs/{id} | Job status, waits up to 25 s | Free |
| GET | /v1/jobs | Job history | Free |
| POST | /v1/jobs/{id}/cancel | Cancel a queued job | Free |
| POST | /v1/jobs/{id}/retry | Retry a failed job | As a new job |
| POST | /v1/batches | Transcribe up to 50 videos | Per item, as above |
| GET | /v1/batches/{id} | Batch status | Free |
| GET | /v1/transcripts/{id} | Transcript JSON | Free |
| GET | /v1/transcripts/{id}/export | txt, srt, vtt or json file | Free |
| DELETE | /v1/transcripts/{id} | Delete a transcript | Free |
| POST | /v1/uploads | Get a URL to upload a file to | Free |
| POST | /v1/uploads/{id}/complete | Mark the upload finished | Free |
| POST | /v1/video-discoveries | Search YouTube, list a channel, playlist or TikTok profile | 1 per page |
| GET | /v1/video-discoveries/{id} | Discovery result | Free |
| POST | /v1/language-discoveries | List a YouTube video's caption languages | 1 |
| GET | /v1/language-discoveries/{id} | Language list | Free |
| POST | /v1/webhook-endpoints | Register a webhook URL | Free |
| GET | /v1/webhook-endpoints | List webhook URLs | Free |
| DELETE | /v1/webhook-endpoints/{id} | Disable a webhook URL | Free |
| GET | /v1/usage | Credits and plan | Free |
| GET | /v1/capabilities | What your key can do | Free |
Create a job#
/v1/jobsReturns 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").
{"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#
Get a job#
/v1/jobs/{id}?wait=25Returns 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#
/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#
/v1/jobs/{id}/cancelCancels 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#
/v1/jobs/{id}/retryCreates 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#
/v1/transcripts/{id}{"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#
/v1/transcripts/{id}/export?format=srt| format | You get |
|---|---|
| txt | Plain text, one segment per line. |
| srt | SubRip subtitles. |
| vtt | WebVTT subtitles. |
| json | The transcript object above. |
Free, unlimited. srt and vtt need timings; a transcript with none returns 422 TIMESTAMPS_UNAVAILABLE.
Batches#
/v1/batchesOne 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.
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.
# 1. Reservecurl -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. Completecurl -X POST https://api.transcriptdock.com/v1/uploads/33333333-3333-4333-8333-333333333333/complete \-H "Authorization: Bearer td_live_YOUR_KEY"# 4. Transcribe itcurl -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" }'
The signed URL is valid for 24 hours; reserve a new slot after that.
Webhooks#
/v1/webhook-endpointsRegister 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.
Events#
{"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 exportresult_id.job.failed:errorcarries 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#
/v1/video-discoveriesSearch 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.
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 }'
{"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#
/v1/language-discoveriesBody { "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#
/v1/usage{"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.
/v1/capabilitiesWhat 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.
{"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#
| Plan | Submits / min | Jobs in progress | Reads / min |
|---|---|---|---|
| Trial | 10 | 10 | 120 |
| Starter | 60 | 100 | 120 |
| Pro | 120 | 500 | 120 |
| Scale | 300 | 2,000 | 120 |
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.
{"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#
| Code | HTTP | Retry | Meaning and what to do |
|---|---|---|---|
INVALID_REQUEST | 422 | No | A field is missing or has the wrong shape. details.detail names it. |
INVALID_URL | 422 | No | The URL is not a valid https link (max 2048 characters). |
UNSUPPORTED_SOURCE | 422 | No | The link is not a YouTube or TikTok video URL. |
UNSAFE_URL | 422 | No | The direct media link points at a private or blocked network address. |
INVALID_CURSOR | 422 | No | The cursor expired or is malformed. Start again from the first page. |
CAPABILITY_UNAVAILABLE | 422 | No | This mode is not available for this source. GET /v1/capabilities shows what is. |
IDEMPOTENCY_CONFLICT | 409 | No | This Idempotency-Key was already used with a different body. Use a new key. |
UNAUTHENTICATED | 401 | No | No valid API key in the Authorization header. |
FORBIDDEN | 403 | No | The key lacks the scope for this endpoint, or the workspace is disabled. |
EMAIL_UNVERIFIED | 403 | No | Verify the account email before using the API. |
NOT_FOUND | 404 | No | No such object in this workspace. |
INSUFFICIENT_BALANCE | 402 | No | Not enough credits for this job. Buy credits or upgrade. |
PLAN_REQUIRED | 402 | No | This needs a paid plan (AI transcription, larger batches) or the trial credits are used up. |
BUDGET_EXCEEDED | 402 | No | The measured cost is above max_credits. Nothing was charged; raise the cap or skip the media. |
JOB_NOT_RETRYABLE | 409 | No | The failure was final (private video, no captions). Fix the input and submit a new job. |
CANCELLATION_NOT_ALLOWED | 409 | No | AI transcription already started; the job will finish. |
RATE_LIMITED | 429 | Yes | Over a rate or queue limit. Wait retry-after seconds; details.detail says which limit. |
SOURCE_NOT_FOUND | 422 | No | The video does not exist or was removed. |
SOURCE_PRIVATE | 422 | No | The video is private. Only public videos work. |
SOURCE_AUTH_REQUIRED | 422 | No | The video needs a login, age check or membership. |
SOURCE_REGION_RESTRICTED | 422 | No | The video is blocked in the regions we fetch from. |
NO_CAPTIONS | 422 | No | No captions on this video and mode was captions_only. Use auto or transcribe. |
LANGUAGE_UNAVAILABLE | 422 | No | None of caption_languages exists on this video. Drop the list to take the default track. |
LANGUAGE_UNSUPPORTED | 422 | No | AI transcription does not support the requested language. |
NO_SPEECH | 422 | No | AI transcription found no speech in the audio. |
NO_AUDIO | 422 | No | The media has no audio track. |
INVALID_MEDIA | 422 | No | The file could not be read as audio or video. |
UNSUPPORTED_MEDIA | 422 | No | Not an audio or video file type. |
DURATION_LIMIT_EXCEEDED | 413 | No | Longer than 2 hours. |
FILE_TOO_LARGE | 413 | No | Larger than 250 MB. |
TIMESTAMPS_UNAVAILABLE | 422 | No | This transcript has no timings, so srt and vtt exports are unavailable. Use txt or json. |
PROVIDER_REJECTED | 422 | No | The AI transcription model could not process this audio. |
SOURCE_RATE_LIMITED | 503 | Yes | The source platform is throttling us. The job retries automatically. |
SOURCE_BLOCKED | 503 | Yes | The source platform blocked the fetch. The job retries automatically. |
SOURCE_TIMEOUT | 503 | Yes | The source platform timed out. The job retries automatically. |
SOURCE_CHANGED | 503 | Yes | The source changed while we read it. The job retries automatically. |
PROVIDER_UNAVAILABLE | 503 | Yes | AI transcription is temporarily unavailable. The job retries automatically. |
STORAGE_UNAVAILABLE | 503 | Yes | File storage is temporarily unavailable. Retry the request. |
INTERNAL_ERROR | 500 | Yes | Our fault. Retry with the same Idempotency-Key; quote X-Request-Id if it persists. |