TikTok API endpoint
Returns one video's own captions translated into the requested language, as TikTok's caption/language (`cla`) service serves them. The response carries the track twice: `webvtt` is the upstream WebVTT text verbatim, and `cues` is the same track parsed into `{start_ms, end_ms, start, end, text}` records so a caller does not have to implement WebVTT. `video_id` is the aweme id returned by the feed, search, profile-posts and hashtag endpoints. Measured 2026-10-21 with the pure-Go (device-free) signer: the Korean-caption video 7696141388324293918 answered 11 English cues for `language=en`, and the same video answered Japanese, Chinese, Spanish, French, German, Korean, Portuguese, Russian, Arabic, Hindi, Indonesian, Thai, Vietnamese, Turkish and Italian tracks; a Portuguese-lyrics video answered an 18,494-character English track (240 cues). Track size is a snapshot rather than a contract (the upstream re-translates from a cache), so assert a non-empty track in the requested language, not a byte count. The route is not signature-gated, so it does not need the device-backed signer. A video with no captions answers 400 (upstream status 5); a nonexistent video answers 500 (upstream status 4).
/tiktok/mobile/video/subtitlesOne video's own captions, translated into a language you choose, as TikTok's caption/language (cla) service
serves them. The upstream is GET /tiktok/cla/subtitle_translation/get/v1/, captured from the running 47.4.3 app
when the caption-translation control is used on a video and reproduced end-to-end against the live gateway.
Both a track and its cues. The response carries the captions twice: webvtt is the upstream WebVTT text
verbatim, and cues is the same track parsed into {start_ms, end_ms, start, end, text} records, so a caller can
build a transcript, burn subtitles into a render, or search caption text without implementing WebVTT. cue_count
is the parsed cue count. Multi-line cues keep their line breaks in text.
Keyed by video id. video_id is the numeric aweme id that the feed, search, profile-posts and hashtag
endpoints already return. You do not need TikTok's internal subtitle_id: the app sends one alongside the
video id, but the server does not validate its value (measured: random values and even 1 returned the identical
track), so this API sends the video id for it and hides it from callers.
Languages. language is required and validated upstream. Measured working codes (2026-10-21): ar, de,
en, es, fr, hi, id, it, ja, ko, pt, ru, th, tr, vi, zh. Anything else is rejected
locally with a 400 instead of paying a round trip for the upstream's status 5.
Device-free. The route is not signature-value gated: the pure-Go placeholder signer returned the same real
tracks, so a deployment needs TIKTOK_MOBILE_PARAMS but not the device-backed signer
(TIKTOK_MOBILE_SIGNER_URL). It answers on the core aggr16-normal.tiktokv.us / api16-normal-useast5.tiktokv.us
pair.
Query parameters:
video_id: required. The numeric aweme id of the video.language: required. Target language code from the measured set above.Errors:
| call | result |
|---|---|
video_id=7696141388324293918 (Korean speech), language=en | 766-character track, 11 cues, English text: "Our family is finally together again after so long." |
same video, language=ja / zh / es / fr / de / ko | 772 / 644 / 709 / 792 / 775 / 644-character tracks, each in the requested language |
same video, language=pt / ru / ar / hi / id / th / vi / tr / it | 715 / 946 / 861 / 1,101 / 755 / 1,017 / 834 / 709 / 720-character tracks |
video_id=7697794478630800660 (Portuguese lyrics), language=en | 18,494-character track, 240 cues |
same video, language=zh | 16,914-character translated track, 240 cues |
| caption-less video | upstream status 5 → 400 |
language=xx | upstream status 5 → rejected locally as 400 |
| nonexistent video | upstream status 4 → 500 |
| pure-Go placeholder signer | the same real tracks — device-free |
Example request:
GET /api/v1/tiktok/mobile/video/subtitles?video_id=7696141388324293918&language=en
The compact example shows two cues; the measured live track returned 11.
{
"code": 200,
"msg": "OK",
"data": {
"video_id": "7696141388324293918",
"language": "en",
"cue_count": 2,
"cues": [
{"start_ms": 80, "end_ms": 880, "start": "00:00:00.080", "end": "00:00:00.880", "text": "Hey."},
{"start_ms": 3080, "end_ms": 4880, "start": "00:00:03.080", "end": "00:00:04.880", "text": "Our family is finally together again after so long."}
],
"webvtt": "WEBVTT\n\n00:00:00.080 --> 00:00:00.880\nHey.\n\n00:00:03.080 --> 00:00:04.880\nOur family is finally together again after so long.",
"variant": "glide_v3_1",
"content_source": "redis",
"host": "aggr16-normal.tiktokv.us",
"fetched_at": "2026-10-21T10:56:17Z"
}
}
Notes for callers:
content_source reported redis/db, translation_cache_time
7–13); measured latency ranged ~0.4–3.9 s per call across measurements.variant is the upstream's translation-pipeline tag and is omitted when the cache returns a track without one.| Parameter | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
| x-api-key (header) | string | Yes | API key required | ||
| video_id | string | Yes | Numeric id of the video (aweme id) | 7696141388324293918 | |
| language | string | Yes | Target language code: ar, de, en, es, fr, hi, id, it, ja, ko, pt, ru, th, tr, vi, zh | en |
curl -X GET "https://api.crawlora.net/api/v1/tiktok/mobile/video/subtitles?video_id=7696141388324293918&language=en" \ -H "x-api-key: $CRAWLORA_API_KEY"
{
"code": 200,
"msg": "OK",
"data": {
"video_id": "7696141388324293918",
"language": "en",
"cue_count": 2,
"cues": [
{
"start_ms": 80,
"end_ms": 880,
"start": "00:00:00.080",
"end": "00:00:00.880",
"text": "Hey."
},
{
"start_ms": 3080,
"end_ms": 4880,
"start": "00:00:03.080",
"end": "00:00:04.880",
"text": "Our family is finally together again after so long."
}
],
"webvtt": "WEBVTT\n\n00:00:00.080 --> 00:00:00.880\nHey.\n\n00:00:03.080 --> 00:00:04.880\nOur family is finally together again after so long.",
"variant": "glide_v3_1",
"content_source": "redis",
"host": "aggr16-normal.tiktokv.us",
"fetched_at": "2026-10-21T10:56:17Z"
}
}Request schema
No body schema
Response schema
#/definitions/tiktokmobile.videoSubtitlesResponseDoc
| Field | Type | Required | Enum | Bounds | Example | Description |
|---|---|---|---|---|---|---|
| content_source | string | No | redis | |||
| cue_count | integer | No | 9 | |||
| cues | array | No | ||||
| cues[].end | string | No | ||||
| cues[].end_ms | integer | No | ||||
| cues[].start | string | No | ||||
| cues[].start_ms | integer | No | ||||
| cues[].text | string | No | ||||
| fetched_at | string | No | ||||
| host | string | No | aggr16-normal.tiktokv.us | |||
| language | string | No | en | |||
| variant | string | No | glide_v3_1 | |||
| video_id | string | No | 7696141388324293918 | |||
| webvtt | string | No | WEBVTT 00:00:00.080 --> 00:00:00.880 Hey. |
| Status | Description | Schema |
|---|---|---|
| 400 | Invalid input, or the video has no captions | #/definitions/app.Response |
| 429 | Rate limit exceeded | #/definitions/app.Response |
| 500 | Internal server error | #/definitions/app.Response |
Use environment variables for secrets and keep Crawlora API keys server-side.
curl -X GET "https://api.crawlora.net/api/v1/tiktok/mobile/video/subtitles?video_id=7696141388324293918&language=en" \
-H "x-api-key: $CRAWLORA_API_KEY"