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 ·.

ItemValue
Base URLhttps://videolingo.io/api/v1
AuthenticationAuthorization: Bearer <API_KEY>
Request and response formatJSON
Available operationsPOST /tasks, GET /tasks/{id}
Required planActive Pro subscription, including the 1-day post-expiry grace period
Maximum video duration4 hours (14400 seconds)
Concurrent tasks3 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_xxxxxxxxxxxxxxxx

Cookies 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 /tasks

Request headers

HeaderRequiredValue and behavior
AuthorizationYesBearer <API_KEY>. Do not send the key without the Bearer scheme.
Content-TypeYesapplication/json. The request body must be valid JSON.
Idempotency-KeyRecommendedAny 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.

FieldRequiredJSON typeDefaultRules and meaning
youtube_urlYesstring—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.
nameNostringAPI · <video id>Dashboard task name. Leading and trailing whitespace is removed; the remaining value must be 1–200 characters.
target_languageNostring 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_languageNostring enumenSpoken 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.
dubbingNobooleanfalsefalse creates translated subtitles without a dub. true also generates a dubbed audio track. Send a JSON boolean, not the strings "true" or "false".
tts_methodNostring enumFirst preset voice for target_languageDub 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_heightNostring 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.
demucsNobooleantrueWhether 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.
glossaryNostringomittedFree-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:

LanguageExact target_language value
EnglishEnglish
FrenchFrançais
GermanDeutsch
ItalianItaliano
SpanishEspañol
RussianРусский
Korean한국어
Japanese日本語
Simplified Chinese简体中文
Traditional Chinese繁體中文
Cantonese粤语

source_language values

Use one of these exact values:

LanguageExact source_language code
Englishen
Simplified Chinesezh
Traditional Chinesezh-TW
Frenchfr
Germande
Italianit
Spanishes
Russianru
Japaneseja

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 valueVoiceGenderExact tts_method value
EnglishSarahFemalefish_voice_933563129e564b19a115bedd57b7406a
EnglishSeleneFemalefish_voice_b347db033a6549378b48d00acb0d06cd
EnglishAdrianMalefish_voice_bf322df2096a46f18c579d0baa36f41d
EnglishEthanMalefish_voice_536d3a5e000945adb7038665781a4aca
FrançaisManonFemalefish_voice_10a3a20742114a4ea6dd441e7591850f
FrançaisThérèseFemalefish_voice_94702e20bb3e44d39c64ba74bb480dfc
FrançaisLucasMalefish_voice_f69bca092b674168a8d02d61ca20943c
FrançaisJulienMalefish_voice_9af982ac4fbc4877af9d33b06e694fb9
DeutschLenaFemalefish_voice_3937b44b604249f18b94913daef94043
DeutschMelanieFemalefish_voice_d979e92f9c8a489fa581f55f6b9e48da
DeutschLeonMalefish_voice_7b7c88bf825a4ab3bca27d55c26ddf16
DeutschJonasMalefish_voice_44f1ec96a51e429f8b87776cbc55a997
ItalianoChiaraFemalefish_voice_ef02f46e6a7540759f9d767d7e22c2e5
ItalianoGiuliaFemalefish_voice_0e36e185d0654d5bac3d24331562a0cc
ItalianoLorenzoMalefish_voice_59ac2b0ad66641b5ab4666d3ed89e5d4
ItalianoMarcoMalefish_voice_f6cbad8b89254774a495c088aeb7822e
EspañolCarmenFemalefish_voice_3f065d660212478a9c697fa965b4d098
EspañolElenaFemalefish_voice_566359628f6a4d5fabf902f6f64ecec1
EspañolPabloMalefish_voice_1bd666ea8ada44c789e0fec21cf78f33
EspañolAndrésMalefish_voice_d85aae3c4f4d4c70860997c27554f533
РусскийПолина PolinaFemalefish_voice_bf75c2e80b0e441485eeec20330a75fa
РусскийMariaFemalefish_voice_7adab54c80d34cba8c11463ab1502c9a
РусскийАртём ArtyomMalefish_voice_b871e425b6c0442baa5cf73f6c336bbe
РусскийDmitriMalefish_voice_2911dfe1c9d54419b24ed8c4a1489355
한국어하윤 HayoonFemalefish_voice_96ed4e3f47334430b990df471d0a0268
한국어YunaFemalefish_voice_3d31499f0e13438bbce8dcce7b7c4298
한국어지훈 JihoonMalefish_voice_1faa840246094164bf4b0b0b94ae1675
한국어MinjunMalefish_voice_82703151e4b043dbbd98f082d4841b71
日本語きょうこ(カスタマーサポート)Femalefish_voice_b2d9d8db057042688a5e318b8f405bc2
日本語しおり(ナレーション)Femalefish_voice_5da7f24e9e274f91b2b677669c818ce9
日本語さとる(ナレーション)Malefish_voice_297a6fd278df47c3b9da9bfdf55ac89a
日本語じん(ラジオパーソナリティ)Malefish_voice_d738c70d206149e69f8e3b6fc8af9d96
简体中文语彤 YutongFemalefish_voice_74c6aba5cbf94a15bbdc547ffce5cb38
简体中文冰冰 BingbingFemalefish_voice_e98fa6cdad6946bf8d9bb8f9cb8c2532
简体中文梓轩 ZixuanMalefish_voice_5d29a99739c14d4ca3e4fe42193105b2
简体中文浩然 HaoranMalefish_voice_d675c275d1d44e57b4ef3840c5a23209
繁體中文詩涵 Shihan(台灣)Femalefish_voice_91ec588cf8ef443a9c0d5b21d0c1fa36
繁體中文Ya-TingFemalefish_voice_49066cb45f0a427484ade915a4474534
繁體中文承翰 Chenghan(台灣)Malefish_voice_1e85fd1e0d3e4cc2b79fbca800e7e3fe
繁體中文Chia-HaoMalefish_voice_e7af89da8abc4fbdacd9a10ab9881933
粤语詠詩 Wing SzeFemalefish_voice_50c586f7c811466cac751c28c3e4a671
粤语VideoLingo 粵語女聲Femalefish_voice_da2d934b77df4561ac51f8ed3edfaa2b
粤语俊傑 Chun KitMalefish_voice_fc917a4b147145258519982fe306b2cf
粤语VideoLingo 粵語男聲Malefish_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

  1. The API authenticates the key and validates the entire JSON body. Unknown or mismatched values fail before processing.
  2. 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 returns 422 invalid_request.
  3. 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"
}
FieldTypeWhen present and how to use it
idstring (UUID)Stable task identifier. Persist this for later status requests.
namestringTask name shown in the dashboard.
statusstring enumLifecycle state. Branch on this field; see the table below.
youtube_urlstring | nullSource URL. It is null for a local-file task created on the website.
video_duration_secondsnumber | nullSource duration in seconds once known.
cost_usdnumber | nullReserved 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.
stepstring | nullHuman-readable current pipeline stage while processing. Display it; do not build business logic around its wording.
step_indexnumber | nullStable numeric progress stage for UI progress. See the stage table below.
errorstring | nullHuman-readable processing failure reason when status is failed. It is not the same as an HTTP API error object.
resultobject | nullDownload URLs when status is completed; otherwise null.
created_atstringISO 8601 UTC creation timestamp.
updated_atstringISO 8601 UTC timestamp of the latest stored task update.

