TikTok Transcript API reference

Full reference for the TikTok Transcript API — turn a TikTok or YouTube video URL into a transcript. Base URL: https://painted-labs.com — auth with Authorization: Bearer sk_… (create keys in the dashboard). All endpoints speak JSON. Jobs are async: submit, then poll. Fields marked $ cost extra and are off by default.

POST /v1/jobs

Submit one video for transcription — or set metadataOnly: true for a free metadata-only pull. The spoken language is auto-detected — you don't (and can't) specify it.

urlrequiredstring

A TikTok or YouTube video URL.

metadataOnlyboolean · default false

Skip transcription (and its fee): title, description, duration, views, likes, comments, uploader, post date.

translateTo$string · default "none"

Also return the transcript translated to this language, e.g. "en", "es".

subtitleFormat"none" | "srt" | "vtt"

Also return a ready-to-save subtitle file built from the timestamps. Free.

includeSegmentsboolean · default true

Set false to leave the per-phrase timestamped segments out of the response.

curl -X POST https://painted-labs.com/v1/jobs \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.tiktok.com/@user/video/7234567890123456789", "translateTo": "en"}'
Response
{"id": "job_a1b2c3d4e5f6...", "status": "IN_QUEUE"}
GET /v1/jobs/:id

Poll a job until it reaches a terminal status: COMPLETED, FAILED, CANCELLED, or TIMED_OUT. Non-COMPLETED terminal jobs carry a human-readable error field and are automatically refunded.

translation/translatedTo appear only when you set translateTo; subtitles only with subtitleFormat; segments is omitted when includeSegments is false. A completed job with empty text and a positive duration means the audio was processed and contains no speech (music-only video) — that's a result, not an error. A metadataOnly job's output has no transcript; instead it carries the full metadata field set: title, description, duration, viewCount, likeCount, commentCount, uploader, uploaderId, postedAt.
curl https://painted-labs.com/v1/jobs/job_a1b2c3d4e5f6... \
  -H "Authorization: Bearer sk_..."
Response
{"id": "job_a1b2c3d4e5f6...", "status": "COMPLETED",
 "output": {
   "text": "full transcript ...",
   "language": "es",
   "duration": 42.7,
   "title": "...", "description": "...",
   "segments": [
     {"start": 0.0, "end": 3.2, "text": "first phrase"},
     {"start": 3.2, "end": 6.9, "text": "second phrase"}
   ],
   "translation": "full transcript, in English ...",
   "translatedTo": "en",
   "subtitles": "1\n00:00:00,000 --> 00:00:03,200\nfirst phrase\n..."
 }}
Engagement fields (viewCount, likeCount, commentCount, uploader, postedAt) come from metadataOnly pulls (they're free) — a transcription's output carries the transcript plus title/description/duration.
POST /v1/expand

Turn a whole TikTok profile, YouTube channel/playlist, or YouTube search into per-video results: every discovered video gets its own metadata pull — same rate as submitting each URL yourself. One call in, one batch id out; poll it below. A bare expand is metadata only.

urlstring

Profile/channel/playlist URL, or a bare TikTok @handle. Provide this or search.

searchstring

YouTube keyword search.

maxVideosPerSourceint · default 30, max 1000

How many videos to take from the source (alias limit).

postedAfter / postedBeforeYYYY-MM-DD

Only include videos posted in this range.

minViewsint

Only include videos with at least this many views.

sourceSorting"newest" | "mostViewed"

Order in which the source's videos are taken before the cap (alias sort).

transcribe$boolean · default false

Also transcribe every discovered video — billed per started minute, like /v1/jobs.

translateTo$string · default "none"

Also translate each video's transcript. Requires transcribe: true.

subtitleFormat / includeSegmentsas on /v1/jobs

Applied to each video.

Free tier: metadata only, capped at your remaining monthly video allowance. With credits, the batch is capped at what your balance covers — if credits run out mid-batch the remaining videos are marked skipped and nothing more is charged. Every discovered video is billed each time you expand — cache the results in your own store if you don't want to pay for them again.
curl -X POST https://painted-labs.com/v1/expand \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "@bromabakery", "maxVideosPerSource": 10, "sourceSorting": "mostViewed"}'
Response
{"id": "exp_9f2c...", "kind": "tiktok-profile", "scanned": 87, "total": 10}
GET /v1/expand/:id

Poll the whole batch with one call until status is done. Each completed video carries the full metadata field set — plus transcript, subtitles and translation when you asked for them.

Per-video status is one of pendingsubmittedcompleted / failed / skipped (out of credits or allowance). Batch views are kept in memory for a limited window — every submitted video also has a jobId that stays pollable at GET /v1/jobs/:jobId.
curl https://painted-labs.com/v1/expand/exp_9f2c... \
  -H "Authorization: Bearer sk_..."
Response
{"id": "exp_9f2c...", "kind": "tiktok-profile", "scanned": 87,
 "status": "done", "done": 10, "total": 10,
 "videos": [
   {"url": "https://www.tiktok.com/@bromabakery/video/7...", "status": "completed",
    "jobId": "job_a1b2c3d4e5f6...", "title": "...", "description": "...", "duration": 34,
    "viewCount": 128000, "likeCount": 9200, "commentCount": 310,
    "uploader": "Broma Bakery", "uploaderId": "@bromabakery",
    "postedAt": "2026-06-30T12:00:00.000Z"}, ...]}
GET /v1/usage

Your credit balance and month-to-date usage.

curl https://painted-labs.com/v1/usage \
  -H "Authorization: Bearer sk_..."
Response
{"creditsUsd": 9.94, "videos": 12, "transcripts": 0, "minutes": 4, "translations": 0,
 "freeLimits": {"videos": 25, "transcripts": 1}}
Errors & limits
401

Missing or invalid API key.

402

Free-tier cap reached or out of credits — buy a pack in the dashboard. A transcription of unknown length reserves its worst case up front (20 min × the minute rate) and refunds whatever the video doesn't use when it completes, so a low balance can be refused even for a short video.

422

The video is unavailable — deleted, private, or a wrong URL. Nothing is charged; retry only if you believe the video exists.

429

Rate limit (120 req/min), too many jobs in flight, or too many expansions running. Honor the Retry-After header.

5xx

Transient — retry with backoff.

Duration caps

Transcription handles videos up to 20 minutes (free tier: 15). Longer videos fail with a clear error before any GPU runs and are never charged. Metadata pulls have no duration limit.

Refunds

Failed, cancelled, or timed-out jobs — and completed transcriptions that produce no text — are refunded automatically, including in expansions. If a requested translation can't be delivered, the job carries a translationError field and the translation fee is not charged.