Dub video and audio from your own systems. Upload a file, start a dub, and get the dubbed audio, video, subtitles and transcript back, with a signed webhook when it's ready.
Create an API keyFiles can be up to 4 GB and 2 hours long. Voice samples up to 50 MB.
Send your key in the Authorization header on every request. Keys start with yf_live_. A missing, wrong or revoked key gets HTTP 401 with the code invalid_api_key.
Authorization: Bearer yf_live_...export YAPFLIP_KEY="yf_live_..."
API="https://yapflip.com/api/v1"
# 1. Create the dub and open the upload
curl -s "$API/dubs" -H "Authorization: Bearer $YAPFLIP_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "intro.mp4", "size_bytes": 52428800}'
# Answer: { "dub": { "id": "DUB_ID", "status": "uploading", ... },
# "upload": { "parts": 1, "part_size": 94371840, ... } }
# 2. Upload the parts (split with the part_size you got)
split -b 94371840 -d -a 3 intro.mp4 part_
n=1
for p in part_*; do
curl -s -X PUT "$API/dubs/DUB_ID/source/parts/$n" \
-H "Authorization: Bearer $YAPFLIP_KEY" --data-binary @"$p"
n=$((n + 1))
done
# 3. Complete the upload
curl -s -X POST "$API/dubs/DUB_ID/source/complete" -H "Authorization: Bearer $YAPFLIP_KEY"
# 4. Start the dub (voice ids come from GET /voices)
curl -s "$API/dubs/DUB_ID/start" -H "Authorization: Bearer $YAPFLIP_KEY" \
-H "Content-Type: application/json" \
-d '{"source_language": "en", "target_language": "es", "target_dialect": "es-MX",
"voice": {"type": "library", "id": "VOICE_ID"}, "outputs": ["audio", "srt"]}'
# 5. Check the status, then download
curl -s "$API/dubs/DUB_ID" -H "Authorization: Bearer $YAPFLIP_KEY"
curl -s -o intro-es.m4a "$API/dubs/DUB_ID/files/audio" -H "Authorization: Bearer $YAPFLIP_KEY"import os, time, requests
API = "https://yapflip.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['YAPFLIP_KEY']}"}
def check(r):
if not r.ok:
e = r.json()
raise RuntimeError(f"{r.status_code} {e.get('code')}: {e.get('error')}")
return r.json()
path = "intro.mp4"
r = check(requests.post(f"https://yapflip.com/api/v1/dubs", headers=H,
json={"filename": "intro.mp4", "size_bytes": os.path.getsize(path)}))
dub, up = r["dub"]["id"], r["upload"]
with open(path, "rb") as fh:
for n in range(1, up["parts"] + 1):
check(requests.put(f"https://yapflip.com/api/v1/dubs/{dub}/source/parts/{n}", headers=H, data=fh.read(up["part_size"])))
check(requests.post(f"https://yapflip.com/api/v1/dubs/{dub}/source/complete", headers=H))
check(requests.post(f"https://yapflip.com/api/v1/dubs/{dub}/start", headers=H, json={
"source_language": "en", "target_language": "es",
"voice": {"type": "library", "id": "VOICE_ID"}, "outputs": ["audio", "srt"],
}))
while True: # or wait for the dub.done webhook
d = check(requests.get(f"https://yapflip.com/api/v1/dubs/{dub}", headers=H))
if d["status"] in ("done", "failed", "canceled"):
break
time.sleep(15)
if d["status"] == "done":
with requests.get(f"https://yapflip.com/api/v1/dubs/{dub}/files/audio", headers=H, stream=True) as f, open("intro-es.m4a", "wb") as out:
for chunk in f.iter_content(1 << 20):
out.write(chunk)// Node.js 18 or newer (built-in fetch)
import { open, stat } from "node:fs/promises";
const API = "https://yapflip.com/api/v1";
const H = { Authorization: `Bearer ${process.env.YAPFLIP_KEY}` };
async function call(method, path, body) {
const r = await fetch(API + path, {
method,
headers: body ? { ...H, "Content-Type": "application/json" } : H,
body: body ? JSON.stringify(body) : undefined,
});
const d = await r.json();
if (!r.ok) throw new Error(`${r.status} ${d.code}: ${d.error}`);
return d;
}
const path = "intro.mp4";
const { dub, upload } = await call("POST", "/dubs", { filename: "intro.mp4", size_bytes: (await stat(path)).size });
const file = await open(path);
for (let n = 1; n <= upload.parts; n++) {
const buf = Buffer.alloc(upload.part_size);
const { bytesRead } = await file.read(buf, 0, upload.part_size, (n - 1) * upload.part_size);
const r = await fetch(`$https://yapflip.com/api/v1/dubs/${dub.id}/source/parts/${n}`, { method: "PUT", headers: H, body: buf.subarray(0, bytesRead) });
if (!r.ok) throw new Error(`part ${n}: ${r.status}`);
}
await file.close();
await call("POST", `/dubs/${dub.id}/source/complete`);
await call("POST", `/dubs/${dub.id}/start`, {
source_language: "en", target_language: "es",
voice: { type: "library", id: "VOICE_ID" }, outputs: ["audio", "srt"],
});
let d;
do {
await new Promise((ok) => setTimeout(ok, 15_000)); // or wait for the dub.done webhook
d = await call("GET", `/dubs/${dub.id}`);
} while (!["done", "failed", "canceled"].includes(d.status));source_language, target_languagevoicetarget_dialectoutputsglossaryreviewsubtitlesexpected_charge_microsstart_nowstatus is one of uploading, ready, queued, running, script_ready, done, failed or canceled. A company dub that is waiting for a free slot is queued with waiting_for_slot: true. When it's done, files lists what you can download; files are deleted after 30 days (files_expire_at). If a dub fails or we cancel it on our side, the charge goes back automatically (refunded: true). A script that you discard or don't confirm in time is not refunded: refunded stays false and script_end says discarded or expired.
Amounts are in micro-dollars: 1,000,000 = USD 1. charge_micros is what this dub cost.
Download with GET /dubs/{id}/files/{kind}, where kind is audio, video, voice, srt_source, srt_target or docx. Range requests work, so you can resume large downloads.
To dub with a person's own voice, you need their written consent, the same as in the dashboard. Before uploading the sample, send the consent version and the exact text the voice owner accepted. We store both as proof, with the date, IP address and API key. Without it, we don't accept the sample.
curl -s "$API/dubs/DUB_ID/voice" -H "Authorization: Bearer $YAPFLIP_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "my-voice.m4a", "size_bytes": 480000,
"consent": {"subject": "self", "version": "own-voice-2026-10-30",
"text": "<the exact text below>"}}'
# Then PUT /dubs/DUB_ID/voice/parts/1 and POST /dubs/DUB_ID/voice/complete,
# and start the dub with "voice": {"type": "own"}.Show the consent in a language the voice owner understands. Each of our 10 languages has its own version of the same text: own-voice-2026-10-30, own-voice-2026-10-30-es and own-voice-2026-10-30-ja, and so on. Send the version of the language you showed, with its text. We compare the text exactly, except for spaces and line breaks.
version own-voice-2026-10-30subject "self" (the voice belongs to the person using your system):
This is my own voice. I am 18 or older. Yapflip may use this sample only to make this dub and its word fixes. The file is deleted within 24 hours. A clip of up to 10 seconds from it stays with this dub's files for 30 days, so fixed sentences keep the same voice, and is then deleted.subject "authorized" (someone else's voice, with their written permission):
This is someone else's voice and I have their written permission to use it for this dub. I am 18 or older. Yapflip may use this sample only to make this dub and its word fixes. The file is deleted within 24 hours. A clip of up to 10 seconds from it stays with this dub's files for 30 days, so fixed sentences keep the same voice, and is then deleted.version own-voice-2026-10-30-essubject "self" (the voice belongs to the person using your system):
Es mi propia voz. Tengo 18 años o más. Yapflip puede usar esta muestra solo para hacer este doblaje y sus correcciones de palabras. El archivo se borra dentro de las 24 horas. Un fragmento de hasta 10 segundos queda con los archivos de este doblaje durante 30 días, para que las frases corregidas mantengan la misma voz, y después se borra.subject "authorized" (someone else's voice, with their written permission):
Es la voz de otra persona y tengo su permiso por escrito para usarla en este doblaje. Tengo 18 años o más. Yapflip puede usar esta muestra solo para hacer este doblaje y sus correcciones de palabras. El archivo se borra dentro de las 24 horas. Un fragmento de hasta 10 segundos queda con los archivos de este doblaje durante 30 días, para que las frases corregidas mantengan la misma voz, y después se borra.version own-voice-2026-10-30-ptsubject "self" (the voice belongs to the person using your system):
É a minha própria voz. Tenho 18 anos ou mais. O Yapflip pode usar esta amostra só para fazer esta dublagem e suas correções de palavras. O arquivo é apagado em até 24 horas. Um trecho de até 10 segundos fica com os arquivos desta dublagem por 30 dias, para que as frases corrigidas mantenham a mesma voz, e depois é apagado.subject "authorized" (someone else's voice, with their written permission):
É a voz de outra pessoa e tenho autorização por escrito dela para usá-la nesta dublagem. Tenho 18 anos ou mais. O Yapflip pode usar esta amostra só para fazer esta dublagem e suas correções de palavras. O arquivo é apagado em até 24 horas. Um trecho de até 10 segundos fica com os arquivos desta dublagem por 30 dias, para que as frases corrigidas mantenham a mesma voz, e depois é apagado.version own-voice-2026-10-30-itsubject "self" (the voice belongs to the person using your system):
È la mia voce. Ho almeno 18 anni. Yapflip può usare questo campione solo per realizzare questo doppiaggio e le sue correzioni di parole. Il file viene eliminato entro 24 ore. Un estratto di massimo 10 secondi resta con i file di questo doppiaggio per 30 giorni, perché le frasi corrette abbiano la stessa voce, e poi viene eliminato.subject "authorized" (someone else's voice, with their written permission):
È la voce di un'altra persona e ho il suo permesso scritto per usarla in questo doppiaggio. Ho almeno 18 anni. Yapflip può usare questo campione solo per realizzare questo doppiaggio e le sue correzioni di parole. Il file viene eliminato entro 24 ore. Un estratto di massimo 10 secondi resta con i file di questo doppiaggio per 30 giorni, perché le frasi corrette abbiano la stessa voce, e poi viene eliminato.version own-voice-2026-10-30-frsubject "self" (the voice belongs to the person using your system):
C’est ma propre voix. J’ai 18 ans ou plus. Yapflip peut utiliser cet échantillon uniquement pour réaliser ce doublage et ses corrections de mots. Le fichier est supprimé dans les 24 heures. Un extrait de 10 secondes au plus reste avec les fichiers de ce doublage pendant 30 jours, pour que les phrases corrigées gardent la même voix, puis il est supprimé.subject "authorized" (someone else's voice, with their written permission):
C’est la voix d’une autre personne et j’ai son autorisation écrite pour l’utiliser dans ce doublage. J’ai 18 ans ou plus. Yapflip peut utiliser cet échantillon uniquement pour réaliser ce doublage et ses corrections de mots. Le fichier est supprimé dans les 24 heures. Un extrait de 10 secondes au plus reste avec les fichiers de ce doublage pendant 30 jours, pour que les phrases corrigées gardent la même voix, puis il est supprimé.version own-voice-2026-10-30-desubject "self" (the voice belongs to the person using your system):
Das ist meine eigene Stimme. Ich bin mindestens 18 Jahre alt. Yapflip darf diese Probe nur für diese Synchronisation und ihre Wortkorrekturen nutzen. Die Datei wird innerhalb von 24 Stunden gelöscht. Ein Ausschnitt von höchstens 10 Sekunden bleibt 30 Tage bei den Dateien dieser Synchronisation, damit korrigierte Sätze dieselbe Stimme behalten, und wird dann gelöscht.subject "authorized" (someone else's voice, with their written permission):
Das ist die Stimme einer anderen Person, und ich habe ihre schriftliche Erlaubnis, sie für diese Synchronisation zu nutzen. Ich bin mindestens 18 Jahre alt. Yapflip darf diese Probe nur für diese Synchronisation und ihre Wortkorrekturen nutzen. Die Datei wird innerhalb von 24 Stunden gelöscht. Ein Ausschnitt von höchstens 10 Sekunden bleibt 30 Tage bei den Dateien dieser Synchronisation, damit korrigierte Sätze dieselbe Stimme behalten, und wird dann gelöscht.version own-voice-2026-10-30-zhsubject "self" (the voice belongs to the person using your system):
这是我自己的声音。 我已年满 18 岁。 Yapflip 只能将这段样本用于完成这次配音及其用词修正。文件会在 24 小时内删除。其中最长 10 秒的片段会随这次配音的文件保留 30 天,让修正后的句子保持同一个声音,之后删除。subject "authorized" (someone else's voice, with their written permission):
这是他人的声音,我已获得对方的书面许可,可在这次配音中使用。 我已年满 18 岁。 Yapflip 只能将这段样本用于完成这次配音及其用词修正。文件会在 24 小时内删除。其中最长 10 秒的片段会随这次配音的文件保留 30 天,让修正后的句子保持同一个声音,之后删除。version own-voice-2026-10-30-jasubject "self" (the voice belongs to the person using your system):
これは私自身の声です。 私は18歳以上です。 Yapflip はこのサンプルを、この吹き替えとその単語修正のためだけに使用できます。ファイルは24時間以内に削除されます。修正した文も同じ声になるよう、最長10秒のクリップがこの吹き替えのファイルとともに30日間保管され、その後削除されます。subject "authorized" (someone else's voice, with their written permission):
これは他の人の声で、この吹き替えに使うことについて本人の書面による許可を得ています。 私は18歳以上です。 Yapflip はこのサンプルを、この吹き替えとその単語修正のためだけに使用できます。ファイルは24時間以内に削除されます。修正した文も同じ声になるよう、最長10秒のクリップがこの吹き替えのファイルとともに30日間保管され、その後削除されます。version own-voice-2026-10-30-kosubject "self" (the voice belongs to the person using your system):
제 목소리예요. 만 18세 이상이에요. Yapflip은 이 샘플을 이 더빙과 단어 수정에만 쓸 수 있어요. 파일은 24시간 안에 삭제돼요. 수정한 문장도 같은 목소리가 나도록 최대 10초 길이의 클립이 이 더빙의 파일과 함께 30일 동안 보관된 뒤 삭제돼요.subject "authorized" (someone else's voice, with their written permission):
다른 사람의 목소리이고, 이 더빙에 쓰는 것에 대해 그 사람의 서면 허락을 받았어요. 만 18세 이상이에요. Yapflip은 이 샘플을 이 더빙과 단어 수정에만 쓸 수 있어요. 파일은 24시간 안에 삭제돼요. 수정한 문장도 같은 목소리가 나도록 최대 10초 길이의 클립이 이 더빙의 파일과 함께 30일 동안 보관된 뒤 삭제돼요.version own-voice-2026-10-30-rusubject "self" (the voice belongs to the person using your system):
Это мой собственный голос. Мне 18 лет или больше. Yapflip может использовать этот образец только для этого дубляжа и исправлений слов в нём. Файл удаляется в течение 24 часов. Фрагмент длиной до 10 секунд хранится вместе с файлами дубляжа 30 дней, чтобы исправленные фразы звучали тем же голосом, а затем удаляется.subject "authorized" (someone else's voice, with their written permission):
Это голос другого человека, и у меня есть его письменное разрешение использовать голос в этом дубляже. Мне 18 лет или больше. Yapflip может использовать этот образец только для этого дубляжа и исправлений слов в нём. Файл удаляется в течение 24 часов. Фрагмент длиной до 10 секунд хранится вместе с файлами дубляжа 30 дней, чтобы исправленные фразы звучали тем же голосом, а затем удаляется.Show this text to the voice owner and record their acceptance in your system before calling the API. When we update the text, the versions change and older versions get 409 consent_outdated.
If a done dub says a name or a word wrong, send changes as { from, to }. The first round, up to 10 changes, is free. Later changes cost 8¢ each, billed like the dub. We redo only the sentences that change.
curl -s "$API/dubs/DUB_ID/fixes" -H "Authorization: Bearer $YAPFLIP_KEY" \
-H "Content-Type: application/json" \
-d '{"changes": [{"from": "Yap flip", "to": "Yapflip"}]}'Start a dub with review: true and it stops at script_ready. Read the script with GET /dubs/{id}/script, fix sentences with PATCH (send the revision you read), then POST /script/confirm to record the voice, or /script/discard to cancel the dub (discarding does not refund the charge). If you don't confirm within 24 hours of the upload, the dub ends as failed and the charge is not refunded either. Fixing the script is free, and each dub includes one free round of word fixes after it's done.
DELETE /dubs/{id} discards a dub that hasn't started, cancels one waiting for a slot (the charge goes back), discards a script waiting for review (no refund), or deletes the files of a finished dub right away. A dub that is running can be deleted when it's done.
Set a URL on the API page of your dashboard. We send a POST request with a JSON body, usually within two minutes, when a dub is done (dub.done), fails (dub.failed) or has a script ready to review (dub.script_ready). The event is short: read the dub with GET /dubs/{id} for the details.
POST /your/webhook HTTP/1.1
Content-Type: application/json
Yapflip-Event: dub.done
Yapflip-Delivery: 6f1c0d7e-1b7e-4a53-9d55-0f6c2b8f1a2e
Yapflip-Signature: t=1793385600,v1=5d41402abc4b2a76b9719d911017c592...
{"id": "6f1c0d7e-1b7e-4a53-9d55-0f6c2b8f1a2e", "type": "dub.done",
"created_at": "2026-10-30T18:00:00.000Z",
"data": {"dub": {"id": "DUB_ID", "status": "done", "title": "intro"}}}Answer with any 2xx status within 10 seconds. Otherwise we retry after 1, 5, 30, 120, 360, 720 and 1,440 minutes. Use the Yapflip-Delivery header to ignore repeats. Test events from the dashboard aren't retried.
Every request has a Yapflip-Signature header: t=<unix time>,v1=<signature>. The signature is the HMAC-SHA256 of "<t>.<raw body>" with your signing secret, in hex. Compute it over the raw body, compare in constant time, and reject requests older than 5 minutes.
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, sig = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const p = Object.fromEntries(header.split(",").map((x) => x.split("=")));
if (!/^\d+$/.test(p.t ?? "") || Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${p.t}.${rawBody}`).digest();
const got = Buffer.from(p.v1 ?? "", "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
}Each key can make 60 requests a minute, and each account 300, counting every request including file parts. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Over the limit you get HTTP 429 with a Retry-After header.
Errors use HTTP status codes and a JSON body with error (a message you can show to a person) and, when it helps, code (stable, for your program) and fields.
{ "error": "You don't have enough balance for this dub. Add balance and try again.", "code": "insufficient_balance" }| code | HTTP | |
|---|---|---|
invalid_api_key | 401 | The key is missing, wrong or revoked. |
rate_limited | 429 | Too many requests. Wait for Retry-After seconds. |
not_found | 404 | The dub doesn't exist or belongs to another account. |
insufficient_balance | 402 | Your prepaid balance doesn't cover this dub. Add balance in the dashboard. |
billing_hold | 402 | Your balance is below zero after a reversed payment. |
business_cap | 402 | Your company reached its monthly usage limit. |
business_past_due | 402 | Your company plan has a payment problem, so new dubs are paused. |
business_closing | 409 | The company's month is closing or the plan ends today. Try again in a few minutes. |
price_changed | 409 | The price isn't the expected_charge_micros you sent. Read the dub again and retry. |
consent_required | 400 | The own-voice consent is missing or its text doesn't match the text of its version. |
consent_outdated | 409 | The consent version isn't a current one. Show the current text to the voice owner. |
review_too_late | 400 | Less than 3 hours are left before the upload is deleted, so there's no time to check the script. Start without review or upload again. |
start_now_required | 400 | Accounts in the EU, EEA or UK have to send start_now: true for each paid dub or paid fix. |
upload_gone | 410 | Uploads are kept for 24 hours and this one is gone. Upload the file again. |
country_blocked | 403 | We can't provide the service in your account's country. |
https://yapflip.com/api/v1
/accountWho is billed (balance or company plan), prices and limits./voicesLanguages, library voices and regional styles./dubsYour dubs, newest first. limit (up to 100) and before (next_before from the last page, an opaque cursor)./dubsCreate a dub and open the upload: { filename, size_bytes }./dubs/{id}/source/parts/{n}Upload part n of the file (raw bytes)./dubs/{id}/source/completeFinish the upload and check the file./dubs/{id}/voiceOwn voice: consent and open the sample upload./dubs/{id}/voice/parts/{n}Upload part n of the voice sample./dubs/{id}/voice/completeFinish the voice sample (10 seconds to 5 minutes of speech)./dubs/{id}/startCharge and start the dub./dubs/{id}Status, progress, charge, files and fixes./dubs/{id}/files/{kind}Download a file./dubs/{id}/fixesFix words in a done dub./dubs/{id}/scriptRead the script./dubs/{id}/scriptFix sentences in the script./dubs/{id}/script/confirmApprove the script and record the voice./dubs/{id}/script/discardDiscard the script and cancel the dub (discarding does not refund the charge)./dubs/{id}Discard, cancel or delete files.Questions about the API: [email protected].