status values

ValueTerminalMeaning and client action
draftNoNot dispatched. A website draft or a task whose create call returned 503 dispatch_failed; retry that create call with the same idempotency key.
queuedNoAccepted and waiting for processing capacity. Continue polling.
processingNoRunning. Use step and step_index for display and continue polling.
completedYesFinished successfully. Read download URLs from result.
failedYesProcessing failed. Read error; submitting again is a new task and should use a new idempotency key unless the original create request itself returned 503.
cancelledYesStopped. No result will be produced.

step and step_index

step_indexSubtitle-only taskDubbed task
0Preparing input videoPreparing input video
1Extracting audio using DemucsExtracting audio using Demucs
2Transcribing using WhisperTranscribing using Whisper
3Splitting sentences using spaCySplitting sentences using spaCy
4Splitting sentences by meaningSplitting sentences by meaning
5Summarizing contentSummarizing content
6Translating textTranslating text
7Splitting for subtitlesSplitting for subtitles
8Aligning timestampsAligning timestamps
9Merging subtitlesReserved / skipped
10Uploading resultsGenerating 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:

FieldFile
videoFinal MP4 video.
subtitles_zipZIP archive containing all subtitle files.
translated_srtTranslated SRT subtitles.
source_srtSource-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
  }
}
HTTPerror.codeMeaningClient action
401invalid_api_keyMissing, malformed, regenerated, or unknown key.Do not retry unchanged. Fix the header or deploy the current key.
403subscription_inactiveThe 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.
404not_foundTask is invalid, missing, deleted, or belongs to another account.Check the id and account. Do not repeatedly retry.
422invalid_requestInvalid 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.
422duration_unavailableDuration 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.
402insufficient_balanceAvailable 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.
429concurrency_limitThree 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.
503dispatch_failedProcessing temporarily refused the task. Extra field: task_id.Retry with exponential backoff and the same key/body. The task remains draft.
500internal_errorUnexpected 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 queued or processing for one account. Tasks started on the website count toward the same limit.
  • Duration: one video can be at most 4 hours (14400 seconds).
  • Billing: subtitle translation and dubbing use the same video-duration rate of USD 5 per hour. The resulting cost_usd is 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 Accepted means 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
done

Rotate 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.