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, as Authorization: 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.

Ir a Ajustes → API

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 concederlevoices:readjobs:readjobs:writenarrations:write
Longitud máxima del nombre, en caracteres64
Caducidad, opcional, en días1–365
Tope diario de excedente de la clave, opcional, en minutos1–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.

No hay entorno de pruebas ni claves de prueba: cada llamada va contra tu cuenta real y cada trabajo gasta minutos de verdad. Prueba el flujo primero con un fichero corto.

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.

Términos del servicioPolítica de privacidad

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 origen300
Peticiones por minuto con una misma credencial120
Tope diario de excedente por credencial, salvo que la clave fije uno menor30 minutos
Cuándo vuelve a empezar el tope diarioa 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.

Usa la misma clave para la subida y para el trabajo que la confirma: son dos pasos de una sola intención, y compartir la clave es lo que impide que un reintento del primer paso acabe creando un segundo trabajo. Reutilizarla después de haber confirmado sí es un conflicto, y se te dice con un código propio.

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.

Ver la lista de cambios

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.

  1. Paso 1createUpload

    Registra la subida y recibe la dirección temporal a la que subir el fichero, con sus límites y su caducidad.

    bash
    curl -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ón
    json
    {
      "filename": "spring-spot.mp4",
      "sizeBytes": 48213907,
      "contentType": "video/mp4"
    }
    Respuesta · 201
    json
    {
      "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"
          ]
        }
      }
    }
  2. 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.

    bash
    curl -X PUT 'https://media.makeityours.app/media/u1s3e5r7a9b1c3d5e7f9g1h3/u8p2l4o6a1d3i5n7g9s2t4r6/source.mp4?signature=example' \
      -H 'Content-Type: video/mp4' \
      --upload-file 'spring-spot.mp4'
  3. Paso 3runUploadPreflight

    Pide la comprobación previa del fichero subido. Es la que mide la duración y decide si se puede convertir.

    bash
    curl -X POST 'https://www.makeityours.app/api/v1/uploads/u8p2l4o6a1d3i5n7g9s2t4r6/preflight' \
      -H 'x-api-key: miy_…'
    Respuesta · 200
    json
    {
      "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
        }
      }
    }
  4. 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.

    bash
    curl -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ón
    json
    {
      "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6",
      "voiceIds": [
        "v4n8k2m6p1q9s3t7w5x1y2z4",
        "l1a3m5v7o9i2c4e6b8d0f2h4"
      ],
      "name": "Spring spot — Ana and Liam",
      "voiceSettings": {
        "v4n8k2m6p1q9s3t7w5x1y2z4": {
          "similarity": 0.8,
          "stability": 0.45,
          "speakerBoost": true
        }
      }
    }
    Respuesta · 202
    json
    {
      "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"
        }
      }
    }
  5. 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.

    bash
    curl -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"
        }
      }
    }'
    http
    t=1789293852,v1=<hex HMAC-SHA256 of "t.body" with your endpoint secret>
    Petición
    json
    {
      "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"
        }
      }
    }
  6. Paso 6getJob

    Consulta el trabajo cuando quieras saber cómo va o qué variantes ha producido.

    bash
    curl -X GET 'https://www.makeityours.app/api/v1/jobs/j2b4d6f8h0k1m3p5r7t9v1x3' \
      -H 'x-api-key: miy_…'
    Respuesta · 200
    json
    {
      "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
      }
    }
  7. 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.

    bash
    curl -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 clave
    bash
    curl -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.

