API v1 Reference
Create and monitor YouTube translation tasks with the VideoLingo API.
Overview
VideoLingo API v1 lets a Pro account create YouTube translation tasks and monitor them from server-side code. API tasks use the same processing pipeline, balance, billing rules, and dashboard task list as tasks created on the website. Their default name starts with API ·.
| Item | Value |
|---|---|
| Base URL | https://videolingo.io/api/v1 |
| Authentication | Authorization: Bearer <API_KEY> |
| Request and response format | JSON |
| Available operations | POST /tasks, GET /tasks/{id} |
| Required plan | Active Pro subscription, including the 1-day post-expiry grace period |
| Maximum video duration | 4 hours (14400 seconds) |
| Concurrent tasks | 3 queued or processing tasks per account, including website tasks |
API v1 currently accepts YouTube URLs only. It does not yet provide list, cancel, or webhook endpoints. Responses include Cache-Control: no-store and no CORS headers. Call the API from your server, not browser code; this also keeps your API key out of the client.
Get an API key
Sign in and open Dashboard → Account → API, or choose API Key from the avatar menu. An active Pro account receives one key beginning with vl_live_. You can reveal, copy, or regenerate it on that page.
Treat the key like a password. Never put it in client-side code, commit it to a repository, include it in a URL, log it, or share a screenshot containing it. Store it in a server-side secret manager or environment variable.
For the examples below:
export VL_API_KEY="vl_live_replace_with_your_key"Authentication
Send this header with every request:
Authorization: Bearer vl_live_xxxxxxxxxxxxxxxxCookies and query-string keys are ignored. A missing, malformed, regenerated, or unknown key returns 401 invalid_api_key. A valid key without an active Pro subscription returns 403 subscription_inactive.
Create a task
POST /tasksRequest headers
| Header | Required | Value and behavior |
|---|---|---|
Authorization | Yes | Bearer <API_KEY>. Do not send the key without the Bearer scheme. |
Content-Type | Yes | application/json. The request body must be valid JSON. |
Idempotency-Key | Recommended | Any stable 8–128 character value unique to this intended task, such as an order id or UUID. |
Idempotent retries
Use a new Idempotency-Key for each new video task. If a timeout or 5xx leaves the result uncertain, retry with the same key and exactly the same JSON body. Replays resolve to the same task and do not create a second charge once that task has started.
- A key belonging to a deleted task returns
422 invalid_request; use a new key. - A key outside 8–128 characters returns
422 invalid_request. - Do not reuse a key for different input or settings. API v1 does not promise payload-conflict detection for every pre-dispatch state.
- Without a key, every retry can create a separate task and charge.
Request body
Send one flat JSON object. Only youtube_url is required. Unknown fields are rejected, including server-controlled fields such as user_plan, watermark, prod, and model names.
| Field | Required | JSON type | Default | Rules and meaning |
|---|---|---|---|---|
youtube_url | Yes | string | — | HTTPS YouTube video URL, at most 2048 characters. Allowed hosts: youtube.com, www.youtube.com, m.youtube.com, youtu.be. Accepted shapes: /watch?v=…, youtu.be/…, /shorts/…, /embed/…, /live/…, /v/…. User info, custom ports, non-HTTPS URLs, playlists without a single video id, and non-YouTube hosts are rejected. |
name | No | string | API · <video id> | Dashboard task name. Leading and trailing whitespace is removed; the remaining value must be 1–200 characters. |
target_language | No | string enum | 简体中文 | Language of the translated subtitles and dub. Send one exact value from the target-language table below; these values are not locale-dependent labels. |
source_language | No | string enum | en | Spoken language of the source video, used for transcription. Send one exact code from the source-language table below. This is not automatic detection; set it explicitly when the video is not English. |
dubbing | No | boolean | false | false creates translated subtitles without a dub. true also generates a dubbed audio track. Send a JSON boolean, not the strings "true" or "false". |
tts_method | No | string enum | First preset voice for target_language | Dub voice. A fish_voice_… preset must belong to the selected target language; fish_clone uses the video's extracted reference voice. It affects audio only when dubbing is true. |
target_height | No | string enum | "1080" | Maximum YouTube download height: "480", "720", or "1080". It must be a JSON string, not the number 1080. The source video may be delivered at a lower available resolution. |
demucs | No | boolean | true | Whether to separate vocals from background audio before transcription and dubbing. Keeping it enabled generally helps videos with music or loud background sound, but adds processing work. |
glossary | No | string | omitted | Free-text terms or translation guidance for names and domain terminology, at most 250 characters. Be concise and include only terminology relevant to this video. |
target_language values
Use one of these exact values:
| Language | Exact target_language value |
|---|---|
| English | English |
| French | Français |
| German | Deutsch |
| Italian | Italiano |
| Spanish | Español |
| Russian | Русский |
| Korean | 한국어 |
| Japanese | 日本語 |
| Simplified Chinese | 简体中文 |
| Traditional Chinese | 繁體中文 |
| Cantonese | 粤语 |
source_language values
Use one of these exact values:
| Language | Exact source_language code |
|---|---|
| English | en |
| Simplified Chinese | zh |
| Traditional Chinese | zh-TW |
| French | fr |
| German | de |
| Italian | it |
| Spanish | es |
| Russian | ru |
| Japanese | ja |
Dubbing voice values
When dubbing is true, either omit tts_method to use the first preset for the selected target language, choose one matching fish_voice_<model-id> value below, or use fish_clone to clone the video's extracted reference voice.
The preset and target_language must come from the same row group. A French voice with target_language: "日本語", an unknown model id, or a retired value such as openai_tts is rejected with 422 invalid_request.
| Exact target_language value | Voice | Gender | Exact tts_method value |
|---|---|---|---|
English | Sarah | Female | fish_voice_933563129e564b19a115bedd57b7406a |
English | Selene | Female | fish_voice_b347db033a6549378b48d00acb0d06cd |
English | Adrian | Male | fish_voice_bf322df2096a46f18c579d0baa36f41d |
English | Ethan | Male | fish_voice_536d3a5e000945adb7038665781a4aca |
Français | Manon | Female | fish_voice_10a3a20742114a4ea6dd441e7591850f |
Français | Thérèse | Female | fish_voice_94702e20bb3e44d39c64ba74bb480dfc |
Français | Lucas | Male | fish_voice_f69bca092b674168a8d02d61ca20943c |
Français | Julien | Male | fish_voice_9af982ac4fbc4877af9d33b06e694fb9 |
Deutsch | Lena | Female | fish_voice_3937b44b604249f18b94913daef94043 |
Deutsch | Melanie | Female | fish_voice_d979e92f9c8a489fa581f55f6b9e48da |
Deutsch | Leon | Male | fish_voice_7b7c88bf825a4ab3bca27d55c26ddf16 |
Deutsch | Jonas | Male | fish_voice_44f1ec96a51e429f8b87776cbc55a997 |
Italiano | Chiara | Female | fish_voice_ef02f46e6a7540759f9d767d7e22c2e5 |
Italiano | Giulia | Female | fish_voice_0e36e185d0654d5bac3d24331562a0cc |
Italiano | Lorenzo | Male | fish_voice_59ac2b0ad66641b5ab4666d3ed89e5d4 |
Italiano | Marco | Male | fish_voice_f6cbad8b89254774a495c088aeb7822e |
Español | Carmen | Female | fish_voice_3f065d660212478a9c697fa965b4d098 |
Español | Elena | Female | fish_voice_566359628f6a4d5fabf902f6f64ecec1 |
Español | Pablo | Male | fish_voice_1bd666ea8ada44c789e0fec21cf78f33 |
Español | Andrés | Male | fish_voice_d85aae3c4f4d4c70860997c27554f533 |
Русский | Полина Polina | Female | fish_voice_bf75c2e80b0e441485eeec20330a75fa |
Русский | Maria | Female | fish_voice_7adab54c80d34cba8c11463ab1502c9a |
Русский | Артём Artyom | Male | fish_voice_b871e425b6c0442baa5cf73f6c336bbe |
Русский | Dmitri | Male | fish_voice_2911dfe1c9d54419b24ed8c4a1489355 |
한국어 | 하윤 Hayoon | Female | fish_voice_96ed4e3f47334430b990df471d0a0268 |
한국어 | Yuna | Female | fish_voice_3d31499f0e13438bbce8dcce7b7c4298 |
한국어 | 지훈 Jihoon | Male | fish_voice_1faa840246094164bf4b0b0b94ae1675 |
한국어 | Minjun | Male | fish_voice_82703151e4b043dbbd98f082d4841b71 |
日本語 | きょうこ(カスタマーサポート) | Female | fish_voice_b2d9d8db057042688a5e318b8f405bc2 |
日本語 | しおり(ナレーション) | Female | fish_voice_5da7f24e9e274f91b2b677669c818ce9 |
日本語 | さとる(ナレーション) | Male | fish_voice_297a6fd278df47c3b9da9bfdf55ac89a |
日本語 | じん(ラジオパーソナリティ) | Male | fish_voice_d738c70d206149e69f8e3b6fc8af9d96 |
简体中文 | 语彤 Yutong | Female | fish_voice_74c6aba5cbf94a15bbdc547ffce5cb38 |
简体中文 | 冰冰 Bingbing | Female | fish_voice_e98fa6cdad6946bf8d9bb8f9cb8c2532 |
简体中文 | 梓轩 Zixuan | Male | fish_voice_5d29a99739c14d4ca3e4fe42193105b2 |
简体中文 | 浩然 Haoran | Male | fish_voice_d675c275d1d44e57b4ef3840c5a23209 |
繁體中文 | 詩涵 Shihan(台灣) | Female | fish_voice_91ec588cf8ef443a9c0d5b21d0c1fa36 |
繁體中文 | Ya-Ting | Female | fish_voice_49066cb45f0a427484ade915a4474534 |
繁體中文 | 承翰 Chenghan(台灣) | Male | fish_voice_1e85fd1e0d3e4cc2b79fbca800e7e3fe |
繁體中文 | Chia-Hao | Male | fish_voice_e7af89da8abc4fbdacd9a10ab9881933 |
粤语 | 詠詩 Wing Sze | Female | fish_voice_50c586f7c811466cac751c28c3e4a671 |
粤语 | VideoLingo 粵語女聲 | Female | fish_voice_da2d934b77df4561ac51f8ed3edfaa2b |
粤语 | 俊傑 Chun Kit | Male | fish_voice_fc917a4b147145258519982fe306b2cf |
粤语 | VideoLingo 粵語男聲 | Male | fish_voice_a197cea20d84442d8462253a7ddf08a3 |
Minimal subtitle-only example
curl --request POST 'https://videolingo.io/api/v1/tasks' \
--header "Authorization: Bearer $VL_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-8fa1a2b3' \
--data '{
"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"source_language": "en",
"target_language": "简体中文",
"dubbing": false
}'Dubbed example with an explicit voice
curl --request POST 'https://videolingo.io/api/v1/tasks' \
--header "Authorization: Bearer $VL_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-4c5d6e7f' \
--data '{
"youtube_url": "https://youtu.be/dQw4w9WgXcQ",
"name": "Japanese dubbed launch video",
"source_language": "en",
"target_language": "日本語",
"dubbing": true,
"tts_method": "fish_voice_b2d9d8db057042688a5e318b8f405bc2",
"target_height": "1080",
"demucs": true,
"glossary": "VideoLingo: keep the product name in Latin characters"
}'What happens before acceptance
- The API authenticates the key and validates the entire JSON body. Unknown or mismatched values fail before processing.
- It reads the public video's duration. This check can take up to about 20 seconds. An inaccessible/private video returns
422 duration_unavailable; a video longer than 4 hours returns422 invalid_request. - It checks the shared concurrency limit and available balance, reserves the task cost, and dispatches processing.
Success returns 202 Accepted and the task object. Its initial status is normally queued or processing.
Get a task
GET /tasks/{id}Use the UUID returned by POST /tasks:
curl 'https://videolingo.io/api/v1/tasks/TASK_ID' \
--header "Authorization: Bearer $VL_API_KEY"The same endpoint can read a task created on the website if it belongs to the authenticated account. An invalid UUID, unknown task, deleted task, or another user's task returns the same 404 not_found response so task ownership is not disclosed.
Poll every 15–30 seconds until status is completed, failed, or cancelled. A read of an in-flight task also refreshes its latest processing state.
Task object
Both successful endpoints return the same shape:
{
"id": "3f2c7a1e-5b8d-4c6f-9a0b-1d2e3f4a5b6c",
"name": "API · dQw4w9WgXcQ",
"status": "processing",
"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"video_duration_seconds": 212,
"cost_usd": 0.294444,
"step": "Translating text",
"step_index": 6,
"error": null,
"result": null,
"created_at": "2026-09-24T08:00:00.000Z",
"updated_at": "2026-09-24T08:03:12.000Z"
}| Field | Type | When present and how to use it |
|---|---|---|
id | string (UUID) | Stable task identifier. Persist this for later status requests. |
name | string | Task name shown in the dashboard. |
status | string enum | Lifecycle state. Branch on this field; see the table below. |
youtube_url | string | null | Source URL. It is null for a local-file task created on the website. |
video_duration_seconds | number | null | Source duration in seconds once known. |
cost_usd | number | null | Reserved task cost in USD: video duration × USD 5 per hour. Subtitle-only and dubbed tasks use the same rate. It is null until duration is known. |
step | string | null | Human-readable current pipeline stage while processing. Display it; do not build business logic around its wording. |
step_index | number | null | Stable numeric progress stage for UI progress. See the stage table below. |
error | string | null | Human-readable processing failure reason when status is failed. It is not the same as an HTTP API error object. |
result | object | null | Download URLs when status is completed; otherwise null. |
created_at | string | ISO 8601 UTC creation timestamp. |
updated_at | string | ISO 8601 UTC timestamp of the latest stored task update. |
status values
| Value | Terminal | Meaning and client action |
|---|---|---|
draft | No | Not dispatched. A website draft or a task whose create call returned 503 dispatch_failed; retry that create call with the same idempotency key. |
queued | No | Accepted and waiting for processing capacity. Continue polling. |
processing | No | Running. Use step and step_index for display and continue polling. |
completed | Yes | Finished successfully. Read download URLs from result. |
failed | Yes | Processing failed. Read error; submitting again is a new task and should use a new idempotency key unless the original create request itself returned 503. |
cancelled | Yes | Stopped. No result will be produced. |
step and step_index
step_index | Subtitle-only task | Dubbed task |
|---|---|---|
| 0 | Preparing input video | Preparing input video |
| 1 | Extracting audio using Demucs | Extracting audio using Demucs |
| 2 | Transcribing using Whisper | Transcribing using Whisper |
| 3 | Splitting sentences using spaCy | Splitting sentences using spaCy |
| 4 | Splitting sentences by meaning | Splitting sentences by meaning |
| 5 | Summarizing content | Summarizing content |
| 6 | Translating text | Translating text |
| 7 | Splitting for subtitles | Splitting for subtitles |
| 8 | Aligning timestamps | Aligning timestamps |
| 9 | Merging subtitles | Reserved / skipped |
| 10 | Uploading results | Generating audio tasks |
| 11 | — | Extracting reference audio |
| 12 | — | Generating audio using TTS |
| 13 | — | Merging full audio |
| 14 | — | Merging dub audio |
| 15 | — | Uploading results |
Older dubbed tasks may briefly report the previously used stage 9. For display, prefer the accompanying step text when it is present.
result
On completion, result contains four direct-download URLs:
| Field | File |
|---|---|
video | Final MP4 video. |
subtitles_zip | ZIP archive containing all subtitle files. |
translated_srt | Translated SRT subtitles. |
source_srt | Source-language SRT subtitles. |
{
"result": {
"video": "https://cloud.videolingo.io/tasks/3f2c7a1e-…/output.mp4",
"subtitles_zip": "https://cloud.videolingo.io/tasks/3f2c7a1e-…/srt.zip",
"translated_srt": "https://cloud.videolingo.io/tasks/3f2c7a1e-…/trans.srt",
"source_srt": "https://cloud.videolingo.io/tasks/3f2c7a1e-…/src.srt"
}
}Download URLs are task artifacts, not a completion signal by themselves. Wait for status: "completed" before using them.
Errors and retry behavior
HTTP errors always use this envelope. Branch on error.code; error.message is explanatory text and may change.
{
"error": {
"code": "concurrency_limit",
"message": "At most 3 tasks can be queued or processing at once",
"active": 3,
"limit": 3
}
}| HTTP | error.code | Meaning | Client action |
|---|---|---|---|
| 401 | invalid_api_key | Missing, malformed, regenerated, or unknown key. | Do not retry unchanged. Fix the header or deploy the current key. |
| 403 | subscription_inactive | The account is not on an active Pro plan or is more than 1 day past expiry. | Renew/upgrade, then retry; the same key becomes usable again. |
| 404 | not_found | Task is invalid, missing, deleted, or belongs to another account. | Check the id and account. Do not repeatedly retry. |
| 422 | invalid_request | Invalid JSON/field/value, unknown field, invalid YouTube URL, video over 4 hours, invalid idempotency key, or a key tied to a deleted task. | Fix the request. Use a new key only if the old key belongs to a deleted task or the intended task changed. |
| 422 | duration_unavailable | Duration could not be read because the video is private, missing, restricted, or temporarily inaccessible. | Verify public access. Retry later with the same key only if the URL is unchanged. |
| 402 | insufficient_balance | Available balance is below the task cost. Extra fields: balance, cost in USD. | Add balance, then submit the same task with the same key. No task or charge was created by this rejection. |
| 429 | concurrency_limit | Three tasks are already queued or processing. Extra fields: active, limit. | Wait for a slot, then retry with the same key. No task or charge was created by this rejection. |
| 503 | dispatch_failed | Processing temporarily refused the task. Extra field: task_id. | Retry with exponential backoff and the same key/body. The task remains draft. |
| 500 | internal_error | Unexpected server error. | Retry with exponential backoff and the same key/body because the outcome may be uncertain. |
Suggested transient-error backoff: 2 s, 5 s, 10 s, 30 s, then stop and surface the error. Honor a future Retry-After header if present. Do not automatically retry 401, 403, 404, or general 422 errors without first changing the underlying condition.
Limits and billing
- Concurrency: at most 3 tasks can be
queuedorprocessingfor one account. Tasks started on the website count toward the same limit. - Duration: one video can be at most 4 hours (
14400seconds). - Billing: subtitle translation and dubbing use the same video-duration rate of USD 5 per hour. The resulting
cost_usdis reserved from account balance when processing starts. - Polling: there is currently no per-minute request quota, but poll responsibly every 15–30 seconds. Increase the interval for long videos and stop on a terminal status.
- Task creation:
202 Acceptedmeans processing was accepted, not completed. Billing and artifacts belong to that asynchronous task.
Complete polling example
This Bash example requires curl and jq. It preserves the idempotency key, fails on API errors, and stops only on a terminal task status.
#!/usr/bin/env bash
set -euo pipefail
: "${VL_API_KEY:?Set VL_API_KEY first}"
API='https://videolingo.io/api/v1'
IDEMPOTENCY_KEY="video-$(date +%s)-$$"
RESPONSE=$(curl --silent --show-error --fail-with-body \
--request POST "$API/tasks" \
--header "Authorization: Bearer $VL_API_KEY" \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data '{
"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"source_language": "en",
"target_language": "简体中文",
"dubbing": false
}')
TASK_ID=$(jq --raw-output '.id // empty' <<<"$RESPONSE")
test -n "$TASK_ID" || { printf 'No task id: %s\n' "$RESPONSE" >&2; exit 1; }
printf 'Task: %s\n' "$TASK_ID"
while true; do
RESPONSE=$(curl --silent --show-error --fail-with-body \
"$API/tasks/$TASK_ID" \
--header "Authorization: Bearer $VL_API_KEY")
STATUS=$(jq --raw-output '.status' <<<"$RESPONSE")
STEP=$(jq --raw-output '.step // ""' <<<"$RESPONSE")
printf '%s %s\n' "$STATUS" "$STEP"
case "$STATUS" in
completed) jq '.result' <<<"$RESPONSE"; break ;;
failed|cancelled) jq '{status, error}' <<<"$RESPONSE" >&2; exit 1 ;;
queued|processing|draft) sleep 30 ;;
*) printf 'Unknown status: %s\n' "$STATUS" >&2; exit 1 ;;
esac
doneRotate an API key
- Choose Regenerate key on Dashboard → Account → API.
- The old key stops working immediately and returns
401; running tasks are not interrupted. - Update every server, CI secret, and integration that uses the key before removing your saved copy.
- Regenerate immediately if exposure is suspected. Each account has only one active key at a time.
- When Pro expires the key returns
403. Renewing reactivates the same key, so routine subscription renewal does not require rotation.