API y servidor MCP
Los mismos casos de uso que la aplicación web —voces, subidas, conversiones, narraciones y descargas— desde tu código o desde tu asistente. Los mismos minutos y la misma factura.
Autenticación#
Cada petición viaja con una clave de API, en su propia cabecera o como credencial de tipo «bearer». Las dos formas valen y hacen lo mismo; elige la que te resulte más cómoda con tu cliente.
x-api-key— An API key created in Settings → API (miy_…).bearer— The same API key, asAuthorization: Bearer miy_…. An OAuth 2.1 access token is NOT accepted on/v1: it authenticates the MCP server only.
Las claves se crean desde Ajustes → API. La clave completa se enseña una sola vez, al crearla: guárdala donde guardes el resto de tus secretos y no la publiques en el código de un cliente. Revocarla es inmediato y no borra nada de lo que ya hiciste con ella.
El servidor MCP no usa claves: autoriza con un token que tu asistente obtiene por ti. Lo cuenta la sección del servidor MCP.
Qué lleva una clave
Al crear una clave eliges su nombre, los permisos que lleva, una caducidad opcional y, si quieres, un tope diario de excedente propio por debajo del general. Una clave no se amplía después: la que necesita más es una clave nueva.
| Permisos que puedes concederle | voices:readjobs:readjobs:writenarrations:write |
|---|---|
| Longitud máxima del nombre, en caracteres | 64 |
| Caducidad, opcional, en días | 1–365 |
| Tope diario de excedente de la clave, opcional, en minutos | 1–1440 |
El acceso es un derecho de tu plan#
La API y el servidor MCP están incluidos en los planes profesionales. Si tu plan no los incluye, la credencial existe pero no autoriza: la respuesta lo dice con un código propio y con un estado de «prohibido».
Perder el plan no borra ni revoca nada. Tus claves y tus autorizaciones siguen donde estaban y vuelven a servir en cuanto el plan vuelve.
La API y el servidor MCP forman parte del servicio: lo que hagas a través de ellos se rige por los mismos términos y la misma política de privacidad que tu cuenta.
Límites#
Las peticiones se limitan por minuto, tanto por dirección de origen como por credencial. Al pasarte de un límite, la respuesta llega con un estado de «demasiadas peticiones» y una cabecera que dice cuántos segundos esperar antes de reintentar. Respétala en vez de reintentar en bucle.
Además hay un tope diario de excedente por credencial, en segundos y por día natural. Cuando lo alcanzas, la operación que consumiría se rechaza sin escribir nada: no se crea el trabajo, no se cobra el excedente y el día siguiente empieza de cero.
Toda respuesta de una operación que consume trae el desglose estimado de lo que sale de tu plan y lo que sería excedente. Es una estimación previa, no la factura: la cifra definitiva se cierra cuando el trabajo termina.
| Peticiones por minuto desde una misma dirección de origen | 300 |
|---|---|
| Peticiones por minuto con una misma credencial | 120 |
| Tope diario de excedente por credencial, salvo que la clave fije uno menor | 30 minutos |
| Cuándo vuelve a empezar el tope diario | a medianoche, hora UTC |
| Cabecera que dice cuántos segundos esperar ante un «demasiadas peticiones» | Retry-After |
Idempotencia#
Las operaciones que crean trabajo exigen una clave de idempotencia que eliges tú y que debe ser distinta para cada intento nuevo. En las subidas es opcional, pero conviene.
Si una petición se corta por un fallo de red y no sabes si llegó, repítela con la MISMA clave: o te devuelve el trabajo que ya se creó o lo crea entonces, pero nunca dos veces.
Paginación#
Las listas se recorren con un cursor opaco: pide la primera página, guarda el cursor que viene en la respuesta y pásalo tal cual en la siguiente petición. No lo interpretes ni lo construyas: su contenido es un detalle interno y puede cambiar.
Cuando el cursor de la respuesta llega vacío, has terminado: no hay más páginas.
Errores#
Todo error, en la API y en el servidor MCP, llega con la misma forma: un código, un mensaje y un identificador de la petición.
El código es estable y es lo que tu programa debe leer. El catálogo completo está más abajo, con el estado que lleva cada uno.
El mensaje está pensado para una persona y sigue el idioma que pidas en la cabecera de idioma; en inglés si no pides ninguno. No lo uses para decidir: cambia de idioma y puede reescribirse.
Guarda el identificador de la petición. Es lo primero que te vamos a pedir si escribes a soporte, y es lo que nos deja encontrar tu llamada exacta entre todas.
Si una respuesta no te cuadra, escríbenos con el identificador de la petición a support@makeityours.app, o abre el canal de ayuda dentro de la aplicación: Ayuda
El mensaje puede llegar en cualquiera de los idiomas que habla el producto; pide uno con la cabecera de idioma usando uno de estos códigos:
esenpt-BRdefritpl
Versionado#
La versión va en la ruta y nunca cambia de significado por debajo de ti: no retiramos un campo, no estrechamos un valor y no cambiamos el sentido de una respuesta sin cambiar de versión.
Lo que sí añadimos —una operación nueva, un campo opcional nuevo, un código de error nuevo— se anuncia en la lista de cambios. Cualquier cosa incompatible saldría bajo una versión distinta.
Inicio rápido#
De un fichero en tu disco a un resultado descargado, en siete pasos. Todos los ejemplos salen del contrato: el método, la ruta, el cuerpo y la respuesta son los que sirve la API de verdad. Sustituye la clave y la clave de idempotencia por las tuyas.
Paso 1createUpload
Registra la subida y recibe la dirección temporal a la que subir el fichero, con sus límites y su caducidad.
bashcurl -X POST 'https://www.makeityours.app/api/v1/uploads' \ -H 'x-api-key: miy_…' \ -H 'Idempotency-Key: <unique-key>' \ -H 'Content-Type: application/json' \ --data '{ "filename": "spring-spot.mp4", "sizeBytes": 48213907, "contentType": "video/mp4" }'Peticiónjson{ "filename": "spring-spot.mp4", "sizeBytes": 48213907, "contentType": "video/mp4" }Respuesta · 201json{ "data": { "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6", "putUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/source.mp4?signature=example", "expiresAt": "2026-09-13T11:00:00.000Z", "constraints": { "maxSizeBytes": 524288000, "maxDurationSeconds": 180, "containers": [ "mp4", "mov" ] } } }Paso 2
Sube el fichero a esa dirección con una sola petición, declarando el mismo tipo de contenido con el que lo registraste: es el tipo con el que se guarda el fichero y con el que responde después la descarga. La petición no lleva ninguna credencial tuya: la autorización viaja dentro de la propia dirección.
bashcurl -X PUT 'https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/source.mp4?signature=example' \ -H 'Content-Type: video/mp4' \ --upload-file 'spring-spot.mp4'Paso 3runUploadPreflight
Pide la comprobación previa del fichero subido. Es la que mide la duración y decide si se puede convertir.
bashcurl -X POST 'https://www.makeityours.app/api/v1/uploads/u8p2l4o6a1d3i5n7g9s2t4r6/preflight' \ -H 'x-api-key: miy_…'Respuesta · 200json{ "data": { "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6", "ready": true, "preflight": { "container": "mov,mp4,m4a,3gp,3g2,mj2", "videoCodec": "h264", "hasAudio": true, "audioCodec": "aac", "containerDurationS": 41.72, "audioDurationS": 41.68, "vfrSuspected": false, "remuxSafe": true } } }Paso 4createJob
Confirma la conversión con las voces que quieras. Aquí es donde se reservan los minutos, así que aquí es donde hace falta la clave de idempotencia.
bashcurl -X POST 'https://www.makeityours.app/api/v1/jobs' \ -H 'x-api-key: miy_…' \ -H 'Idempotency-Key: <unique-key>' \ -H 'Content-Type: application/json' \ --data '{ "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6", "voiceIds": [ "v4n8k2m6p1q9s3t7w5x1y2z4", "l1a3m5v7o9i2c4e6b8d0f2h4" ], "name": "Spring spot — Ana and Liam", "voiceSettings": { "v4n8k2m6p1q9s3t7w5x1y2z4": { "similarity": 0.8, "stability": 0.45, "speakerBoost": true } } }'Peticiónjson{ "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6", "voiceIds": [ "v4n8k2m6p1q9s3t7w5x1y2z4", "l1a3m5v7o9i2c4e6b8d0f2h4" ], "name": "Spring spot — Ana and Liam", "voiceSettings": { "v4n8k2m6p1q9s3t7w5x1y2z4": { "similarity": 0.8, "stability": 0.45, "speakerBoost": true } } }Respuesta · 202json{ "data": { "job": { "id": "k7f2q9x1m3p8w5z2c4v6b8n0", "status": "queued", "steps": [ { "key": "prepare", "done": false }, { "key": "separate", "done": false }, { "key": "convert", "done": false }, { "key": "package", "done": false }, { "key": "finalize", "done": false } ], "variants": [ { "variantId": "r3v5a7r9i1a3n5t7x9y1z3w5", "voiceName": "Ana", "driftMs": null, "hasResult": false, "speakers": [] }, { "variantId": "r4v6a8r0i2a4n6t8x0y2z4w6", "voiceName": "Liam", "driftMs": null, "hasResult": false, "speakers": [] } ], "variantsTotal": 2, "convertVariantsDone": 0, "paused": false, "isRemaster": false, "isNarration": false }, "estimate": { "planSeconds": 66, "overageSeconds": 18, "overageAmountCents": 18, "currency": "usd" } } }Paso 5
Si tienes un endpoint dado de alta, recibes el aviso de que el trabajo ha terminado en cuanto termina, firmado. Es la alternativa a preguntar en bucle.
bashcurl -X POST 'https://example.com/webhooks' \ -H 'MIY-Signature: t=1789293852,v1=<hex HMAC-SHA256 of "t.body" with your endpoint secret>' \ -H 'Content-Type: application/json' \ --data '{ "id": "e5v7e9n1t3a5b7c9d1e3f5g7", "type": "job.completed", "createdAt": "2026-09-13T10:04:12.000Z", "data": { "jobId": "k7f2q9x1m3p8w5z2c4v6b8n0", "status": "done", "kind": "conversion", "links": { "job": "https://www.makeityours.app/api/v1/jobs/k7f2q9x1m3p8w5z2c4v6b8n0", "download": "https://www.makeityours.app/api/v1/jobs/k7f2q9x1m3p8w5z2c4v6b8n0/download" } } }'httpt=1789293852,v1=<hex HMAC-SHA256 of "t.body" with your endpoint secret>Peticiónjson{ "id": "e5v7e9n1t3a5b7c9d1e3f5g7", "type": "job.completed", "createdAt": "2026-09-13T10:04:12.000Z", "data": { "jobId": "k7f2q9x1m3p8w5z2c4v6b8n0", "status": "done", "kind": "conversion", "links": { "job": "https://www.makeityours.app/api/v1/jobs/k7f2q9x1m3p8w5z2c4v6b8n0", "download": "https://www.makeityours.app/api/v1/jobs/k7f2q9x1m3p8w5z2c4v6b8n0/download" } } }Paso 6getJob
Consulta el trabajo cuando quieras saber cómo va o qué variantes ha producido.
bashcurl -X GET 'https://www.makeityours.app/api/v1/jobs/j2b4d6f8h0k1m3p5r7t9v1x3' \ -H 'x-api-key: miy_…'Respuesta · 200json{ "data": { "id": "j2b4d6f8h0k1m3p5r7t9v1x3", "status": "done", "steps": [ { "key": "prepare", "done": true }, { "key": "separate", "done": true }, { "key": "convert", "done": true }, { "key": "package", "done": true }, { "key": "finalize", "done": true } ], "variants": [ { "variantId": "r3v5a7r9i1a3n5t7x9y1z3w5", "voiceName": "Ana", "driftMs": 120, "hasResult": true, "speakers": [] } ], "variantsTotal": 1, "convertVariantsDone": 1, "paused": false, "isRemaster": false, "isNarration": false } }Paso 7downloadJob
Pide la descarga con tu clave y lee la cabecera de redirección: apunta a una dirección temporal desde la que bajar el fichero. Baja el fichero con un segundo comando SIN la clave —la dirección ya lleva su propia autorización y caduca en poco tiempo—; no dejes que tu cliente siga la redirección reenviando la clave a otro servidor.
bashcurl -X GET 'https://www.makeityours.app/api/v1/jobs/j2b4d6f8h0k1m3p5r7t9v1x3/download' \ -H 'x-api-key: miy_…' \ -sS -o /dev/null -D -Y después, sin la clavebashcurl -sS -o result.mp4 '<the Location header of the previous answer>'
Todo el flujo en un solo programa
Los mismos siete pasos en un único fichero ejecutable, en Node.js y en Python, sin más dependencia que la biblioteca estándar. Cada uno lee tu clave de una variable de entorno, recibe el vídeo y la voz como argumentos, espera al trabajo y guarda el resultado al lado. Empieza por aquí y quédate con lo que necesites.
// MakeItYours API - one complete integration: register an upload, PUT the video,
// run the pre-flight, confirm the conversion, wait for it and download the result.
// Node.js 20 or newer, standard library only (global fetch).
//
// usage: MIY_API_KEY=miy_… node convert.js <video.mp4|.mov> <voiceId>
//
// Pick a voiceId from GET /v1/voices (the shared bank, or ?kind=own for your clones).
const { randomUUID } = require('crypto');
const { readFile, writeFile } = require('fs/promises');
const { basename, extname } = require('path');
const API = 'https://www.makeityours.app/api';
const API_KEY = process.env.MIY_API_KEY;
const POLL_SECONDS = 5;
// A job never leaves one of these: the loop below stops on them.
const TERMINAL_STATUSES = new Set(['done', 'failed', 'cancelled']);
// The contentType declared in step 1. The PUT of step 2 MUST send the same value.
const CONTENT_TYPES = { '.mp4': 'video/mp4', '.mov': 'video/quicktime' };
class ApiError extends Error {
constructor(status, envelope) {
const error = envelope && envelope.error ? envelope.error : {};
super(`${status} ${error.code || 'unknown'}: ${error.message || ''} (requestId ${error.requestId || '?'})`);
this.status = status;
this.code = error.code;
this.requestId = error.requestId;
}
}
// One call to the API: the key, the optional idempotency key, an optional JSON body.
// Any 4xx/5xx answer becomes an ApiError carrying the code and the requestId of the envelope.
async function call(method, path, { body, idempotencyKey, redirect = 'follow' } = {}) {
const headers = { 'x-api-key': API_KEY };
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
if (body !== undefined) headers['Content-Type'] = 'application/json';
const response = await fetch(API + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
redirect,
});
if (response.status >= 400) throw new ApiError(response.status, await response.json().catch(() => null));
return response;
}
const json = (response) => response.json();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main(file, voiceId) {
if (!API_KEY || !file || !voiceId) {
console.error('usage: MIY_API_KEY=miy_… node convert.js <video.mp4|.mov> <voiceId>');
process.exit(2);
}
const bytes = await readFile(file);
const contentType = CONTENT_TYPES[extname(file).toLowerCase()];
if (!contentType) throw new Error('only .mp4 and .mov files are accepted');
// ONE idempotency key for the upload AND the job that confirms it (see "Idempotency").
const idempotencyKey = randomUUID();
// 1. Register the upload: the answer carries the presigned PUT URL and the limits.
const upload = await json(await call('POST', '/v1/uploads', {
body: { filename: basename(file), sizeBytes: bytes.byteLength, contentType },
idempotencyKey,
}));
const { uploadId, putUrl } = upload.data;
// 2. PUT the bytes. No API key here (the URL carries its own authorisation) and
// the SAME Content-Type as step 1: it is the media type the stored file keeps.
const put = await fetch(putUrl, { method: 'PUT', headers: { 'Content-Type': contentType }, body: bytes });
if (!put.ok) throw new Error(`upload failed with status ${put.status}`);
// 3. Pre-flight: measures the duration and decides whether the file can be converted.
await call('POST', '/v1/uploads/' + uploadId + '/preflight');
// 4. Confirm the conversion. Minutes are reserved HERE; the estimate says how many.
const created = await json(await call('POST', '/v1/jobs', {
body: { uploadId, voiceIds: [voiceId], name: basename(file) },
idempotencyKey,
}));
let job = created.data.job;
const { planSeconds, overageSeconds } = created.data.estimate;
console.log(`job ${job.id}: ${job.status} (estimate: ${planSeconds} s from the plan, ${overageSeconds} s of overage)`);
// 5. Wait. In production a webhook (see "Webhooks") replaces this loop.
while (!TERMINAL_STATUSES.has(job.status)) {
await sleep(POLL_SECONDS * 1000);
job = (await json(await call('GET', '/v1/jobs/' + job.id))).data;
console.log(`job ${job.id}: ${job.status}`);
}
if (job.status !== 'done') throw new Error(`the job ended ${job.status}`);
// 6. Download: the API answers a redirect to a short-lived URL. Read the Location
// WITHOUT following it, then fetch that URL WITHOUT the key.
const redirect = await call('GET', '/v1/jobs/' + job.id + '/download', { redirect: 'manual' });
const location = redirect.headers.get('location');
if (!location) throw new Error(`expected a redirect, got status ${redirect.status}`);
const result = await fetch(location);
if (!result.ok) throw new Error(`download failed with status ${result.status}`);
await writeFile('result.mp4', Buffer.from(await result.arrayBuffer()));
console.log('saved result.mp4');
}
main(process.argv[2], process.argv[3]).catch((error) => {
console.error(error instanceof ApiError ? `API error: ${error.message}` : error);
process.exit(1);
});"""MakeItYours API - one complete integration: register an upload, PUT the video,
run the pre-flight, confirm the conversion, wait for it and download the result.
Python 3.9 or newer, standard library only.
usage: MIY_API_KEY=miy_… python3 convert.py <video.mp4|.mov> <voiceId>
Pick a voiceId from GET /v1/voices (the shared bank, or ?kind=own for your clones).
"""
import json
import os
import sys
import time
import uuid
from urllib import error, request
API = "https://www.makeityours.app/api"
API_KEY = os.environ.get("MIY_API_KEY", "")
POLL_SECONDS = 5
# A job never leaves one of these: the loop below stops on them.
TERMINAL_STATUSES = {"done", "failed", "cancelled"}
# The contentType declared in step 1. The PUT of step 2 MUST send the same value.
CONTENT_TYPES = {".mp4": "video/mp4", ".mov": "video/quicktime"}
class ApiError(Exception):
"""A 4xx/5xx answer: the code and the requestId of the envelope."""
def __init__(self, status, envelope):
err = (envelope or {}).get("error") or {}
super().__init__(
f"{status} {err.get('code', 'unknown')}: {err.get('message', '')} (requestId {err.get('requestId', '?')})"
)
self.status = status
self.code = err.get("code")
self.request_id = err.get("requestId")
class _NoRedirect(request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def call(method, path, body=None, idempotency_key=None, follow_redirects=True):
"""One call to the API: the key, the optional idempotency key, an optional JSON body."""
headers = {"x-api-key": API_KEY}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode("utf-8")
req = request.Request(API + path, data=data, method=method, headers=headers)
opener = request.build_opener() if follow_redirects else request.build_opener(_NoRedirect)
try:
with opener.open(req) as response:
return response.status, response.headers, response.read()
except error.HTTPError as exc:
payload = exc.read()
if 300 <= exc.code < 400:
return exc.code, exc.headers, payload
try:
envelope = json.loads(payload)
except ValueError:
envelope = None
raise ApiError(exc.code, envelope) from None
def call_json(method, path, **kwargs):
_, _, payload = call(method, path, **kwargs)
return json.loads(payload)
def main(file, voice_id):
if not API_KEY or not file or not voice_id:
print("usage: MIY_API_KEY=miy_… python3 convert.py <video.mp4|.mov> <voiceId>", file=sys.stderr)
sys.exit(2)
with open(file, "rb") as handle:
data = handle.read()
content_type = CONTENT_TYPES.get(os.path.splitext(file)[1].lower())
if not content_type:
raise SystemExit("only .mp4 and .mov files are accepted")
# ONE idempotency key for the upload AND the job that confirms it (see "Idempotency").
idempotency_key = str(uuid.uuid4())
# 1. Register the upload: the answer carries the presigned PUT URL and the limits.
upload = call_json(
"POST",
"/v1/uploads",
body={"filename": os.path.basename(file), "sizeBytes": len(data), "contentType": content_type},
idempotency_key=idempotency_key,
)
upload_id, put_url = upload["data"]["uploadId"], upload["data"]["putUrl"]
# 2. PUT the bytes. No API key here (the URL carries its own authorisation) and
# the SAME Content-Type as step 1: it is the media type the stored file keeps.
put = request.Request(put_url, data=data, method="PUT", headers={"Content-Type": content_type})
with request.urlopen(put) as response:
if response.status >= 300:
raise SystemExit(f"upload failed with status {response.status}")
# 3. Pre-flight: measures the duration and decides whether the file can be converted.
call("POST", "/v1/uploads/" + upload_id + "/preflight")
# 4. Confirm the conversion. Minutes are reserved HERE; the estimate says how many.
created = call_json(
"POST",
"/v1/jobs",
body={"uploadId": upload_id, "voiceIds": [voice_id], "name": os.path.basename(file)},
idempotency_key=idempotency_key,
)
job = created["data"]["job"]
estimate = created["data"]["estimate"]
print(f"job {job['id']}: {job['status']} (estimate: {estimate['planSeconds']} s from the plan, {estimate['overageSeconds']} s of overage)")
# 5. Wait. In production a webhook (see "Webhooks") replaces this loop.
while job["status"] not in TERMINAL_STATUSES:
time.sleep(POLL_SECONDS)
job = call_json("GET", "/v1/jobs/" + job["id"])["data"]
print(f"job {job['id']}: {job['status']}")
if job["status"] != "done":
raise SystemExit(f"the job ended {job['status']}")
# 6. Download: the API answers a redirect to a short-lived URL. Read the Location
# WITHOUT following it, then fetch that URL WITHOUT the key.
status, headers, _ = call("GET", "/v1/jobs/" + job["id"] + "/download", follow_redirects=False)
location = headers.get("Location")
if not location:
raise SystemExit(f"expected a redirect, got status {status}")
with request.urlopen(location) as result, open("result.mp4", "wb") as out:
out.write(result.read())
print("saved result.mp4")
if __name__ == "__main__":
try:
main(sys.argv[1] if len(sys.argv) > 1 else None, sys.argv[2] if len(sys.argv) > 2 else None)
except ApiError as exc:
print(f"API error: {exc}", file=sys.stderr)
sys.exit(1)Voces#
Hay dos familias de voces: el banco, común a todo el mundo, y las tuyas propias. La respuesta dice a cuál pertenece cada una, y solo tú ves las tuyas.
Cada voz trae un enlace a una muestra para que la escuches antes de gastar minutos. Consultar una voz propia activa por primera vez GENERA su muestra durante esa misma petición —una lectura de verdad de la voz, que se hace una sola vez y se guarda—, así que la respuesta tarda un momento y ya trae el enlace.
Una muestra vacía significa que la voz no está activa, o que generarla falló. No se pierde nada: la respuesta sigue completa, y una consulta posterior vuelve a intentarlo en cuanto pasa una breve espera.
El guion que se lee en esa muestra va en el idioma de la cuenta dueña de la voz; si la cuenta no tiene ninguno fijado, en el que pidas en la cabecera de idioma; y en español si ninguno de los dos lo dice. Como la muestra se hace una sola vez y se guarda, el idioma vigente en la petición que la genera es el que se conserva.
Etiquetas del banco
El banco se filtra por estas etiquetas. Los códigos son invariantes; lo que cambia con el idioma es la palabra que ves.
- language (Etiqueta)
arárabedealemáneninglésesespañolfrfrancéshihindiititalianojajaponéskocoreanonlneerlandésplpolacoptportuguésrorumanosvsuecotrturcozhchino- accent (Etiqueta)
americanamericanoandalusianandaluzargentineargentinoaustralianaustralianoaustrianaustríacobavarianbávarobelgianbelgabrazilianbrasileñobritishbritánicocanadiancanadiensecastiliancastellanocatalancatalánchileanchilenocolombiancolombianocubancubanoeuropeaneuropeofilipinofilipinofrenchfrancésgaliciangallegogermanalemánindianindioirishirlandésitalianitalianojamaicanjamaicanolatin-americanlatinoamericanomexicanmexicanoneutralneutronew-zealandneozelandésnigeriannigerianoparisianparisinoperuvianperuanopuerto-ricanpuertorriqueñoquebecoisquebequésscottishescocéssingaporeansingapurensesouth-africansudafricanospanishespañolstandardestándarswedishsuecoswisssuizotransatlantictransatlánticotuscantoscanous-southernsureñovenezuelanvenezolanowelshgalés- age (Etiqueta)
youngjovenadultadultoseniormayor- gender (Etiqueta)
femalefemeninomalemasculinoneutralneutro- useCase (Etiqueta)
advertisementpublicidadasmrASMRaudiobookaudiolibroscharacterspersonajesconversationalconversacióneducationaldivulgaciónentertainmententretenimientomeditationmeditaciónnarrationnarraciónnewsinformativossocial-mediaredes sociales
Conversiones#
Crear una conversión devuelve un trabajo con un estado que vas consultando hasta que termina. Los estados posibles viajan en el esquema, y son los mismos que ves en la aplicación web.
Un trabajo recorre estos estados en este orden. Los tres últimos son finales: una vez ahí, no vuelve a cambiar.
confirmedqueuedprocessingdone(final)failed(final)cancelled(final)
Si preguntas en bucle en vez de recibir un webhook, no consultes el trabajo más a menudo que esto: 5 segundos
Cancelar es una orden, no un efecto inmediato: la petición se acepta y la cancelación ocurre después. Vuelve a consultar el trabajo para saber si llegó a tiempo. Si se cancela antes de terminar, los minutos reservados vuelven a tu saldo.
Varias voces en un mismo vídeo#
Si en tu vídeo hablan varios personajes, cada uno puede sonar con su propia voz. La subida se analiza para separar a quien habla, y en la conversión eliges una voz para cada hablante.
- Después del análisis previo de la subida, pide la detección de hablantes. Se hace en segundo plano y pedirla otra vez no cambia nada.
- Consulta la detección hasta que deje de estar en curso. Cuando está lista, cada hablante trae los tramos en los que habla y un fragmento corto de su propia voz para reconocerlo.
- Crea la conversión desde esa subida indicando, para cada variante, qué voz lleva cada hablante. El primer hablante conserva siempre la voz de la variante; los que no indiques, también.
Una detección pasa por estos estados; todos menos el primero son finales:
pendingready(final)single(final)too_many(final)failed(final)
La detección no consume minutos, y una conversión con varias voces cuesta exactamente lo mismo que con una: cuenta la duración del vídeo, no el número de voces.
Si la función de varias voces no está disponible para tu cuenta en ese momento, o la detección no está lista, la conversión se rechaza con su código: nunca se convierte con una sola voz en su lugar. Si una detección sigue en curso pasado un rato largo, convierte con una sola voz.
Una conversión que reutiliza el vídeo de otra no lleva detección propia: para varias voces, sube el vídeo de nuevo.
{
"data": {
"speakers": [
{
"index": 0,
"previewUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/speaker-0.m4a?signature=example",
"segments": [
{
"endMs": 12300,
"startMs": 0
},
{
"endMs": 41680,
"startMs": 28640
}
],
"totalMs": 25340
},
{
"index": 1,
"previewUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/speaker-1.m4a?signature=example",
"segments": [
{
"endMs": 28640,
"startMs": 12300
}
],
"totalMs": 16340
}
],
"status": "ready",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}{
"name": "Spring spot — two characters",
"speakers": [
{
"assignments": [
{
"speaker": 1,
"voiceId": "l1a3m5v7o9i2c4e6b8d0f2h4"
}
],
"variantVoiceId": "v4n8k2m6p1q9s3t7w5x1y2z4"
}
],
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6",
"voiceIds": [
"v4n8k2m6p1q9s3t7w5x1y2z4"
]
}El servidor MCP sigue convirtiendo con una voz por variante: varias voces es, por ahora, cosa de la API.
Descarga#
La misma operación sirve el vídeo convertido, su extracto de audio, una pista separada o la narración: cuál de ellos es un parámetro de la consulta, documentado en la referencia de más abajo, y el vídeo es lo que recibes si no dices nada.
Cuando un trabajo tiene varias variantes —porque pediste varias voces o varias pistas— hay que decir cuál quieres: sin ese dato no hay una respuesta correcta y la petición se rechaza. La consulta del trabajo te dice cuáles hay.
Todo fichero que descargues lleva la marca de contenido generado con inteligencia artificial que exige la normativa europea. Quitarla está prohibido por los términos del servicio.
Narraciones#
Narrar un guion con una de tus voces usables crea un trabajo igual que una conversión: se consulta y se descarga por las mismas operaciones.
La respuesta dice que la petición ha sido ACEPTADA, no que el trabajo ya esté en cola: el encolado ocurre justo después. Si la respuesta no te llega o dudas de si se registró, repite la petición con la misma clave de idempotencia; no se duplicará.
Webhooks#
Si no quieres preguntar en bucle por el estado de un trabajo, da de alta un endpoint desde Ajustes y te avisamos cuando termine. La lista de eventos y la forma exacta de cada uno están más abajo.
Las entregas no salen de un conjunto fijo de direcciones, así que no filtres por origen: la firma es lo que demuestra que una entrega es nuestra.
job.completed
A job finished successfully
Delivered to every enabled endpoint of yours subscribed to job.completed. Verify MIY-Signature (t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your endpoint secret>, tolerance 300 s) before trusting the body. Answer any 2xx; anything else is retried — 8 attempts in all, waiting 1 min, 5 min, 15 min, 1 h, 4 h, 12 h and 24 h between them — and given up after the last one.
MIY-Signature— Signature of the delivery:t=<unix seconds>,v1=<hex>.
{
"createdAt": "2026-09-13T10:04:12.000Z",
"data": {
"jobId": "k7f2q9x1m3p8w5z2c4v6b8n0",
"kind": "conversion",
"links": {
"download": "https://www.makeityours.app/api/v1/jobs/k7f2q9x1m3p8w5z2c4v6b8n0/download",
"job": "https://www.makeityours.app/api/v1/jobs/k7f2q9x1m3p8w5z2c4v6b8n0"
},
"status": "done"
},
"id": "e5v7e9n1t3a5b7c9d1e3f5g7",
"type": "job.completed"
}job.failed
A job failed or was cancelled
Delivered to every enabled endpoint of yours subscribed to job.failed. Verify MIY-Signature (t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your endpoint secret>, tolerance 300 s) before trusting the body. Answer any 2xx; anything else is retried — 8 attempts in all, waiting 1 min, 5 min, 15 min, 1 h, 4 h, 12 h and 24 h between them — and given up after the last one.
MIY-Signature— Signature of the delivery:t=<unix seconds>,v1=<hex>.
{
"createdAt": "2026-09-13T10:04:12.000Z",
"data": {
"jobId": "f4a6i8l0e2d4j6o8b0c2d4e6",
"kind": "conversion",
"links": {
"download": null,
"job": "https://www.makeityours.app/api/v1/jobs/f4a6i8l0e2d4j6o8b0c2d4e6"
},
"status": "failed"
},
"id": "e6v8e0n2t4a6b8c0d2e4f6g8",
"type": "job.failed"
}Verificar la firma
La cabecera puede traer más de una suma mientras rotas el secreto: acepta la entrega si cualquiera de ellas cuadra.
const { createHmac, timingSafeEqual } = require('node:crypto');
const SIGNATURE_HEADER = 'MIY-Signature';
const SCHEME = 'v1';
const TOLERANCE_SECONDS = 300;
// `header` is the value of SIGNATURE_HEADER. `body` MUST be the raw payload you
// received, as a string: re-serialising the parsed JSON changes the bytes, and a
// different byte is a different signature.
function verify(header, body, secret, nowSeconds) {
if (typeof header !== 'string' || header === '') return false;
// `t` is kept TWICE: as the header wrote it (the bytes the sender signed over)
// and as a number (for the clock). Re-serialising it would be the same mistake
// as re-serialising the body.
let timestampText = null;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const eq = part.indexOf('=');
if (eq <= 0) continue;
const name = part.slice(0, eq).trim();
const value = part.slice(eq + 1).trim();
if (name === 't') {
if (!/^[0-9]{1,15}$/.test(value)) return false;
timestampText = value;
timestamp = Number(value);
} else if (name === SCHEME) {
// Several entries are allowed while a secret is being rotated.
signatures.push(value);
}
}
if (timestamp === null || signatures.length === 0) return false;
const expected = createHmac('sha256', secret).update(timestampText + '.' + body).digest('hex');
const expectedBytes = Buffer.from(expected, 'hex');
const matches = signatures.some((candidate) => {
const candidateBytes = Buffer.from(candidate, 'hex');
if (candidateBytes.length !== expectedBytes.length) return false;
return timingSafeEqual(candidateBytes, expectedBytes);
});
if (!matches) return false;
// Signature first, clock second: a tampered body is a forgery, a well-signed
// old one is merely late — and only the second is worth a different log line.
return Math.abs(nowSeconds - timestamp) <= TOLERANCE_SECONDS;
}
module.exports = { verify };import hashlib
import hmac
SIGNATURE_HEADER = "MIY-Signature"
SCHEME = "v1"
TOLERANCE_SECONDS = 300
def verify(header, body, secret, now_seconds):
"""`body` is the raw payload as bytes. Never re-serialise the parsed JSON."""
if not header:
return False
# `t` is kept twice: as the header wrote it (the bytes the sender signed
# over) and as a number (for the clock). Re-serialising it would be the same
# mistake as re-serialising the body.
timestamp_text = None
timestamp = None
signatures = []
for part in header.split(","):
name, _, value = part.partition("=")
name, value = name.strip(), value.strip()
if name == "t":
# `str.isdigit()` alone accepts Arabic-Indic and other non-ASCII
# digits, which `int()` then happily parses: the Node twin bounds
# the same field with `[0-9]{1,15}` and this has to agree.
if not (value.isascii() and value.isdigit()) or len(value) > 15:
return False
timestamp_text = value
timestamp = int(value)
elif name == SCHEME:
# Several entries are allowed while a secret is being rotated.
signatures.append(value)
if timestamp is None or not signatures:
return False
expected = hmac.new(
secret.encode("utf-8"),
(timestamp_text + ".").encode("utf-8") + body,
hashlib.sha256,
).hexdigest()
# `compare_digest` RAISES on two `str` with a non-ASCII character in them,
# and `candidate` comes straight from the request: a stray byte in the header
# must be a rejected delivery, not a crashed handler. The Node twin reaches the
# same verdict through `Buffer.from(candidate, "hex")`.
if not any(
candidate.isascii() and hmac.compare_digest(candidate, expected)
for candidate in signatures
):
return False
# Signature first, clock second.
return abs(now_seconds - timestamp) <= TOLERANCE_SECONDSReintentos
Responde con cualquier estado de éxito y hazlo rápido: encola el trabajo por tu lado y contesta, no proceses antes de contestar. Cualquier otra respuesta —o ninguna— hace que reintentemos con estas esperas, y después se da por perdida.
| Intento | Espera antes del siguiente |
|---|---|
| 1 | 1 minuto |
| 2 | 5 minutos |
| 3 | 15 minutos |
| 4 | 1 hora |
| 5 | 4 horas |
| 6 | 12 horas |
| 7 | 24 horas |
| 8 | Ninguno |
Cuando varios intentos seguidos fallan te avisamos por correo, una sola vez, para que un endpoint caído no se quede caído sin que nadie lo vea. Volver a entregar lo limpia.
Una entrega a un endpoint que has desactivado, o hecha mientras tu plan no incluye la API, ni se intenta ni se da por perdida: se retiene, se vuelve a examinar periódicamente y se entrega en cuanto el endpoint se activa o el plan vuelve. Las entregas que agotaron su último intento se conservan un tiempo para que veas qué falló.
Desde Ajustes puedes enviarte un evento de prueba. Va firmado igual que los de verdad, pero NO forma parte del contrato: su tipo no está en la lista de eventos y no debes escribir lógica que dependa de él.
Servidor MCP#
El servidor MCP deja que un asistente use tu cuenta por ti: listar voces, empezar una subida, crear una conversión o una narración y consultar cómo va. Son las mismas operaciones de la API, con las mismas reglas de plan, de límites y de idempotencia.
https://www.makeityours.app/api/mcphttps://www.makeityours.app/.well-known/oauth-protected-resourcevoices:readjobs:readjobs:writenarrations:writewebhooks:manageoffline_access
webhooks:manage— reservado: ninguna operación ni tool lo exige todavía.
Cómo añadirlo a tu asistente
Los asistentes que hablan este protocolo se configuran igual: se añade un servidor remoto con su dirección y se autoriza en el navegador. Los menús cambian de nombre de una versión a otra; el camino es siempre éste.
El mismo camino en cualquier asistente
- Abre los ajustes del asistente y busca la sección de conectores o servidores externos.
- Elige añadir un servidor remoto (no uno local ni un comando).
- Pega la dirección del servidor que aparece arriba.
- Acepta cuando te lleve al navegador a iniciar sesión y autorizar.
- Vuelve al asistente y comprueba que las herramientas aparecen en su lista.
Claude
En Claude, en la web y en la aplicación de escritorio, el servidor es un conector personalizado: abre los ajustes, busca la sección de conectores, elige añadir un conector personalizado y pega la dirección.
En Claude Code lo añade un solo comando; después, el comando /mcp abre el navegador para autorizarlo:
claude mcp add --transport http makeityours https://www.makeityours.app/api/mcpCursor
En Cursor el servidor va en el fichero de configuración MCP, el del proyecto o el de tu directorio personal. Pega esto; Cursor te lleva al navegador la primera vez que conecta:
{
"mcpServers": {
"makeityours": {
"url": "https://www.makeityours.app/api/mcp"
}
}
}ChatGPT
En ChatGPT los servidores propios se añaden como conectores desde los ajustes, en la sección para desarrolladores; el menú exacto depende de tu plan. Pega la dirección y autoriza en el navegador.
La autorización, por dentro
No hace falta registrar nada por adelantado: el asistente se da de alta solo la primera vez y la autorización usa el intercambio con verificador que exige el protocolo. Tú solo ves la pantalla de consentimiento en el navegador, con el nombre del cliente y los permisos que pide.
Si el asistente pide además el permiso de acceso prolongado, recibe una credencial de renovación y no tiene que volver a mandarte al navegador cada hora.
El canje del código tiene que indicar para qué recurso se pide el token. Un cliente anterior a esa parte del estándar no puede usar este servidor: recibirá un token que el servidor no acepta. Si tu asistente falla ahí, actualízalo.
Los asistentes que piden el permiso de acceso de larga duración piden también la pantalla de consentimiento cada vez (lo hace el cliente del protocolo por su cuenta), así que ves qué permisos se están pidiendo en cada nueva autorización, también cuando ya habías autorizado antes al mismo cliente. Un cliente que no lo pide reutiliza una autorización que ya diste a ese mismo cliente para los mismos permisos, sin volver a enseñar la pantalla.
Las autorizaciones se revocan desde Ajustes → API → Autorizaciones, y el corte es inmediato para todas las credenciales de ese cliente.
Cada herramienta declara en su descripción que lo que produce es contenido sintético marcado como tal.
Herramientas disponibles
Cada herramienta envuelve una operación de la API y exige el mismo permiso que ella.
list_voicesListar voces
Lista las voces disponibles: el banco de voces o las voces propias del titular, paginadas por cursor. El resultado es contenido sintético marcado como tal (AI Act).
List the voice bank or your own voices.
listVoicesPermiso: voices:readClave de idempotencia: No
Resultado
get_voiceConsultar una voz
Devuelve una voz con su enlace de muestra, sea del banco o propia del titular. El resultado es contenido sintético marcado como tal (AI Act).
Get one voice with a sample link.
getVoicePermiso: voices:readClave de idempotencia: No
Resultado
list_jobsListar conversiones
Lista las conversiones y narraciones del titular, de la más reciente a la más antigua, paginadas por cursor. Las subidas sin confirmar no se listan. El resultado es contenido sintético marcado como tal (AI Act).
List your jobs.
listJobsPermiso: jobs:readClave de idempotencia: No
Resultado
get_jobConsultar una conversión
Devuelve el estado de una conversión o narración del titular y sus variantes. El resultado es contenido sintético marcado como tal (AI Act).
Get the state of a job.
getJobPermiso: jobs:readClave de idempotencia: No
Resultado
create_conversionCrear una conversión
Confirma una conversión a partir de una subida o de un trabajo anterior del titular, con hasta tres voces. Ejecuta él mismo el pre-vuelo de la subida si aún no se ha hecho. Consume minutos del plan igual que la web y devuelve el desglose estimado de plan y excedente. Requiere una clave de idempotencia. El resultado es contenido sintético marcado como tal (AI Act).
Create a conversion (runs the pre-flight first if needed).
createJobPermiso: jobs:writeClave de idempotencia: Sí
Resultado
Errores posibles
rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundidempotency_key_requiredidempotency_conflictapi_daily_overage_capupload_not_readynot_confirmableinvalid_stateinvalid_voicesinvalid_tuneoverage_unavailable_currencyvoice_service_pausedaccount_closingsubscription_pausedenqueue_failedunavailablerejectednot_configuredpreflight_unavailable
create_narrationCrear una narración
Narra un guion con una de las voces usables del titular y devuelve el trabajo que luego se consulta y se descarga. Consume minutos del plan igual que la web. Requiere una clave de idempotencia. El resultado es contenido sintético marcado como tal (AI Act).
Create a narration.
createNarrationPermiso: narrations:writeClave de idempotencia: Sí
Resultado
Errores posibles
rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundidempotency_key_requiredidempotency_conflictapi_daily_overage_capnarration_disabledinvalid_scriptvoice_not_usableoverage_unavailable_currencyvoice_service_pausedaccount_closingsubscription_pausedenqueue_failedunavailable
get_download_urlObtener la URL de descarga
Devuelve una URL de descarga temporal del resultado (vídeo, extracto de audio, una pista suelta o la narración) y cuándo caduca. No redirige. El resultado es contenido sintético marcado como tal (AI Act).
Get a short-lived download URL of a result (no redirect).
downloadJobPermiso: jobs:readClave de idempotencia: No
Resultado
dataObligatorioDownloadUrlResult
create_uploadEmpezar una subida
Empieza una subida y devuelve la URL a la que subir el fichero con un PUT, sus límites y cuándo caduca. Después se llama a create_conversion con el uploadId. El resultado es contenido sintético marcado como tal (AI Act).
Start an upload and get the PUT URL.
createUploadPermiso: jobs:writeClave de idempotencia: Sí
Resultado
Referencia#
Generada del contrato: lo que hay aquí es exactamente lo que la API valida y responde.
1.1.0Dirección base: https://www.makeityours.app/apivoices:readjobs:readjobs:writenarrations:writewebhooks:manage
webhooks:manage— reservado: ninguna operación ni tool lo exige todavía.
https://www.makeityours.app/api/openapi.jsonvoices
GET/v1/voices
List voices
Lists the shared voice bank (default) or your own cloned voices (kind=own). Every item carries kind: a bank voice is the full Voice shape, one of your clones is the smaller OwnVoice (id, name, status). Bank filters take the stable codes served by GET /v1/voices/facets. The bank is paginated by cursor, newest first, and may change between pages: a voice retired meanwhile simply stops appearing, and a voice added to the bank while you page sits above every cursor, so it shows up on your next full pass, not in the current one. A nextCursor is bound to the FILTERS it was minted under, so replaying it with a different set of filters answers 400 invalid_cursor instead of paging a different sequence. With kind=own the list is your live clones (active and recycled) returned COMPLETE in one response: nextCursor is always null, and cursor, limit and the facet filters are not applied — they are still validated, so an out-of-range value answers 400 invalid_request with either kind.
Permiso: voices:readlistVoices
Consulta
kindOpcionalstringValores admitidos"bank""own"Por defecto:"bank"bank(default) lists the shared bank;ownlists your own clones (complete, unpaginated).languageOpcionalstringValores admitidos"ar""de""en""es""fr""hi""it""ja""ko""nl""pl""pt""ro""sv""tr""zh"accentOpcionalstringValores admitidos"american""andalusian""argentine""australian""austrian""bavarian""belgian""brazilian""british""canadian""castilian""catalan""chilean""colombian""cuban""european""filipino""french""galician""german""indian""irish""italian""jamaican""latin-american""mexican""neutral""new-zealand""nigerian""parisian""peruvian""puerto-rican""quebecois""scottish""singaporean""south-african""spanish""standard""swedish""swiss""transatlantic""tuscan""us-southern""venezuelan""welsh"ageOpcionalstringValores admitidos"young""adult""senior"genderOpcionalstringValores admitidos"female""male""neutral"useCaseOpcionalstringValores admitidos"advertisement""asmr""audiobook""characters""conversational""educational""entertainment""meditation""narration""news""social-media"cursorOpcionalstringLongitud mínima:1Longitud máxima:256Opaque cursor of the next page, fromnextCursor. A cursor of another list answers 400invalid_cursor.limitOpcionalintegerPor defecto:20Mínimo:1Máximo:100Page size, 1–100 (default 20).
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200One page of voices.
VoiceListResponseEjemplo: bank — The shared bank (the default): one page, nextCursor to continue.
{
"data": [
{
"accent": "Castilian Spanish",
"accentCode": "castilian",
"ageCode": "adult",
"ageRange": "Adult",
"description": "Warm, close adult female voice in Castilian Spanish, for advertisements.",
"gender": "Female",
"genderCode": "female",
"id": "v4n8k2m6p1q9s3t7w5x1y2z4",
"kind": "bank",
"language": "Spanish",
"languageCode": "es",
"name": "Ana",
"sampleText": "Hi, I am Ana. This is how your ad sounds with my voice.",
"useCase": "Advertisement",
"useCaseCode": "advertisement"
},
{
"accent": "British English",
"accentCode": "british",
"ageCode": "young",
"ageRange": "Young",
"description": "Clear, energetic young male voice in British English, for narration.",
"gender": "Male",
"genderCode": "male",
"id": "l1a3m5v7o9i2c4e6b8d0f2h4",
"kind": "bank",
"language": "English",
"languageCode": "en",
"name": "Liam",
"sampleText": "Hi, I am Liam. This is how your ad sounds with my voice.",
"useCase": "Narration",
"useCaseCode": "narration"
}
],
"nextCursor": "eyJsIjoidm9pY2VzIiwiYyI6IjIwMjYtMDktMTJUMTg6MzA6MDAuMDAwWiIsImkiOiJsMWEzbTV2N285aTJjNGU2YjhkMGYyaDQifQ"
}Ejemplo: own — Your own clones (kind=own): complete in one response, nextCursor always null.
{
"data": [
{
"id": "o1w3n5v7o9i1c2e4b6d8f0h2",
"kind": "own",
"name": "My voice",
"status": "active"
}
],
"nextCursor": null
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
- invalid_cursor: The cursor is not one this list issued: malformed, or minted for another list. Restart from the first page.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}GET/v1/voices/facets
List the served filter values
The codes, per dimension, for which the bank currently has at least one voice. Not paginated.
Permiso: voices:readgetVoiceFacets
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200The served filter values.
VoiceFacetsResponse{
"data": {
"accent": [
"castilian",
"mexican",
"british",
"american",
"brazilian",
"neutral"
],
"age": [
"young",
"adult",
"senior"
],
"gender": [
"female",
"male"
],
"language": [
"es",
"en",
"pt",
"de",
"fr",
"it"
],
"useCase": [
"advertisement",
"narration",
"social-media",
"educational"
]
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}GET/v1/voices/{id}
Get a voice
One voice of the bank, or one of your own clones, with a short-lived link to its sample clip — kind says which of the two shapes you got. A clone with no clip yet gets one minted on this call; when that is not possible — a recycled clone, or a clip that could not be produced right now — sample is null: for an active clone, ask again later. A voice that is retired, belongs to someone else or does not exist answers 404.
Permiso: voices:readgetVoice
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200The voice with its sample.
VoiceResponseEjemplo: bank — A voice of the shared bank, with its short-lived sample clip.
{
"data": {
"accent": "Castilian Spanish",
"accentCode": "castilian",
"ageCode": "adult",
"ageRange": "Adult",
"description": "Warm, close adult female voice in Castilian Spanish, for advertisements.",
"gender": "Female",
"genderCode": "female",
"id": "v4n8k2m6p1q9s3t7w5x1y2z4",
"kind": "bank",
"language": "Spanish",
"languageCode": "es",
"name": "Ana",
"sample": {
"expiresAt": "2026-09-13T10:15:00.000Z",
"url": "https://media.makeityours.app/samples/v4n8k2m6p1q9s3t7w5x1y2z4.mp3?signature=example"
},
"sampleText": "Hi, I am Ana. This is how your ad sounds with my voice.",
"useCase": "Advertisement",
"useCaseCode": "advertisement"
}
}Ejemplo: own — One of your own clones, with the clip minted for it.
{
"data": {
"id": "o1w3n5v7o9i1c2e4b6d8f0h2",
"kind": "own",
"name": "My voice",
"sample": {
"expiresAt": "2026-09-13T10:15:00.000Z",
"url": "https://media.makeityours.app/samples/o1w3n5v7o9i1c2e4b6d8f0h2.mp3?signature=example"
},
"status": "active"
}
}Ejemplo: ownWithoutSample — One of your own clones with no clip available right now: sample is null.
{
"data": {
"id": "o1w3n5v7o9i1c2e4b6d8f0h2",
"kind": "own",
"name": "My voice",
"sample": null,
"status": "active"
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}uploads
POST/v1/uploads
Start an upload
Registers a file you are about to upload and returns a presigned PUT URL plus the product limits. PUT the bytes there, then call the pre-flight. The server never fetches remote URLs. A repeated Idempotency-Key returns the same upload while it is still a draft, with a fresh putUrl — and discards any pre-flight already run on it, so run the pre-flight again after your PUT. Once that upload has been confirmed into a job under the same key, the key answers 409 idempotency_conflict. Nothing has been uploaded yet, so a 422 rejected here can only carry reason unsupported_container or too_large; the other reasons belong to the pre-flight.
Permiso: jobs:writeClave de idempotencia: optionalcreateUpload
Cabeceras
Idempotency-KeyOpcionalstringPatrón:^[\x21-\x7E]+$Longitud mínima:1Longitud máxima:255Client-chosen token (1–255 printable ASCII characters, no spaces; a UUID is ideal). The key is scoped to your account — every API key and OAuth client of yours shares one namespace. Replaying the same key with the same request (same operation, same body) returns the original result again for as long as the job it created exists: the key is bound to that job (for an upload, to its draft), so it is released only when the job is deleted or the abandoned draft is purged. The same key with a different request answers 409idempotency_conflict, and so does an upload key replayed after that upload was confirmed into a job (seePOST /v1/uploads). Optional on this operation.Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Cuerpo de la petición
{
"contentType": "video/mp4",
"filename": "spring-spot.mp4",
"sizeBytes": 48213907
}Respuestas
201The upload is registered; PUT the file to putUrl.
{
"data": {
"constraints": {
"containers": [
"mp4",
"mov"
],
"maxDurationSeconds": 180,
"maxSizeBytes": 524288000
},
"expiresAt": "2026-09-13T11:00:00.000Z",
"putUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/source.mp4?signature=example",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
409Error codes:
- idempotency_conflict: The Idempotency-Key was already used with a different request, or with an upload that has since been confirmed into a job. Use a new key.
{
"error": {
"code": "idempotency_conflict",
"message": "This Idempotency-Key was already used with a different request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}422Error codes:
- rejected: The file was refused by the pre-flight: unsupported container, no video or audio track, too long or too large. error.reason says which — the closed ApiErrorReason enum — and message says the same thing in the negotiated language.
{
"error": {
"code": "rejected",
"message": "The file was refused: check the format, duration, size and audio track.",
"reason": "no_audio",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- upload_unavailable: The upload could not be registered (our side). Retry.
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "upload_unavailable",
"message": "We could not prepare the upload. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
- not_configured: The environment is missing a piece of configuration (our side). Retry later.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}POST/v1/uploads/{id}/preflight
Run the pre-flight of an upload
Analyses the uploaded file (container, codecs, duration) and persists the report. Call it after the PUT succeeded; before that it answers 409. A file outside the product limits or without an audio track answers 422 rejected with reason naming the rule (unsupported_container, no_video, no_audio, too_long or too_large) — the same rules the web applies.
Permiso: jobs:writerunUploadPreflight
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200The file is analysed and ready to convert.
PreflightResponse{
"data": {
"preflight": {
"audioCodec": "aac",
"audioDurationS": 41.68,
"container": "mov,mp4,m4a,3gp,3g2,mj2",
"containerDurationS": 41.72,
"hasAudio": true,
"remuxSafe": true,
"vfrSuspected": false,
"videoCodec": "h264"
},
"ready": true,
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}409Error codes:
- upload_not_ready: The upload is not ready for this step: on the pre-flight, the PUT has not completed; on a job, the upload has not been pre-flighted yet.
{
"error": {
"code": "upload_not_ready",
"message": "The upload has not been analysed yet. Run the pre-flight first.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}422Error codes:
- rejected: The file was refused by the pre-flight: unsupported container, no video or audio track, too long or too large. error.reason says which — the closed ApiErrorReason enum — and message says the same thing in the negotiated language.
{
"error": {
"code": "rejected",
"message": "The file was refused: check the format, duration, size and audio track.",
"reason": "no_audio",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- preflight_unavailable: The file could not be analysed (our side). Retry.
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "preflight_unavailable",
"message": "We could not analyse the file. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
- not_configured: The environment is missing a piece of configuration (our side). Retry later.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}GET/v1/uploads/{id}/speakers
Get the speakers of an upload
The speaker detection of an upload: its status and, once ready, each speaker with the stretches in which it talks and a short presigned clip of its own voice. Read-only: it never starts a detection — 409 speakers_not_ready if none was requested, or if the last one stopped without an outcome (POST retries it once). Poll it while pending, for up to 15 minutes (then convert with one voice); every other status is final. Requires the several-voices feature (403 multi_voice_disabled otherwise). An upload whose pre-flight refused the file answers 409 not_confirmable.
Permiso: jobs:readgetSpeakers
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200The detection as it stands now.
SpeakerDetectionResponseEjemplo: pending — Still running: ask again.
{
"data": {
"speakers": [],
"status": "pending",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}Ejemplo: ready — Two speakers found: assign a voice to speaker 1 in POST /v1/jobs.
{
"data": {
"speakers": [
{
"index": 0,
"previewUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/speaker-0.m4a?signature=example",
"segments": [
{
"endMs": 12300,
"startMs": 0
},
{
"endMs": 41680,
"startMs": 28640
}
],
"totalMs": 25340
},
{
"index": 1,
"previewUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/speaker-1.m4a?signature=example",
"segments": [
{
"endMs": 28640,
"startMs": 12300
}
],
"totalMs": 16340
}
],
"status": "ready",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}Ejemplo: single — One speaker: convert with one voice.
{
"data": {
"speakers": [],
"status": "single",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
- multi_voice_disabled: The several-voices feature is not available to this account right now: it is switched off for the moment, the plan does not include it, or the account has no open billing period at this instant (a renewal in progress — retry in a while). Nothing was written. A job without speakers still converts with one voice per variant.
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scopemulti_voice_disabled
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}409Error codes:
- upload_not_ready: The upload is not ready for this step: on the pre-flight, the PUT has not completed; on a job, the upload has not been pre-flighted yet.
- not_confirmable: The upload cannot be turned into a job: its draft is gone or was already confirmed.
- speakers_not_ready: The upload has no finished speaker detection with several speakers: it was never requested, or it stopped without an outcome (request it with POST /v1/uploads/{id}/speakers — a stopped detection is retried once), it is still pending, or its outcome is single, too_many or failed. A job created from a previous job (sourceJobId) never has one: upload the video again. Otherwise create the job without speakers, or wait for ready.
{
"error": {
"code": "upload_not_ready",
"message": "The upload has not been analysed yet. Run the pre-flight first.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}POST/v1/uploads/{id}/speakers
Detect the speakers of an upload
Asks for the speaker detection of a pre-flighted upload, so that a conversion can give each speaker its own voice (speakers of POST /v1/jobs). It runs in the background: the answer is 202 with the detection as it stands now, and GET on the same path tells you when it is no longer pending. Idempotent by upload, without Idempotency-Key: while a detection is pending, or once it has an outcome, calling it again changes nothing (an outcome is never recomputed — upload the file again for a new one); a detection that stopped without an outcome is retried once. It does not consume minutes. Up to 4 speakers are told apart; more is too_many. Requires the several-voices feature (403 multi_voice_disabled otherwise). An upload that has not been pre-flighted yet answers 409 upload_not_ready; one whose pre-flight refused the file answers 409 not_confirmable; one already turned into a job answers 404.
Permiso: jobs:writestartSpeakerDetection
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
202The detection is requested, or already has an outcome; its state as it stands now.
SpeakerDetectionResponseEjemplo: pending — Requested: the detection runs in the background.
{
"data": {
"speakers": [],
"status": "pending",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}Ejemplo: ready — Already detected: calling again changes nothing.
{
"data": {
"speakers": [
{
"index": 0,
"previewUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/speaker-0.m4a?signature=example",
"segments": [
{
"endMs": 12300,
"startMs": 0
},
{
"endMs": 41680,
"startMs": 28640
}
],
"totalMs": 25340
},
{
"index": 1,
"previewUrl": "https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/speaker-1.m4a?signature=example",
"segments": [
{
"endMs": 28640,
"startMs": 12300
}
],
"totalMs": 16340
}
],
"status": "ready",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
- multi_voice_disabled: The several-voices feature is not available to this account right now: it is switched off for the moment, the plan does not include it, or the account has no open billing period at this instant (a renewal in progress — retry in a while). Nothing was written. A job without speakers still converts with one voice per variant.
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scopemulti_voice_disabled
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}409Error codes:
- upload_not_ready: The upload is not ready for this step: on the pre-flight, the PUT has not completed; on a job, the upload has not been pre-flighted yet.
- not_confirmable: The upload cannot be turned into a job: its draft is gone or was already confirmed.
{
"error": {
"code": "upload_not_ready",
"message": "The upload has not been analysed yet. Run the pre-flight first.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- enqueue_failed: The job could not be handed to the queue (our side). Retry.
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "enqueue_failed",
"message": "The job could not be queued. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}jobs
GET/v1/jobs
List jobs
Your jobs, newest first, paginated by cursor. Drafts (uploads not yet confirmed) are not listed.
Permiso: jobs:readlistJobs
Consulta
cursorOpcionalstringLongitud mínima:1Longitud máxima:256Opaque cursor of the next page, fromnextCursor. A cursor of another list answers 400invalid_cursor.limitOpcionalintegerPor defecto:20Mínimo:1Máximo:100Page size, 1–100 (default 20).
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200One page of jobs.
JobListResponse{
"data": [
{
"createdAt": "2026-09-13T10:00:00.000Z",
"durationSeconds": 42,
"folderId": "f1o3l5d7e9r1a3b5c7d9e1f3",
"id": "k7f2q9x1m3p8w5z2c4v6b8n0",
"name": "Spring spot — Ana and Liam",
"narration": false,
"paused": false,
"status": "queued",
"tags": [
{
"id": "t2a4g6b8c0d2e4f6g8h0i2j4",
"name": "spring"
}
],
"unnamed": false,
"variantCount": 2
},
{
"createdAt": "2026-09-12T18:30:00.000Z",
"durationSeconds": 31,
"folderId": null,
"id": "j2b4d6f8h0k1m3p5r7t9v1x3",
"name": "Untitled conversion",
"narration": false,
"paused": false,
"status": "done",
"tags": [],
"unnamed": true,
"variantCount": 1
}
],
"nextCursor": "eyJsIjoiam9icyIsImMiOiIyMDI2LTA5LTEyVDE4OjMwOjAwLjAwMFoiLCJpIjoiajJiNGQ2ZjhoMGsxbTNwNXI3dDl2MXgzIn0"
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
- invalid_cursor: The cursor is not one this list issued: malformed, or minted for another list. Restart from the first page.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}POST/v1/jobs
Create a conversion
Confirms a conversion from a pre-flighted upload or from a previous job of yours, with up to 3 voices. Consumes minutes from your plan exactly as the web does; the response carries the estimated plan/overage split. Idempotency-Key is required: the same key with the same request answers 202 again with the same job (its current state) and the same estimate; the same key with a different request answers 409. 404 not_found means the uploadId or sourceJobId is unknown or not yours; a voice that cannot be used is 422 invalid_voices. Exceeding your daily overage cap answers 402 and writes nothing. Optional speakers gives some speakers of the video another voice, per variant, for the same minutes: it needs uploadId and a ready detection of that upload (409 speakers_not_ready otherwise), a request that does not fit answers 400 speakers_mismatch, and an account without the feature answers 403 multi_voice_disabled — nothing is ever converted with one voice in its place.
Permiso: jobs:writeClave de idempotencia: requiredcreateJob
Cabeceras
Idempotency-KeyObligatoriostringPatrón:^[\x21-\x7E]+$Longitud mínima:1Longitud máxima:255Client-chosen token (1–255 printable ASCII characters, no spaces; a UUID is ideal). The key is scoped to your account — every API key and OAuth client of yours shares one namespace. Replaying the same key with the same request (same operation, same body) returns the original result again for as long as the job it created exists: the key is bound to that job (for an upload, to its draft), so it is released only when the job is deleted or the abandoned draft is purged. The same key with a different request answers 409idempotency_conflict, and so does an upload key replayed after that upload was confirmed into a job (seePOST /v1/uploads). Required on this operation.Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Cuerpo de la petición
Ejemplo: oneVoice — One voice per variant (the usual request).
{
"name": "Spring spot — Ana and Liam",
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6",
"voiceIds": [
"v4n8k2m6p1q9s3t7w5x1y2z4",
"l1a3m5v7o9i2c4e6b8d0f2h4"
],
"voiceSettings": {
"v4n8k2m6p1q9s3t7w5x1y2z4": {
"similarity": 0.8,
"speakerBoost": true,
"stability": 0.45
}
}
}Ejemplo: withSpeakers — Speaker 1 of the upload in another voice, for the minutes of one voice.
{
"name": "Spring spot — two characters",
"speakers": [
{
"assignments": [
{
"speaker": 1,
"voiceId": "l1a3m5v7o9i2c4e6b8d0f2h4"
}
],
"variantVoiceId": "v4n8k2m6p1q9s3t7w5x1y2z4"
}
],
"uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6",
"voiceIds": [
"v4n8k2m6p1q9s3t7w5x1y2z4"
]
}Respuestas
202The conversion is queued.
JobCreatedResponseEjemplo: oneVoice — Two variants, one voice each.
{
"data": {
"estimate": {
"currency": "usd",
"overageAmountCents": 18,
"overageSeconds": 18,
"planSeconds": 66
},
"job": {
"convertVariantsDone": 0,
"id": "k7f2q9x1m3p8w5z2c4v6b8n0",
"isNarration": false,
"isRemaster": false,
"paused": false,
"status": "queued",
"steps": [
{
"done": false,
"key": "prepare"
},
{
"done": false,
"key": "separate"
},
{
"done": false,
"key": "convert"
},
{
"done": false,
"key": "package"
},
{
"done": false,
"key": "finalize"
}
],
"variants": [
{
"driftMs": null,
"hasResult": false,
"speakers": [],
"variantId": "r3v5a7r9i1a3n5t7x9y1z3w5",
"voiceName": "Ana"
},
{
"driftMs": null,
"hasResult": false,
"speakers": [],
"variantId": "r4v6a8r0i2a4n6t8x0y2z4w6",
"voiceName": "Liam"
}
],
"variantsTotal": 2
}
}
}Ejemplo: withSpeakers — One variant whose speaker 1 takes another voice.
{
"data": {
"estimate": {
"currency": "usd",
"overageAmountCents": 0,
"overageSeconds": 0,
"planSeconds": 42
},
"job": {
"convertVariantsDone": 0,
"id": "k7f2q9x1m3p8w5z2c4v6b8n0",
"isNarration": false,
"isRemaster": false,
"paused": false,
"status": "queued",
"steps": [
{
"done": false,
"key": "prepare"
},
{
"done": false,
"key": "separate"
},
{
"done": false,
"key": "convert"
},
{
"done": false,
"key": "package"
},
{
"done": false,
"key": "finalize"
}
],
"variants": [
{
"driftMs": null,
"hasResult": false,
"speakers": [
{
"index": 1,
"voiceId": "l1a3m5v7o9i2c4e6b8d0f2h4"
}
],
"variantId": "r3v5a7r9i1a3n5t7x9y1z3w5",
"voiceName": "Ana"
}
],
"variantsTotal": 1
}
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
- idempotency_key_required: The operation requires an Idempotency-Key header and none was sent.
- speakers_mismatch: The speakers of the request are malformed or do not fit: not a list of { variantVoiceId, assignments }, a voice id that is not a cuid2, more entries or assignments than allowed, a variantVoiceId that is not in voiceIds or is listed twice, a speaker listed twice, speaker 0 (it always keeps the voice of its variant), or a speaker the detection of the upload did not find. Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}402Error codes:
- api_daily_overage_cap: This credential reached its daily overage cap (UTC day). Nothing was written; retry after midnight UTC or raise the cap in Settings.
{
"error": {
"code": "api_daily_overage_cap",
"message": "This credential reached its daily overage cap. Retry tomorrow or raise the cap in Settings.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
- multi_voice_disabled: The several-voices feature is not available to this account right now: it is switched off for the moment, the plan does not include it, or the account has no open billing period at this instant (a renewal in progress — retry in a while). Nothing was written. A job without speakers still converts with one voice per variant.
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scopemulti_voice_disabled
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}409Error codes:
- idempotency_conflict: The Idempotency-Key was already used with a different request, or with an upload that has since been confirmed into a job. Use a new key.
- speakers_not_ready: The upload has no finished speaker detection with several speakers: it was never requested, or it stopped without an outcome (request it with POST /v1/uploads/{id}/speakers — a stopped detection is retried once), it is still pending, or its outcome is single, too_many or failed. A job created from a previous job (sourceJobId) never has one: upload the video again. Otherwise create the job without speakers, or wait for ready.
- upload_not_ready: The upload is not ready for this step: on the pre-flight, the PUT has not completed; on a job, the upload has not been pre-flighted yet.
- not_confirmable: The upload cannot be turned into a job: its draft is gone or was already confirmed.
- invalid_state: The job is not in a state that admits the operation.
- account_closing: The account requested its deletion; no new jobs can be created while it closes.
- subscription_paused: The plan is paused (a billing incident); no new jobs until billing resumes.
{
"error": {
"code": "idempotency_conflict",
"message": "This Idempotency-Key was already used with a different request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}idempotency_conflictspeakers_not_readyupload_not_readynot_confirmableinvalid_stateaccount_closingsubscription_paused
422Error codes:
- invalid_voices: One or more of voiceIds, or of the voices in speakers, cannot be used for this job (unknown, not yours, retired or not usable right now).
- invalid_tune: A voiceSettings value is outside the documented range.
- overage_unavailable_currency: The job would need overage minutes and overage is billed in US dollars only, which is not the billing currency of this account. Stay within the plan allowance.
{
"error": {
"code": "invalid_voices",
"message": "One or more voices cannot be used for this job.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- enqueue_failed: The job could not be handed to the queue (our side). Retry.
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "enqueue_failed",
"message": "The job could not be queued. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
- voice_service_paused: Voice processing is temporarily paused. Retry later.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}Errores posibles
rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundidempotency_key_requiredidempotency_conflictapi_daily_overage_capspeakers_mismatchmulti_voice_disabledspeakers_not_readyupload_not_readynot_confirmableinvalid_stateinvalid_voicesinvalid_tuneoverage_unavailable_currencyvoice_service_pausedaccount_closingsubscription_pausedenqueue_failedunavailable
GET/v1/jobs/{id}
Get a job
The live state of one of your jobs, conversions and narrations alike. Poll it, or subscribe to webhooks for the terminal events. A draft (an upload not yet confirmed) is not a job: its id answers 404 here.
Permiso: jobs:readgetJob
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
200The job.
JobResponse{
"data": {
"convertVariantsDone": 1,
"id": "j2b4d6f8h0k1m3p5r7t9v1x3",
"isNarration": false,
"isRemaster": false,
"paused": false,
"status": "done",
"steps": [
{
"done": true,
"key": "prepare"
},
{
"done": true,
"key": "separate"
},
{
"done": true,
"key": "convert"
},
{
"done": true,
"key": "package"
},
{
"done": true,
"key": "finalize"
}
],
"variants": [
{
"driftMs": 120,
"hasResult": true,
"speakers": [],
"variantId": "r3v5a7r9i1a3n5t7x9y1z3w5",
"voiceName": "Ana"
}
],
"variantsTotal": 1
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}POST/v1/jobs/{id}/cancel
Cancel a job
Requests the cancellation of a job that is confirmed, queued or processing. The cancellation is applied asynchronously by the worker and the reserved minutes are returned to your plan. A job already finished, failed or cancelled answers 409; a draft answers 404.
Permiso: jobs:writecancelJob
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
202The cancellation is queued; the job as it stands now.
JobResponse{
"data": {
"convertVariantsDone": 0,
"id": "k7f2q9x1m3p8w5z2c4v6b8n0",
"isNarration": false,
"isRemaster": false,
"paused": false,
"status": "processing",
"steps": [
{
"done": false,
"key": "prepare"
},
{
"done": false,
"key": "separate"
},
{
"done": false,
"key": "convert"
},
{
"done": false,
"key": "package"
},
{
"done": false,
"key": "finalize"
}
],
"variants": [
{
"driftMs": null,
"hasResult": false,
"speakers": [],
"variantId": "r3v5a7r9i1a3n5t7x9y1z3w5",
"voiceName": "Ana"
},
{
"driftMs": null,
"hasResult": false,
"speakers": [],
"variantId": "r4v6a8r0i2a4n6t8x0y2z4w6",
"voiceName": "Liam"
}
],
"variantsTotal": 2
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}409Error codes:
- job_not_cancellable: The job is already done, failed or cancelled; there is nothing left to cancel.
{
"error": {
"code": "job_not_cancellable",
"message": "This job has already finished and cannot be cancelled.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "unavailable",
"message": "An error occurred on our side. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}GET/v1/jobs/{id}/download
Download a result
Redirects (302) to a presigned URL, valid for 900 seconds, of the packaged result: the video, its audio extract, one stem track or the narration. The file is the same marked object the web serves. A job without that result answers 404. variant may be omitted only for a job with a single variant, which is then used; a job with several variants requires it (400 invalid_request otherwise).
Permiso: jobs:readdownloadJob
Ruta
idObligatorioCuid2Resource id (cuid2). An id that is not a cuid2 answers 400invalid_request; one that does not exist or is not yours answers 404not_found.
Consulta
formatOpcionalstringValores admitidos"video""audio""stems""narration"Por defecto:"video"video(default), theaudioextract, onestemstrack, or thenarrationresult.stemOpcionalRequired withstringValores admitidos"vocals""background""voice"format=stems: which track.variantOpcionalstringPatrón:^[a-z][a-z0-9]{7,31}$Which variant to download when the job has several. Defaults to the first.
Cabeceras
Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Respuestas
302Redirect to the presigned download URL (Location).
Cabeceras:Location
400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- download_unavailable: The download link could not be prepared (our side). Retry.
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "download_unavailable",
"message": "We could not prepare the download. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
- not_configured: The environment is missing a piece of configuration (our side). Retry later.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}narrations
POST/v1/narrations
Create a narration
Narrates a script with one of your usable voices, as a job you then follow with GET /v1/jobs/{id} and download with format=narration. Same minutes, same estimate and same Idempotency-Key rules as a conversion.
Permiso: narrations:writeClave de idempotencia: requiredcreateNarration
Cabeceras
Idempotency-KeyObligatoriostringPatrón:^[\x21-\x7E]+$Longitud mínima:1Longitud máxima:255Client-chosen token (1–255 printable ASCII characters, no spaces; a UUID is ideal). The key is scoped to your account — every API key and OAuth client of yours shares one namespace. Replaying the same key with the same request (same operation, same body) returns the original result again for as long as the job it created exists: the key is bound to that job (for an upload, to its draft), so it is released only when the job is deleted or the abandoned draft is purged. The same key with a different request answers 409idempotency_conflict, and so does an upload key replayed after that upload was confirmed into a job (seePOST /v1/uploads). Required on this operation.Accept-LanguageOpcionalstringLanguage oferror.messageand of the voice labels (gender,accent,ageRange,language,useCase,description), negotiated as on the website;enwhen absent or unsupported. Codes, field names anderror.codenever vary with it.
Cuerpo de la petición
{
"name": "Spring narration",
"script": "This spring, refresh your home with twenty percent off the whole collection. Only until the end of the month.",
"voiceId": "v4n8k2m6p1q9s3t7w5x1y2z4"
}Respuestas
202The narration is queued.
JobCreatedResponse{
"data": {
"estimate": {
"currency": "usd",
"overageAmountCents": 0,
"overageSeconds": 0,
"planSeconds": 5
},
"job": {
"convertVariantsDone": 0,
"id": "n3a5r7r9a1t3i5o7n9j1o3b5",
"isNarration": true,
"isRemaster": false,
"paused": false,
"status": "queued",
"steps": [
{
"done": false,
"key": "prepare"
},
{
"done": false,
"key": "separate"
},
{
"done": false,
"key": "convert"
},
{
"done": false,
"key": "package"
},
{
"done": false,
"key": "finalize"
}
],
"variants": [
{
"driftMs": null,
"hasResult": false,
"speakers": [],
"variantId": "r5v7a9r1i3a5n7t9x1y3z5w7",
"voiceName": "Ana"
}
],
"variantsTotal": 1
}
}
}400Error codes:
- invalid_request: The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged.
- idempotency_key_required: The operation requires an Idempotency-Key header and none was sent.
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}401Error codes:
- unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.
Cabeceras:WWW-Authenticate
{
"error": {
"code": "unauthorized",
"message": "Provide a valid API key or access token.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}402Error codes:
- api_daily_overage_cap: This credential reached its daily overage cap (UTC day). Nothing was written; retry after midnight UTC or raise the cap in Settings.
{
"error": {
"code": "api_daily_overage_cap",
"message": "This credential reached its daily overage cap. Retry tomorrow or raise the cap in Settings.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}403Error codes:
- auth_email_not_verified: The account owning the credential has not confirmed its email address, so it cannot create jobs.
- api_access_required: The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does.
- insufficient_scope: The credential does not carry the scope this operation requires (x-scope).
{
"error": {
"code": "auth_email_not_verified",
"message": "Confirm your email before creating jobs.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}auth_email_not_verifiedapi_access_requiredinsufficient_scope
404Error codes:
- not_found: The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
{
"error": {
"code": "not_found",
"message": "We could not find that resource.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}409Error codes:
- idempotency_conflict: The Idempotency-Key was already used with a different request, or with an upload that has since been confirmed into a job. Use a new key.
- account_closing: The account requested its deletion; no new jobs can be created while it closes.
- subscription_paused: The plan is paused (a billing incident); no new jobs until billing resumes.
{
"error": {
"code": "idempotency_conflict",
"message": "This Idempotency-Key was already used with a different request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}422Error codes:
- invalid_script: The script is outside its length bounds after trimming, or carries no visible text.
- voice_not_usable: The voiceId cannot narrate (unknown, not yours, or not usable right now).
- overage_unavailable_currency: The job would need overage minutes and overage is billed in US dollars only, which is not the billing currency of this account. Stay within the plan allowance.
{
"error": {
"code": "invalid_script",
"message": "The script must be between 20 and 2500 characters.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}429Error codes:
- rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
Cabeceras:Retry-After
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Wait and retry after the indicated seconds.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}500Error codes:
- enqueue_failed: The job could not be handed to the queue (our side). Retry.
- unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
{
"error": {
"code": "enqueue_failed",
"message": "The job could not be queued. Retry the request.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}503Error codes:
- api_disabled: The public API is switched off for now. Retry later; nothing was revoked.
- narration_disabled: Narration is switched off for now. Retry later.
- voice_service_paused: Voice processing is temporarily paused. Retry later.
{
"error": {
"code": "api_disabled",
"message": "The API is temporarily switched off.",
"requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
}
}Errores posibles
rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundidempotency_key_requiredidempotency_conflictapi_daily_overage_capnarration_disabledinvalid_scriptvoice_not_usableoverage_unavailable_currencyvoice_service_pausedaccount_closingsubscription_pausedenqueue_failedunavailable
Catálogo de errores#
Todos los códigos que puede devolver la API, con el estado que llevan.
| Código | Estado | Descripción |
|---|---|---|
| account_closing | 409 | The account requested its deletion; no new jobs can be created while it closes. |
| api_access_required | 403 | The plan of the account does not include API access (a Pro or Agency right). Credentials start working again as soon as the plan does. |
| api_daily_overage_cap | 402 | This credential reached its daily overage cap (UTC day). Nothing was written; retry after midnight UTC or raise the cap in Settings. |
| api_disabled | 503 | The public API is switched off for now. Retry later; nothing was revoked. |
| auth_email_not_verified | 403 | The account owning the credential has not confirmed its email address, so it cannot create jobs. |
| download_unavailable | 500 | The download link could not be prepared (our side). Retry. |
| enqueue_failed | 500 | The job could not be handed to the queue (our side). Retry. |
| idempotency_conflict | 409 | The Idempotency-Key was already used with a different request, or with an upload that has since been confirmed into a job. Use a new key. |
| idempotency_key_required | 400 | The operation requires an Idempotency-Key header and none was sent. |
| insufficient_scope | 403 | The credential does not carry the scope this operation requires (x-scope). |
| invalid_cursor | 400 | The cursor is not one this list issued: malformed, or minted for another list. Restart from the first page. |
| invalid_request | 400 | The body, query, path or headers do not match the schema of the operation (a path id that is not a cuid2 included). Fix the request; do not retry it unchanged. |
| invalid_script | 422 | The script is outside its length bounds after trimming, or carries no visible text. |
| invalid_state | 409 | The job is not in a state that admits the operation. |
| invalid_tune | 422 | A voiceSettings value is outside the documented range. |
| invalid_voices | 422 | One or more of voiceIds, or of the voices in speakers, cannot be used for this job (unknown, not yours, retired or not usable right now). |
| job_not_cancellable | 409 | The job is already done, failed or cancelled; there is nothing left to cancel. |
| mcp_disabled | 503 | The MCP server is switched off for now. Retry later; nothing was revoked. |
| multi_voice_disabled | 403 | The several-voices feature is not available to this account right now: it is switched off for the moment, the plan does not include it, or the account has no open billing period at this instant (a renewal in progress — retry in a while). Nothing was written. A job without speakers still converts with one voice per variant. |
| narration_disabled | 503 | Narration is switched off for now. Retry later. |
| not_configured | 503 | The environment is missing a piece of configuration (our side). Retry later. |
| not_confirmable | 409 | The upload cannot be turned into a job: its draft is gone or was already confirmed. |
| not_found | 404 | The resource does not exist or is not yours. The two cases are deliberately indistinguishable. |
| overage_unavailable_currency | 422 | The job would need overage minutes and overage is billed in US dollars only, which is not the billing currency of this account. Stay within the plan allowance. |
| preflight_unavailable | 500 | The file could not be analysed (our side). Retry. |
| rate_limited | 429 | Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry. |
| rejected | 422 | The file was refused by the pre-flight: unsupported container, no video or audio track, too long or too large. error.reason says which — the closed ApiErrorReason enum — and message says the same thing in the negotiated language. |
| speakers_mismatch | 400 | The speakers of the request are malformed or do not fit: not a list of { variantVoiceId, assignments }, a voice id that is not a cuid2, more entries or assignments than allowed, a variantVoiceId that is not in voiceIds or is listed twice, a speaker listed twice, speaker 0 (it always keeps the voice of its variant), or a speaker the detection of the upload did not find. Fix the request; do not retry it unchanged. |
| speakers_not_ready | 409 | The upload has no finished speaker detection with several speakers: it was never requested, or it stopped without an outcome (request it with POST /v1/uploads/{id}/speakers — a stopped detection is retried once), it is still pending, or its outcome is single, too_many or failed. A job created from a previous job (sourceJobId) never has one: upload the video again. Otherwise create the job without speakers, or wait for ready. |
| subscription_paused | 409 | The plan is paused (a billing incident); no new jobs until billing resumes. |
| unauthorized | 401 | No usable credential: the API key or access token is missing, malformed, revoked or expired. |
| unavailable | 500 | An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists. |
| upload_expired | 409 | The resumable upload can no longer be resumed: its parts expired or it was discarded. Start the upload again. Reserved for the resumable upload, which is not on /v1 yet: no /v1 operation answers it today. |
| upload_not_ready | 409 | The upload is not ready for this step: on the pre-flight, the PUT has not completed; on a job, the upload has not been pre-flighted yet. |
| upload_unavailable | 500 | The upload could not be registered (our side). Retry. |
| voice_not_usable | 422 | The voiceId cannot narrate (unknown, not yours, or not usable right now). |
| voice_service_paused | 503 | Voice processing is temporarily paused. Retry later. |
Esquemas#
Las formas que viajan en los cuerpos y en las respuestas.
ApiErrorCode
message. Every code is explained in the x-error-catalog of this document and in each response that can answer it.stringValores admitidos"invalid_request""invalid_cursor""idempotency_key_required""speakers_mismatch""unauthorized""api_daily_overage_cap""auth_email_not_verified""api_access_required""insufficient_scope""multi_voice_disabled""not_found""idempotency_conflict""upload_not_ready""job_not_cancellable""not_confirmable""invalid_state""account_closing""subscription_paused""speakers_not_ready""upload_expired""rejected""invalid_voices""invalid_tune""invalid_script""voice_not_usable""overage_unavailable_currency""rate_limited""unavailable""upload_unavailable""preflight_unavailable""download_unavailable""enqueue_failed""api_disabled""mcp_disabled""narration_disabled""not_configured""voice_service_paused"ApiErrorReason
rejected answer: the pre-flight rule that refused the file. Only a rejected (422) carries one.stringValores admitidos"unsupported_container""no_video""no_audio""too_long""too_large"ApiScope
stringValores admitidos"voices:read""jobs:read""jobs:write""narrations:write""webhooks:manage"CreateJobRequest
nameOpcionalstringDisplay name (at most 120 characters). Empty or blank means no name.Longitud máxima:120sourceJobIdOpcionalstringA previous job of yours whose video is reused. Exactly one ofuploadId/sourceJobId.Patrón:^[a-z][a-z0-9]{7,31}$speakersOpcionalLista deVariantSpeakersOptional: another voice for some speakers of the video, per variant. Any assignment — even one that repeats the variant voice — needsuploadId, the several-voices feature and areadyspeaker detection of that upload (POST /v1/uploads/{id}/speakers); a request that cannot have them is refused, never converted with one voice instead. The minutes are the same as with one voice. Absent, empty, or with only emptyassignments, the job is converted with one voice per variant, exactly as without this field.Elementos máximos:3uploadIdOpcionalstringA pre-flighted upload (POST /v1/uploads/{id}/preflight). Exactly one ofuploadId/sourceJobId.Patrón:^[a-z][a-z0-9]{7,31}$voiceIdsObligatorioLista deCuid21 to 3 distinct voice ids; one variant is produced per voice.Elementos mínimos:1Elementos máximos:3Sin elementos repetidosvoiceSettingsOpcionalOptional tuning per voice id (only ids present invoiceIdsare used).Campos adicionalessimilarityObligatorionumberMínimo:0Máximo:1speakerBoostObligatoriobooleanstabilityObligatorionumberMínimo:0Máximo:1
uploadIdsourceJobIdCreateNarrationRequest
nameOpcionalstringDisplay name (at most 120 characters). Empty or blank means no name.Longitud máxima:120scriptObligatoriostringThe text to narrate: 20 to 2500 characters after trimming.Longitud mínima:20Longitud máxima:2500voiceIdObligatoriostringThe voice that narrates. Must be usable for narration.Patrón:^[a-z][a-z0-9]{7,31}$
CreateUploadRequest
constraints in the response).contentTypeObligatoriostringMIME type of the file (for examplevideo/mp4). The PUT must send it asContent-Type; the pre-flight decides on the real container.Longitud mínima:1Longitud máxima:200filenameObligatoriostringOriginal file name, used only to pick the container extension.Longitud mínima:1Longitud máxima:500sizeBytesObligatoriointegerSize of the file you are about to PUT, in bytes. Aboveconstraints.maxSizeBytes(see the response) the upload answers 422rejected.Mínimo:0
CreateUploadResponse
POST /v1/uploads/{uploadId}/preflight.dataObligatorioconstraintsObligatorioUploadConstraintsexpiresAtObligatoriostringWhenputUrlstops accepting the PUT — about an hour after this response. Start the upload right away: a single PUT must finish before then.Formato:date-timeputUrlObligatoriostringPresigned URL. PUT the raw bytes here with the sameContent-Type.Formato:uriuploadIdObligatorioCuid2
Cuid2
stringResource identifier (cuid2: 8 to 32 lowercase letters or digits, starting with a letter).Patrón: ^[a-z][a-z0-9]{7,31}$DetectedSpeaker
indexObligatoriointegerThe speaker number, in order of first appearance. Speaker 0 keeps the voice of its variant; assign voices to speakers 1 and up.Mínimo:0Máximo:3previewUrlObligatorioA clip of a few seconds of this speaker's own voice, to tell the speakers apart. Presigned for 900 seconds: fetch it without your key, do not store it, and read the detection again for a fresh one. Null when this speaker has no clip.Cualquiera destringFormato:urinull
segmentsObligatorioLista deSpeakerSegmentThe stretches in which this speaker talks, in order.totalMsObligatoriointegerHow long this speaker talks in the video, in milliseconds: the sum ofsegments.Mínimo:0
DownloadUrlResult
expiresAtObligatoriostringWhen the URL stops working: 900 seconds after it was issued.Formato:date-timeurlObligatoriostringPresigned GET URL of the result. The same object the 302 of the REST operation points at.Formato:uri
ErrorEnvelope
errorObligatoriocodeObligatorioApiErrorCodemessageObligatoriostringHuman-readable explanation in the negotiated language. Not stable; never branch on it.reasonOpcionalApiErrorReasonrequestIdObligatoriostringCorrelation id of this request. Quote it when contacting support.Longitud mínima:1Longitud máxima:128
JobCreatedResponse
dataObligatorioestimateObligatorioJobEstimatejobObligatorioJobDetail
JobDetail
GET /v1/jobs/{id} serves it.convertVariantsDoneObligatoriointegerVoices whose conversion has finished, out ofvariantsTotal.Mínimo:0idObligatorioCuid2isNarrationObligatoriobooleanTrue for a narration: no video, and the conversion checklist does not apply.isRemasterObligatoriobooleanTrue for a remaster (an audio re-edit of a previous conversion): theseparateandconvertsteps do not run.pausedObligatoriobooleanProcessing is temporarily parked; it resumes on its own.statusObligatorioJobStatusstepsObligatorioLista deJobStepThe five-step checklist of a conversion, in order. For a narration (isNarration) or a remaster (isRemaster) the steps that do not apply never turndone; rely onstatus.variantsObligatorioLista deJobVariantvariantsTotalObligatoriointegerNumber of voices applied (theconvertstep counts up to it).Mínimo:0
JobEstimate
currencyObligatorioValor fijo"usd"ISO 4217 code, lowercase, ofoverageAmountCents. Overage is billed in US dollars only; an account billed in another currency cannot go beyond its allowance (422overage_unavailable_currency).overageAmountCentsObligatoriointegerEstimated overage amount in whole cents ofcurrency.Mínimo:0overageSecondsObligatoriointegerSeconds beyond the allowance.Mínimo:0planSecondsObligatoriointegerSeconds charged to the plan allowance.Mínimo:0
JobListResponse
dataObligatorioLista deLibraryItemnextCursorObligatorioOpaque cursor of the next page, or null when this page was the last one.Cualquiera destringnull
JobResponse
dataObligatorioJobDetail
JobStatus
confirmed → queued → processing → done | failed | cancelled (the last three are terminal). An upload that has not been confirmed is not a job and never appears here.stringValores admitidos"confirmed""queued""processing""done""failed""cancelled"JobStep
done: false even when the job is done — read status for completion and steps only for the progress of a conversion.doneObligatoriobooleankeyObligatoriostringValores admitidos"prepare""separate""convert""package""finalize"
JobVariant
driftMsObligatorioMeasured timing drift in whole milliseconds, or null until measured.Cualquiera deintegernull
hasResultObligatoriobooleanWhether the packaged result exists and can be downloaded.speakersObligatorioLista deJobVariantSpeakerThe speakers of this variant converted with a voice other than the variant voice (speakersofPOST /v1/jobs), ordered byindex. Empty when the whole video takes the variant voice.variantIdObligatorioCuid2voiceNameObligatorioName of the voice applied, or null when that voice no longer exists.Cualquiera destringnull
JobVariantSpeaker
index, from the detection of its upload) converted with a voice (voiceId) other than the variant voice.indexObligatoriointegerMínimo:1Máximo:3voiceIdObligatorioCuid2
LibraryItem
GET /v1/jobs.createdAtObligatoriostringFormato:date-timedurationSecondsObligatorioCualquiera deintegernull
folderIdObligatorioCualquiera de- Cuid2
null
idObligatorioCuid2nameObligatoriostringnarrationObligatoriobooleanpausedObligatoriobooleanstatusObligatorioJobStatustagsObligatorioLista deTagunnamedObligatoriobooleanTrue whennameis the product fallback because the job has no name.variantCountObligatoriointegerMínimo:0
McpErrorData
error.data of an MCP (JSON-RPC) error: the stable API code, the same correlation id as REST, and the same optional reason.codeObligatorioApiErrorCodereasonOpcionalApiErrorReasonrequestIdObligatoriostringCorrelation id of this request. Quote it when contacting support.Longitud mínima:1Longitud máxima:128
OwnVoice
GET /v1/voices?kind=own.idObligatorioCuid2kindObligatorioValor fijo"own"nameObligatoriostringThe name the holder gave this voice.statusObligatorioactive— usable right now;recycled— kept, and rebuilt automatically the next time you use it.stringValores admitidos"active""recycled"
OwnVoiceDetail
GET /v1/voices/{id}. sample is null when this voice has no clip yet.idObligatorioCuid2kindObligatorioValor fijo"own"nameObligatoriostringThe name the holder gave this voice.sampleObligatorioCualquiera de- VoiceSample
null
statusObligatorioactive— usable right now;recycled— kept, and rebuilt automatically the next time you use it.stringValores admitidos"active""recycled"
PreflightReport
audioCodecObligatorioAudio codec name as probed (for exampleaac), or null.Cualquiera destringnull
audioDurationSObligatorioMeasured duration of the audio track, in seconds (not rounded). The minutes a job reserves round this up.Cualquiera denumbernull
containerObligatorioContainer family as the probe reports it — for MP4/MOV files the ISOBMFF family stringmov,mp4,m4a,3gp,3g2,mj2. The accepted extensions areconstraints.containersof the upload.Cualquiera destringnull
containerDurationSObligatorioDuration declared by the container, in seconds (not rounded).Cualquiera denumbernull
hasAudioObligatoriobooleanWhether the file has an audio track.remuxSafeObligatoriobooleanWhether the container can be rewritten without re-encoding the video.vfrSuspectedObligatoriobooleanWhether the video looks variable-frame-rate.videoCodecObligatorioVideo codec name as probed (for exampleh264), or null.Cualquiera destringnull
PreflightResponse
dataObligatoriopreflightObligatorioPreflightReportreadyObligatorioValor fijotrueAlways true when the pre-flight accepted the file; a refused file answers 422 instead.uploadIdObligatorioCuid2
SpeakerAssignment
speakerObligatoriointegerThe speaker (itsindexin the detection) that takesvoiceId. Speaker 0 cannot be assigned: it always keeps the voice of the variant.Mínimo:1Máximo:3voiceIdObligatoriostringThe voice for this speaker: a voice of the bank or one of your own, under the same rules asvoiceIds.Patrón:^[a-z][a-z0-9]{7,31}$
SpeakerDetection
ready, who talks when.speakersObligatorioLista deDetectedSpeakerThe speakers, ordered byindex— only whenstatusisready(2 to 4 of them); empty otherwise.Elementos máximos:4statusObligatorioSpeakerDetectionStatusuploadIdObligatorioCuid2
SpeakerDetectionResponse
dataObligatorioSpeakerDetection
SpeakerDetectionStatus
pending: running, ask again (if it is still pending after 15 minutes, convert with one voice). ready: 2 to 4 speakers were found and speakers lists them — you can assign a voice to each one in POST /v1/jobs. single: one speaker (or no speech) — convert with one voice. too_many: more than 4 speakers — convert with one voice. failed: the detection could not finish — convert with one voice. Every status but pending is final.stringValores admitidos"pending""ready""single""too_many""failed"SpeakerSegment
endMsObligatoriointegerWhere it ends, in milliseconds from the start of the video.Mínimo:0startMsObligatoriointegerWhere the stretch starts, in milliseconds from the start of the video.Mínimo:0
Tag
idObligatorioCuid2nameObligatoriostring
UploadConstraints
containersObligatorioLista deAccepted containers.stringValores admitidos"mp4""mov"maxDurationSecondsObligatorioValor fijo180Maximum video duration in seconds.maxSizeBytesObligatorioValor fijo524288000Maximum object size in bytes.
VariantSpeakers
assignmentsObligatorioLista deSpeakerAssignmentOne entry per speaker that takes another voice. A speaker left out keeps the variant voice, and so does one assigned the variant voice.Elementos máximos:3variantVoiceIdObligatoriostringThe variant this assignment belongs to, named by its voice: one ofvoiceIds. That voice is speaker 0 of the variant.Patrón:^[a-z][a-z0-9]{7,31}$
Voice
GET /v1/voices. Fetch GET /v1/voices/{id} for a playable sample.accentObligatorioLabel ofaccentCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
accentCodeObligatorioStable accent code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
ageCodeObligatorioStable age code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
ageRangeObligatorioLabel ofageCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
descriptionObligatoriostringOne-paragraph description of the voice, in the negotiated language.genderObligatorioLabel ofgenderCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
genderCodeObligatorioStable gender code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
idObligatorioCuid2kindObligatorioValor fijo"bank"languageObligatorioLabel oflanguageCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
languageCodeObligatorioStable language code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
nameObligatoriostringDisplay name of the voice.sampleTextObligatoriostringWhat the sample clip says — in the language of the voice itself, not localised.useCaseObligatorioLabel ofuseCaseCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
useCaseCodeObligatorioStable use-case code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
VoiceDetail
GET /v1/voices/{id}.accentObligatorioLabel ofaccentCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
accentCodeObligatorioStable accent code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
ageCodeObligatorioStable age code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
ageRangeObligatorioLabel ofageCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
descriptionObligatoriostringOne-paragraph description of the voice, in the negotiated language.genderObligatorioLabel ofgenderCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
genderCodeObligatorioStable gender code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
idObligatorioCuid2kindObligatorioValor fijo"bank"languageObligatorioLabel oflanguageCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
languageCodeObligatorioStable language code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
nameObligatoriostringDisplay name of the voice.sampleObligatorioVoiceSamplesampleTextObligatoriostringWhat the sample clip says — in the language of the voice itself, not localised.useCaseObligatorioLabel ofuseCaseCodein the negotiated language (Accept-Language,enby default), or null when unclassified. Copy, not a filter value.Cualquiera destringnull
useCaseCodeObligatorioStable use-case code — one of the valuesGET /v1/voices/facetsserves, usable as aGET /v1/voicesfilter — or null when unclassified.Cualquiera destringnull
VoiceFacets
GET /v1/voices filters.accentObligatorioLista destringValores admitidos"american""andalusian""argentine""australian""austrian""bavarian""belgian""brazilian""british""canadian""castilian""catalan""chilean""colombian""cuban""european""filipino""french""galician""german""indian""irish""italian""jamaican""latin-american""mexican""neutral""new-zealand""nigerian""parisian""peruvian""puerto-rican""quebecois""scottish""singaporean""south-african""spanish""standard""swedish""swiss""transatlantic""tuscan""us-southern""venezuelan""welsh"ageObligatorioLista destringValores admitidos"young""adult""senior"genderObligatorioLista destringValores admitidos"female""male""neutral"languageObligatorioLista destringValores admitidos"ar""de""en""es""fr""hi""it""ja""ko""nl""pl""pt""ro""sv""tr""zh"useCaseObligatorioLista destringValores admitidos"advertisement""asmr""audiobook""characters""conversational""educational""entertainment""meditation""narration""news""social-media"
VoiceFacetsResponse
dataObligatorioVoiceFacets
VoiceListResponse
VoiceResponse
kind says which of the two shapes it is.dataObligatorio
VoiceSample
expiresAtObligatoriostringUntil when the URL is guaranteed to work.Formato:date-timeurlObligatoriostringURL of the sample clip (audio/mpeg). Do not store it: it is valid untilexpiresAt, then fetch the voice again for a fresh one.Formato:uri
WebhookEvent
job.completed carries status: done and a download link; job.failed carries status: failed or cancelled (a cancelled job emits job.failed with status: cancelled) and no download. Verify the MIY-Signature header before trusting it.Una decreatedAtObligatoriostringFormato:date-timedataObligatoriojobIdObligatorioCuid2kindObligatorioconversion(a remaster of a conversion included) ornarration.stringValores admitidos"conversion""narration"linksObligatoriodownloadObligatoriostringGET /v1/jobs/{id}/downloadof the result.Formato:urijobObligatoriostringGET /v1/jobs/{id}of this job.Formato:uri
statusObligatorioValor fijo"done"
idObligatoriostringDelivery-independent event id. Deduplicate on it.Patrón:^[a-z][a-z0-9]{7,31}$typeObligatorioValor fijo"job.completed"
createdAtObligatoriostringFormato:date-timedataObligatoriojobIdObligatorioCuid2kindObligatorioconversion(a remaster of a conversion included) ornarration.stringValores admitidos"conversion""narration"linksObligatoriodownloadObligatorionullAlways null: a failed or cancelled job has no result.jobObligatoriostringGET /v1/jobs/{id}of this job.Formato:uri
statusObligatoriostringValores admitidos"failed""cancelled"
idObligatoriostringDelivery-independent event id. Deduplicate on it.Patrón:^[a-z][a-z0-9]{7,31}$typeObligatorioValor fijo"job.failed"