javascriptconvert.js
// 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);
});
pythonconvert.py
"""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.

  1. confirmed
  2. queued
  3. processing
  4. done(final)
  5. failed(final)
  6. 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.

  1. 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.
  2. 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.
  3. 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:

  1. pending
  2. ready(final)
  3. single(final)
  4. too_many(final)
  5. 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.

jsonDetección lista
{
  "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"
  }
}
jsonConversión con otra voz para un hablante
{
  "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 descarga no devuelve el fichero: devuelve una redirección a una dirección temporal desde la que bajarlo. Esa dirección es de otro servidor y ya lleva su propia autorización: bájalo desde ahí SIN reenviar tu clave (muchos clientes, curl incluido, reenvían las cabeceras personalizadas al seguir una redirección) y no la guardes, que caduca en minutos.

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.

Ir a Ajustes → Webhooks

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>.
json
{
  "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>.
json
{
  "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

Cada entrega lleva una cabecera de firma con una marca de tiempo y una suma calculada con el secreto de tu endpoint sobre la marca de tiempo, un punto y el cuerpo EXACTO que has recibido. Calcula la tuya sobre los bytes tal cual llegan, sin volver a serializar el JSON, y compárala en tiempo constante. Rechaza la entrega si la marca de tiempo se sale de la tolerancia, aunque la suma cuadre.

La cabecera puede traer más de una suma mientras rotas el secreto: acepta la entrega si cualquiera de ellas cuadra.

javascript
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 };
python
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_SECONDS

Reintentos

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.

IntentoEspera antes del siguiente
11 minuto
25 minutos
315 minutos
41 hora
54 horas
612 horas
724 horas
8Ninguno

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.

textDirección del servidor
https://www.makeityours.app/api/mcp
textMetadatos del recurso protegido
https://www.makeityours.app/.well-known/oauth-protected-resource
Permisos que se pueden pedir

voices: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

  1. Abre los ajustes del asistente y busca la sección de conectores o servidores externos.
  2. Elige añadir un servidor remoto (no uno local ni un comando).
  3. Pega la dirección del servidor que aparece arriba.
  4. Acepta cuando te lleve al navegador a iniciar sesión y autorizar.
  5. 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:

bash
claude mcp add --transport http makeityours https://www.makeityours.app/api/mcp

Cursor

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:

json.cursor/mcp.json
{
  "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
Errores posibles

rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestinvalid_cursorunavailable

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
Errores posibles

rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundunavailable

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
Errores posibles

rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestinvalid_cursorunavailable

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
Errores posibles

rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundunavailable

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
Errores posibles

rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestnot_foundnot_configureddownload_unavailableunavailable

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
Errores posibles

rate_limitedapi_disabledunauthorizedauth_email_not_verifiedapi_access_requiredinsufficient_scopeinvalid_requestidempotency_conflictrejectednot_configuredupload_unavailableunavailable

Referencia#

Generada del contrato: lo que hay aquí es exactamente lo que la API valida y responde.

Versión: 1.1.0Dirección base: https://www.makeityours.app/api

voices:readjobs:readjobs:writenarrations:writewebhooks:manage

  • webhooks:manage — reservado: ninguna operación ni tool lo exige todavía.
El documento del que se genera esta página se sirve también como fichero, sin credencial, para que generes un cliente tipado o lo cargues en tu propio visor. Descargar el documento OpenAPI https://www.makeityours.app/api/openapi.json

voices

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
  • kindOpcional
    stringValores admitidos"bank""own"Por defecto: "bank"
    bank (default) lists the shared bank; own lists your own clones (complete, unpaginated).
  • languageOpcional
    stringValores admitidos"ar""de""en""es""fr""hi""it""ja""ko""nl""pl""pt""ro""sv""tr""zh"
  • accentOpcional
    stringValores 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"
  • ageOpcional
    stringValores admitidos"young""adult""senior"
  • genderOpcional
    stringValores admitidos"female""male""neutral"
  • useCaseOpcional
    stringValores 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, from nextCursor. A cursor of another list answers 400 invalid_cursor.
  • limitOpcionalintegerPor defecto: 20Mínimo: 1Máximo: 100Page size, 1–100 (default 20).
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200One page of voices.

VoiceListResponse

Ejemplo: bank — The shared bank (the default): one page, nextCursor to continue.

json
{
  "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.

json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_requestinvalid_cursor

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200The served filter values.

VoiceFacetsResponse
json
{
  "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

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200The voice with its sample.

VoiceResponse

Ejemplo: bank — A voice of the shared bank, with its short-lived sample clip.

json
{
  "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.

json
{
  "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.

json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 409 idempotency_conflict, and so does an upload key replayed after that upload was confirmed into a job (see POST /v1/uploads). Optional on this operation.
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Cuerpo de la petición
json
{
  "contentType": "video/mp4",
  "filename": "spring-spot.mp4",
  "sizeBytes": 48213907
}
Respuestas

201The upload is registered; PUT the file to putUrl.

CreateUploadResponse
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was already used with a different request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

idempotency_conflict

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.

ErrorEnvelope
json
{
  "error": {
    "code": "rejected",
    "message": "The file was refused: check the format, duration, size and audio track.",
    "reason": "no_audio",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rejected

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

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.

ErrorEnvelope
json
{
  "error": {
    "code": "upload_unavailable",
    "message": "We could not prepare the upload. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

upload_unavailableunavailable

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disablednot_configured

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200The file is analysed and ready to convert.

PreflightResponse
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

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.

ErrorEnvelope
json
{
  "error": {
    "code": "upload_not_ready",
    "message": "The upload has not been analysed yet. Run the pre-flight first.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

upload_not_ready

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.

ErrorEnvelope
json
{
  "error": {
    "code": "rejected",
    "message": "The file was refused: check the format, duration, size and audio track.",
    "reason": "no_audio",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rejected

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

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.

ErrorEnvelope
json
{
  "error": {
    "code": "preflight_unavailable",
    "message": "We could not analyse the file. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

preflight_unavailableunavailable

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disablednot_configured

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200The detection as it stands now.

SpeakerDetectionResponse

Ejemplo: pending — Still running: ask again.

json
{
  "data": {
    "speakers": [],
    "status": "pending",
    "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
  }
}

Ejemplo: ready — Two speakers found: assign a voice to speaker 1 in POST /v1/jobs.

json
{
  "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.

json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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.

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

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.

ErrorEnvelope
json
{
  "error": {
    "code": "upload_not_ready",
    "message": "The upload has not been analysed yet. Run the pre-flight first.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

upload_not_readynot_confirmablespeakers_not_ready

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

202The detection is requested, or already has an outcome; its state as it stands now.

SpeakerDetectionResponse

Ejemplo: pending — Requested: the detection runs in the background.

json
{
  "data": {
    "speakers": [],
    "status": "pending",
    "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6"
  }
}

Ejemplo: ready — Already detected: calling again changes nothing.

json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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.

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

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.

ErrorEnvelope
json
{
  "error": {
    "code": "upload_not_ready",
    "message": "The upload has not been analysed yet. Run the pre-flight first.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

upload_not_readynot_confirmable

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

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.

ErrorEnvelope
json
{
  "error": {
    "code": "enqueue_failed",
    "message": "The job could not be queued. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

enqueue_failedunavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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, from nextCursor. A cursor of another list answers 400 invalid_cursor.
  • limitOpcionalintegerPor defecto: 20Mínimo: 1Máximo: 100Page size, 1–100 (default 20).
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200One page of jobs.

JobListResponse
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_requestinvalid_cursor

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 409 idempotency_conflict, and so does an upload key replayed after that upload was confirmed into a job (see POST /v1/uploads). Required on this operation.
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Cuerpo de la petición

Ejemplo: oneVoice — One voice per variant (the usual request).

json
{
  "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.

json
{
  "name": "Spring spot — two characters",
  "speakers": [
    {
      "assignments": [
        {
          "speaker": 1,
          "voiceId": "l1a3m5v7o9i2c4e6b8d0f2h4"
        }
      ],
      "variantVoiceId": "v4n8k2m6p1q9s3t7w5x1y2z4"
    }
  ],
  "uploadId": "u8p2l4o6a1d3i5n7g9s2t4r6",
  "voiceIds": [
    "v4n8k2m6p1q9s3t7w5x1y2z4"
  ]
}
Respuestas

202The conversion is queued.

JobCreatedResponse

Ejemplo: oneVoice — Two variants, one voice each.

json
{
  "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.

json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_requestidempotency_key_requiredspeakers_mismatch

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_daily_overage_cap",
    "message": "This credential reached its daily overage cap. Retry tomorrow or raise the cap in Settings.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_daily_overage_cap

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.

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

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.

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_voices",
    "message": "One or more voices cannot be used for this job.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_voicesinvalid_tuneoverage_unavailable_currency

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

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.

ErrorEnvelope
json
{
  "error": {
    "code": "enqueue_failed",
    "message": "The job could not be queued. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

enqueue_failedunavailable

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabledvoice_service_paused

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

200The job.

JobResponse
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Cabeceras
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Respuestas

202The cancellation is queued; the job as it stands now.

JobResponse
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

409Error codes: - job_not_cancellable: The job is already done, failed or cancelled; there is nothing left to cancel.

ErrorEnvelope
json
{
  "error": {
    "code": "job_not_cancellable",
    "message": "This job has already finished and cannot be cancelled.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

job_not_cancellable

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

500Error codes: - unavailable: An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.

ErrorEnvelope
json
{
  "error": {
    "code": "unavailable",
    "message": "An error occurred on our side. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unavailable

503Error codes: - api_disabled: The public API is switched off for now. Retry later; nothing was revoked.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disabled

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 400 invalid_request; one that does not exist or is not yours answers 404 not_found.
Consulta
  • formatOpcional
    stringValores admitidos"video""audio""stems""narration"Por defecto: "video"
    video (default), the audio extract, one stems track, or the narration result.
  • stemOpcional
    stringValores admitidos"vocals""background""voice"
    Required with 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 of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never 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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_request

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

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.

ErrorEnvelope
json
{
  "error": {
    "code": "download_unavailable",
    "message": "We could not prepare the download. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

download_unavailableunavailable

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disablednot_configured

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 409 idempotency_conflict, and so does an upload key replayed after that upload was confirmed into a job (see POST /v1/uploads). Required on this operation.
  • Accept-LanguageOpcionalstringLanguage of error.message and of the voice labels (gender, accent, ageRange, language, useCase, description), negotiated as on the website; en when absent or unsupported. Codes, field names and error.code never vary with it.
Cuerpo de la petición
json
{
  "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
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_requestidempotency_key_required

401Error codes: - unauthorized: No usable credential: the API key or access token is missing, malformed, revoked or expired.

Cabeceras:WWW-Authenticate

ErrorEnvelope
json
{
  "error": {
    "code": "unauthorized",
    "message": "Provide a valid API key or access token.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

unauthorized

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_daily_overage_cap",
    "message": "This credential reached its daily overage cap. Retry tomorrow or raise the cap in Settings.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_daily_overage_cap

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

ErrorEnvelope
json
{
  "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.

ErrorEnvelope
json
{
  "error": {
    "code": "not_found",
    "message": "We could not find that resource.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

not_found

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.

ErrorEnvelope
json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was already used with a different request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

idempotency_conflictaccount_closingsubscription_paused

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.

ErrorEnvelope
json
{
  "error": {
    "code": "invalid_script",
    "message": "The script must be between 20 and 2500 characters.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

invalid_scriptvoice_not_usableoverage_unavailable_currency

429Error codes: - rate_limited: Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.

Cabeceras:Retry-After

ErrorEnvelope
json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Wait and retry after the indicated seconds.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

rate_limited

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.

ErrorEnvelope
json
{
  "error": {
    "code": "enqueue_failed",
    "message": "The job could not be queued. Retry the request.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

enqueue_failedunavailable

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.

ErrorEnvelope
json
{
  "error": {
    "code": "api_disabled",
    "message": "The API is temporarily switched off.",
    "requestId": "r2q4u6e8s1t3i5d7a9b1c3d5"
  }
}

api_disablednarration_disabledvoice_service_paused

Catálogo de errores#

Todos los códigos que puede devolver la API, con el estado que llevan.

CódigoEstadoDescripción
account_closing409The account requested its deletion; no new jobs can be created while it closes.
api_access_required403The 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_cap402This credential reached its daily overage cap (UTC day). Nothing was written; retry after midnight UTC or raise the cap in Settings.
api_disabled503The public API is switched off for now. Retry later; nothing was revoked.
auth_email_not_verified403The account owning the credential has not confirmed its email address, so it cannot create jobs.
download_unavailable500The download link could not be prepared (our side). Retry.
enqueue_failed500The job could not be handed to the queue (our side). Retry.
idempotency_conflict409The 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_required400The operation requires an Idempotency-Key header and none was sent.
insufficient_scope403The credential does not carry the scope this operation requires (x-scope).
invalid_cursor400The cursor is not one this list issued: malformed, or minted for another list. Restart from the first page.
invalid_request400The 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_script422The script is outside its length bounds after trimming, or carries no visible text.
invalid_state409The job is not in a state that admits the operation.
invalid_tune422A voiceSettings value is outside the documented range.
invalid_voices422One 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_cancellable409The job is already done, failed or cancelled; there is nothing left to cancel.
mcp_disabled503The MCP server is switched off for now. Retry later; nothing was revoked.
multi_voice_disabled403The 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_disabled503Narration is switched off for now. Retry later.
not_configured503The environment is missing a piece of configuration (our side). Retry later.
not_confirmable409The upload cannot be turned into a job: its draft is gone or was already confirmed.
not_found404The resource does not exist or is not yours. The two cases are deliberately indistinguishable.
overage_unavailable_currency422The 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_unavailable500The file could not be analysed (our side). Retry.
rate_limited429Too many requests from this IP or credential. Wait the seconds in Retry-After, then retry.
rejected422The 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_mismatch400The 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_ready409The 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_paused409The plan is paused (a billing incident); no new jobs until billing resumes.
unauthorized401No usable credential: the API key or access token is missing, malformed, revoked or expired.
unavailable500An unexpected failure on our side. Retry with backoff; quote requestId to support if it persists.
upload_expired409The 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_ready409The 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_unavailable500The upload could not be registered (our side). Retry.
voice_not_usable422The voiceId cannot narrate (unknown, not yours, or not usable right now).
voice_service_paused503Voice processing is temporarily paused. Retry later.

Esquemas#

Las formas que viajan en los cuerpos y en las respuestas.

ApiErrorCode

Stable, documented error code. Branch on this, never on 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

Machine-readable detail of a 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

A scope an API key or OAuth authorisation can carry.stringValores admitidos"voices:read""jobs:read""jobs:write""narrations:write""webhooks:manage"

CreateJobRequest

Create a conversion: the video source, the voices to apply, optional tuning and, optionally, a voice per speaker.
  • nameOpcionalstringDisplay name (at most 120 characters). Empty or blank means no name.Longitud máxima: 120
  • sourceJobIdOpcionalstringA previous job of yours whose video is reused. Exactly one of uploadId / sourceJobId.Patrón: ^[a-z][a-z0-9]{7,31}$
  • speakersOpcional
    Optional: another voice for some speakers of the video, per variant. Any assignment — even one that repeats the variant voice — needs uploadId, the several-voices feature and a ready speaker 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 empty assignments, the job is converted with one voice per variant, exactly as without this field.Elementos máximos: 3
  • uploadIdOpcionalstringA pre-flighted upload (POST /v1/uploads/{id}/preflight). Exactly one of uploadId / sourceJobId.Patrón: ^[a-z][a-z0-9]{7,31}$
  • voiceIdsObligatorio
    Lista deCuid2
    1 to 3 distinct voice ids; one variant is produced per voice.Elementos mínimos: 1Elementos máximos: 3Sin elementos repetidos
  • voiceSettingsOpcional
    Optional tuning per voice id (only ids present in voiceIds are used).
    Campos adicionales
    • similarityObligatorionumberMínimo: 0Máximo: 1
    • speakerBoostObligatorioboolean
    • stabilityObligatorionumberMínimo: 0Máximo: 1
Exactamente uno de estos grupos de camposuploadIdsourceJobId

CreateNarrationRequest

Create a narration: a voice and a script.
  • nameOpcionalstringDisplay name (at most 120 characters). Empty or blank means no name.Longitud máxima: 120
  • scriptObligatoriostringThe text to narrate: 20 to 2500 characters after trimming.Longitud mínima: 20Longitud máxima: 2500
  • voiceIdObligatoriostringThe voice that narrates. Must be usable for narration.Patrón: ^[a-z][a-z0-9]{7,31}$

CreateUploadRequest

What you are about to upload. The product limits apply (see constraints in the response).
  • contentTypeObligatoriostringMIME type of the file (for example video/mp4). The PUT must send it as Content-Type; the pre-flight decides on the real container.Longitud mínima: 1Longitud máxima: 200
  • filenameObligatoriostringOriginal file name, used only to pick the container extension.Longitud mínima: 1Longitud máxima: 500
  • sizeBytesObligatoriointegerSize of the file you are about to PUT, in bytes. Above constraints.maxSizeBytes (see the response) the upload answers 422 rejected.Mínimo: 0

CreateUploadResponse

Where to PUT the file. Then call POST /v1/uploads/{uploadId}/preflight.
  • dataObligatorio
    • constraintsObligatorioUploadConstraints
    • expiresAtObligatoriostringWhen putUrl stops accepting the PUT — about an hour after this response. Start the upload right away: a single PUT must finish before then.Formato: date-time
    • putUrlObligatoriostringPresigned URL. PUT the raw bytes here with the same Content-Type.Formato: uri
    • uploadIdObligatorioCuid2

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

One speaker found in the upload.
  • 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: 3
  • previewUrlObligatorio
    A 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 de
    • stringFormato: uri
    • null
  • segmentsObligatorio
    The stretches in which this speaker talks, in order.
  • totalMsObligatoriointegerHow long this speaker talks in the video, in milliseconds: the sum of segments.Mínimo: 0

DownloadUrlResult

A short-lived download URL and its expiry — the MCP answer to a download (no redirect).
  • expiresAtObligatoriostringWhen the URL stops working: 900 seconds after it was issued.Formato: date-time
  • urlObligatoriostringPresigned GET URL of the result. The same object the 302 of the REST operation points at.Formato: uri

ErrorEnvelope

The single error shape of the API. Every non-2xx JSON response has it.
  • errorObligatorio
    • codeObligatorioApiErrorCode
    • messageObligatoriostringHuman-readable explanation in the negotiated language. Not stable; never branch on it.
    • reasonOpcionalApiErrorReason
    • requestIdObligatoriostringCorrelation id of this request. Quote it when contacting support.Longitud mínima: 1Longitud máxima: 128

JobCreatedResponse

The job was accepted and queued, with its estimate.

JobDetail

The live state of one job, as GET /v1/jobs/{id} serves it.
  • convertVariantsDoneObligatoriointegerVoices whose conversion has finished, out of variantsTotal.Mínimo: 0
  • idObligatorioCuid2
  • isNarrationObligatoriobooleanTrue for a narration: no video, and the conversion checklist does not apply.
  • isRemasterObligatoriobooleanTrue for a remaster (an audio re-edit of a previous conversion): the separate and convert steps do not run.
  • pausedObligatoriobooleanProcessing is temporarily parked; it resumes on its own.
  • statusObligatorioJobStatus
  • stepsObligatorio
    Lista deJobStep
    The five-step checklist of a conversion, in order. For a narration (isNarration) or a remaster (isRemaster) the steps that do not apply never turn done; rely on status.
  • variantsObligatorio
    Lista deJobVariant
  • variantsTotalObligatoriointegerNumber of voices applied (the convert step counts up to it).Mínimo: 0

JobEstimate

The estimated plan/overage split of a job. Settled against the real duration when the job finishes.
  • currencyObligatorioValor fijo"usd"ISO 4217 code, lowercase, of overageAmountCents. Overage is billed in US dollars only; an account billed in another currency cannot go beyond its allowance (422 overage_unavailable_currency).
  • overageAmountCentsObligatoriointegerEstimated overage amount in whole cents of currency.Mínimo: 0
  • overageSecondsObligatoriointegerSeconds beyond the allowance.Mínimo: 0
  • planSecondsObligatoriointegerSeconds charged to the plan allowance.Mínimo: 0

JobListResponse

One page of jobs, newest first.
  • dataObligatorio
    Lista deLibraryItem
  • nextCursorObligatorio
    Opaque cursor of the next page, or null when this page was the last one.Cualquiera de
    • string
    • null

JobResponse

One job.

JobStatus

Lifecycle state of a job: 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

One step of the conversion checklist. A narration or a remaster does not run every step: the ones that do not apply stay done: false even when the job is done — read status for completion and steps only for the progress of a conversion.
  • doneObligatorioboolean
  • keyObligatorio
    stringValores admitidos"prepare""separate""convert""package""finalize"

JobVariant

One voice applied to the job.
  • driftMsObligatorio
    Measured timing drift in whole milliseconds, or null until measured.Cualquiera de
    • integer
    • null
  • hasResultObligatoriobooleanWhether the packaged result exists and can be downloaded.
  • speakersObligatorio
    The speakers of this variant converted with a voice other than the variant voice (speakers of POST /v1/jobs), ordered by index. Empty when the whole video takes the variant voice.
  • variantIdObligatorioCuid2
  • voiceNameObligatorio
    Name of the voice applied, or null when that voice no longer exists.Cualquiera de
    • string
    • null

JobVariantSpeaker

A speaker of the variant (index, from the detection of its upload) converted with a voice (voiceId) other than the variant voice.
  • indexObligatoriointegerMínimo: 1Máximo: 3
  • voiceIdObligatorioCuid2

LibraryItem

One job as listed by GET /v1/jobs.
  • createdAtObligatoriostringFormato: date-time
  • durationSecondsObligatorio
    Cualquiera de
    • integer
    • null
  • folderIdObligatorio
    Cualquiera de
  • idObligatorioCuid2
  • nameObligatoriostring
  • narrationObligatorioboolean
  • pausedObligatorioboolean
  • statusObligatorioJobStatus
  • tagsObligatorio
    Lista deTag
  • unnamedObligatoriobooleanTrue when name is the product fallback because the job has no name.
  • variantCountObligatoriointegerMínimo: 0

McpErrorData

The error.data of an MCP (JSON-RPC) error: the stable API code, the same correlation id as REST, and the same optional reason.
  • codeObligatorioApiErrorCode
  • reasonOpcionalApiErrorReason
  • requestIdObligatoriostringCorrelation id of this request. Quote it when contacting support.Longitud mínima: 1Longitud máxima: 128

OwnVoice

One of your own cloned voices, as listed by GET /v1/voices?kind=own.
  • idObligatorioCuid2
  • kindObligatorioValor fijo"own"
  • nameObligatoriostringThe name the holder gave this voice.
  • statusObligatorio
    active — usable right now; recycled — kept, and rebuilt automatically the next time you use it.stringValores admitidos"active""recycled"

OwnVoiceDetail

One of your own cloned voices with its sample clip, as returned by GET /v1/voices/{id}. sample is null when this voice has no clip yet.
  • idObligatorioCuid2
  • kindObligatorioValor fijo"own"
  • nameObligatoriostringThe name the holder gave this voice.
  • sampleObligatorio
    Cualquiera de
  • statusObligatorio
    active — usable right now; recycled — kept, and rebuilt automatically the next time you use it.stringValores admitidos"active""recycled"

PreflightReport

The technical analysis of the uploaded file: container, codecs, durations (seconds), variable-frame-rate suspicion and whether a remux is safe.
  • audioCodecObligatorio
    Audio codec name as probed (for example aac), or null.Cualquiera de
    • string
    • null
  • audioDurationSObligatorio
    Measured duration of the audio track, in seconds (not rounded). The minutes a job reserves round this up.Cualquiera de
    • number
    • null
  • containerObligatorio
    Container family as the probe reports it — for MP4/MOV files the ISOBMFF family string mov,mp4,m4a,3gp,3g2,mj2. The accepted extensions are constraints.containers of the upload.Cualquiera de
    • string
    • null
  • containerDurationSObligatorio
    Duration declared by the container, in seconds (not rounded).Cualquiera de
    • number
    • null
  • 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.
  • videoCodecObligatorio
    Video codec name as probed (for example h264), or null.Cualquiera de
    • string
    • null

PreflightResponse

The upload is analysed and can be turned into a job.
  • dataObligatorio
    • preflightObligatorioPreflightReport
    • readyObligatorioValor fijotrueAlways true when the pre-flight accepted the file; a refused file answers 422 instead.
    • uploadIdObligatorioCuid2

SpeakerAssignment

One speaker of a variant and the voice it takes.
  • speakerObligatoriointegerThe speaker (its index in the detection) that takes voiceId. Speaker 0 cannot be assigned: it always keeps the voice of the variant.Mínimo: 1Máximo: 3
  • voiceIdObligatoriostringThe voice for this speaker: a voice of the bank or one of your own, under the same rules as voiceIds.Patrón: ^[a-z][a-z0-9]{7,31}$

SpeakerDetection

The speaker detection of an upload: its status and, once ready, who talks when.
  • speakersObligatorio
    The speakers, ordered by index — only when status is ready (2 to 4 of them); empty otherwise.Elementos máximos: 4
  • statusObligatorioSpeakerDetectionStatus
  • uploadIdObligatorioCuid2

SpeakerDetectionResponse

The speaker detection of an upload, as it stands now.

SpeakerDetectionStatus

State of the speaker detection of an upload. 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

One stretch of the video in which this speaker talks.
  • endMsObligatoriointegerWhere it ends, in milliseconds from the start of the video.Mínimo: 0
  • startMsObligatoriointegerWhere the stretch starts, in milliseconds from the start of the video.Mínimo: 0

Tag

A tag of the library.
  • idObligatorioCuid2
  • nameObligatoriostring

UploadConstraints

The fixed product limits every upload must respect.
  • containersObligatorio
    Lista de
    stringValores admitidos"mp4""mov"
    Accepted containers.
  • maxDurationSecondsObligatorioValor fijo180Maximum video duration in seconds.
  • maxSizeBytesObligatorioValor fijo524288000Maximum object size in bytes.

VariantSpeakers

The voices of the speakers of one variant.
  • assignmentsObligatorio
    One 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: 3
  • variantVoiceIdObligatoriostringThe variant this assignment belongs to, named by its voice: one of voiceIds. That voice is speaker 0 of the variant.Patrón: ^[a-z][a-z0-9]{7,31}$

Voice

A voice of the shared bank, as listed by GET /v1/voices. Fetch GET /v1/voices/{id} for a playable sample.
  • accentObligatorio
    Label of accentCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • accentCodeObligatorio
    Stable accent code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • ageCodeObligatorio
    Stable age code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • ageRangeObligatorio
    Label of ageCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • descriptionObligatoriostringOne-paragraph description of the voice, in the negotiated language.
  • genderObligatorio
    Label of genderCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • genderCodeObligatorio
    Stable gender code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • idObligatorioCuid2
  • kindObligatorioValor fijo"bank"
  • languageObligatorio
    Label of languageCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • languageCodeObligatorio
    Stable language code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • nameObligatoriostringDisplay name of the voice.
  • sampleTextObligatoriostringWhat the sample clip says — in the language of the voice itself, not localised.
  • useCaseObligatorio
    Label of useCaseCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • useCaseCodeObligatorio
    Stable use-case code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null

VoiceDetail

A voice of the bank with its sample clip, as returned by GET /v1/voices/{id}.
  • accentObligatorio
    Label of accentCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • accentCodeObligatorio
    Stable accent code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • ageCodeObligatorio
    Stable age code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • ageRangeObligatorio
    Label of ageCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • descriptionObligatoriostringOne-paragraph description of the voice, in the negotiated language.
  • genderObligatorio
    Label of genderCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • genderCodeObligatorio
    Stable gender code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • idObligatorioCuid2
  • kindObligatorioValor fijo"bank"
  • languageObligatorio
    Label of languageCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • languageCodeObligatorio
    Stable language code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null
  • nameObligatoriostringDisplay name of the voice.
  • sampleObligatorioVoiceSample
  • sampleTextObligatoriostringWhat the sample clip says — in the language of the voice itself, not localised.
  • useCaseObligatorio
    Label of useCaseCode in the negotiated language (Accept-Language, en by default), or null when unclassified. Copy, not a filter value.Cualquiera de
    • string
    • null
  • useCaseCodeObligatorio
    Stable use-case code — one of the values GET /v1/voices/facets serves, usable as a GET /v1/voices filter — or null when unclassified.Cualquiera de
    • string
    • null

VoiceFacets

The filter values the bank currently serves, per dimension. Use them as GET /v1/voices filters.
  • accentObligatorio
    Lista de
    stringValores 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"
  • ageObligatorio
    Lista de
    stringValores admitidos"young""adult""senior"
  • genderObligatorio
    Lista de
    stringValores admitidos"female""male""neutral"
  • languageObligatorio
    Lista de
    stringValores admitidos"ar""de""en""es""fr""hi""it""ja""ko""nl""pl""pt""ro""sv""tr""zh"
  • useCaseObligatorio
    Lista de
    stringValores admitidos"advertisement""asmr""audiobook""characters""conversational""educational""entertainment""meditation""narration""news""social-media"

VoiceFacetsResponse

The served filter values.

VoiceListResponse

One page of voices. Every item carries kind: bank for a voice of the shared bank, own for one of your clones.
  • dataObligatorio
    Lista de
  • nextCursorObligatorio
    Opaque cursor of the next page, or null when this page was the last one.Cualquiera de
    • string
    • null

VoiceResponse

One voice with its sample. kind says which of the two shapes it is.

VoiceSample

A short-lived link to the voice sample clip.
  • expiresAtObligatoriostringUntil when the URL is guaranteed to work.Formato: date-time
  • urlObligatoriostringURL of the sample clip (audio/mpeg). Do not store it: it is valid until expiresAt, then fetch the voice again for a fresh one.Formato: uri

WebhookEvent

The body of every webhook delivery. 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 de
    • createdAtObligatoriostringFormato: date-time
    • dataObligatorio
      • jobIdObligatorioCuid2
      • kindObligatorio
        conversion (a remaster of a conversion included) or narration.stringValores admitidos"conversion""narration"
      • linksObligatorio
        • downloadObligatoriostringGET /v1/jobs/{id}/download of the result.Formato: uri
        • jobObligatoriostringGET /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-time
    • dataObligatorio
      • jobIdObligatorioCuid2
      • kindObligatorio
        conversion (a remaster of a conversion included) or narration.stringValores admitidos"conversion""narration"
      • linksObligatorio
        • downloadObligatorionullAlways null: a failed or cancelled job has no result.
        • jobObligatoriostringGET /v1/jobs/{id} of this job.Formato: uri
      • statusObligatorio
        stringValores admitidos"failed""cancelled"
    • idObligatoriostringDelivery-independent event id. Deduplicate on it.Patrón: ^[a-z][a-z0-9]{7,31}$
    • typeObligatorioValor fijo"job.failed"