La API
Para conectar Liora Flow con tus propios sistemas: crear salas, consultar el consumo, mandar trabajos de traducción.
⚠️Hace falta el modo integrador
Las llaves de API se emiten desde Cuenta, y esa tarjeta sólo aparece con el modo integrador encendido. Cómo se enciende.
Cómo se identifica quien llama
Una llave de API. Se emite desde Cuenta y se enseña una sola vez: de ella guardamos sólo una huella, así que si se pierde hay que emitir otra.
Se manda en una cabecera, de estas dos formas:
curl -H "X-API-Key: lk_…" https://…/api/v1/rooms
curl -H "Authorization: Bearer lk_…" https://…/api/v1/rooms
⛔No la pongas en la dirección
Se acepta ?api_key=… por compatibilidad, pero no lo uses: una llave en
la dirección queda en el historial del navegador, en el Referer de
cualquier cosa que cargue esa página, y en los registros de cualquier proxy
por el que pase.
Los nuestros no la guardan. Los de otro sí pueden.
Lo que abre una llave
La cuenta entera. No hay llaves de sólo lectura ni acotadas a un sitio: quien tiene la llave puede todo lo que puede la cuenta.
Por eso sólo la emite quien administra la cuenta —ni siquiera quien lleva las facturas—, y por eso conviene tener una por sistema y revocar la que ya no se use: así se corta uno sin tocar los demás.
✅Cada llave ve sólo lo suyo
Una llave de una cuenta no puede leer ni tocar los datos de otra. Pedir una sala ajena contesta que no existe, y borrarla, lo mismo.
Qué se puede hacer
| Camino | Para qué |
|---|---|
GET /api/v1/rooms |
Las salas de tu cuenta |
POST /api/v1/rooms |
Crear una. Si tu cuenta tiene varios sitios, di en cuál con site_id (el id de GET /api/v1/sites) |
GET /api/v1/rooms/{id} |
Una en concreto, por su id, no por su identificador de emisión |
PATCH /api/v1/rooms/{id} |
Cambiarle el nombre, y desde qué redes se entra (allowed_ips) |
PUT /api/v1/rooms/{id}/languages |
A qué idiomas traduce |
DELETE /api/v1/rooms/{id} |
Borrarla |
GET /api/v1/rooms/{id}/events |
Qué ha ido pasando en ella |
GET /api/v1/usage |
El consumo en directo, día a día. Lo de los trabajos y las actas va en cada trabajo, en /jobs |
GET /api/v1/credit |
El saldo |
GET /api/v1/jobs · POST /api/v1/jobs |
Trabajos de traducción de audio o texto |
GET /api/v1/jobs/{id}/result |
Lo transcrito, en txt, json, srt o vtt |
GET /api/v1/jobs/{id}/result?format=acta |
El acta del fichero, en txt. También acta.md, acta.json y acta.pdf |
POST /api/v1/jobs/{id}/summary |
Encarga el resumen. Contesta 202: se está haciendo |
GET /api/v1/jobs/{id}/summary |
Lo recoge cuando está |
PUT · POST · DELETE /api/v1/jobs/{id}/acta/email |
A dónde mandar el acta, mandarla ahora, o dejar de mandarla |
PUT · DELETE /api/v1/jobs/{id}/acta/webhook |
Que te avisemos a una dirección cuando esté, o dejar de avisar |
El acta sólo existe en un trabajo de acta: una grabación pedida con with_acta: true. En cualquier
otro trabajo —una traducción, un doblaje— sus puertas contestan 404 (descargarla, mandarla, avisar,
y GET …/summary aunque se hubiera encargado antes) o 409 NOT_AN_ACTA_JOB (encargarla), sin cobrar
ni escribir nada, y un acta_email sin with_acta se rechaza al crear el trabajo con
400 ACTA_EMAIL_NEEDS_WITH_ACTA. acta_language sólo viene en un trabajo de acta.
Mandar el acta ahora (POST …/acta/email, a la dirección guardada o a otra con to) tiene los
mismos límites por hora que la aplicación: pasados, contesta 429 TOO_MANY sin mandar nada, y dos
envíos iguales en el mismo minuto contestan 409 ALREADY_SENT. Una copia a otra dirección no cambia
lo que se apunta de la dirección guardada.
El aviso (PUT …/acta/webhook) sólo va a una dirección https pública: no sigue redirecciones
—una respuesta 3xx cuenta como fallo— y, si falla, webhook_error dice el motivo sin repetir la
dirección.
Los ajustes acústicos de una sala no están en la API: se afinan escuchando cómo suena ese sitio, y de eso nos encargamos nosotros. Si una sala no se oye como debería, dilo y la ajustamos.
Y si pides que te avisemos: qué te llega
PUT /api/v1/jobs/{id}/acta/webhook con {"url": "https://…"} hace que te
mandemos un POST cuando el acta esté. Esto es lo que llega, con
Content-Type: application/json:
{
"event": "acta.ready",
"job_id": 4711,
"name": "reunion-de-ventas.mp4",
"lines": 128,
"formats": ["txt", "md", "json", "pdf"],
"download": "https://<nuestra-api>/api/v1/jobs/4711/result?format=acta",
"at": 1788557146020
}
event— hoy sólo hay uno,acta.ready, y va igualmente: quien lo reciba quiere poder distinguirlo el día que haya un segundo.formats— los que de verdad se pueden escribir de este documento: elpdfdepende de que haya letra para lo que se dijo. No des por hecho los cuatro.download— ya montada, para que no tengas que componer la dirección a mano ni acertar con el parámetro.at— en milisegundos, para poder ordenar dos avisos sin fiarte de la hora del que los recibe.
Estos nombres son un contrato. Quien monte un Zapier contra esto lo lee por nombre de campo, así que se añaden campos y no se renombran.
Lo que devuelve
JSON llano. Un campo que venga vacío no aparece en la respuesta, en vez de salir como un cero o como una fecha del año 1.
{
"rooms": [
{
"id": "039e1c2d-96da-4a62-8b39-1102e11d1d22",
"company_id": 7,
"private": false,
"access_ttl_minutes": 0,
"review_room": false,
"auto_stop_minutes": 0,
"source": "srt",
"client_id": "rm_sh55iky7i5zgw",
"enabled": true,
"name": "Sala principal",
"created_at": "2026-08-15T14:01:19Z"
}
]
}
Los campos son ésos y en ese orden. access_ttl_minutes en cero significa
«mientras dure la emisión», y private en falso, que no hace falta código para
entrar. review_room dice si es la sala abierta que usan quienes revisan la
aplicación en las tiendas (la marcamos nosotros), y auto_stop_minutes cuántos
minutos sin que nadie hable cortan una emisión; en cero, nunca.
Desde qué redes se entra en una sala
Una sala puede abrirse sólo desde unas direcciones o unas redes:
Se manda en el PATCH de la sala (PATCH /api/v1/rooms/{id}), con el cuerpo:
{ "allowed_ips": "LA-IP-PUBLICA-DE-TU-SEDE, LA-RED-DE-TU-SEDE/24" }
Se escribe una dirección, o una red en notación CIDR, o varias separadas por comas. Fuera de ellas la sala no existe: no sale en la lista de la aplicación, no se entrega testigo y no se deja escuchar — la misma respuesta que una sala inventada, porque distinguirlas diría qué salas hay ahí dentro.
Tres cosas que conviene saber antes de ponerlo:
- Una dirección suelta es ella sola, y no su vecindario: se guarda con
/32. Para abrir una red entera hay que escribir la red, con su máscara. - Se suma a la contraseña, no la sustituye. Una sala privada con lista necesita las dos cosas: el código de la emisión y estar en la red.
- También para quien emite. La lista dice desde dónde se entra en la sala, y quien la enciende entra igual que quien escucha.
Mandar "allowed_ips": "" quita la lista y la sala vuelve a abrirse desde
cualquier sitio. Una entrada que no es una dirección ni una red se contesta con
400 ALLOWED_IPS_INVALID y no se guarda nada, tampoco lo demás que fuera en
la misma petición.
ℹ️En qué servidor está tu sala no sale
Es infraestructura nuestra, no información tuya. Si algún día cambiamos de sitio una sala, tu integración no se entera — que es como tiene que ser.
Traducir un documento
POST /api/v1/jobs/document, como multipart/form-data, con el fichero en el campo
document y los mismos campos que un texto —targets (separados por comas o repetido),
source, clean, voice, voice_gender, voice_format, voice_speed,
mail_when_done, mail_also, webhook_url—. Leemos el texto como lo lee el panel y
abrimos un trabajo de texto con él: mismo precio, misma cola, mismo resultado.
curl -s -X POST "$LIORA_URL/api/v1/jobs/document" -H "x-api-key: $LIORA_KEY" \
-F document=@contrato.docx -F targets=en,fr -F source=es
Formatos: .pdf, .docx, .odt, .doc, .rtf, .txt, .md y .csv, hasta 20 MB.
| Código | error |
Cuándo |
|---|---|---|
400 |
NO_DOCUMENT |
No viene el campo document |
413 |
DOCUMENT_TOO_BIG |
Pasa de 20 MB |
415 |
UNSUPPORTED_FORMAT |
No es uno de los formatos de arriba |
422 |
EMPTY_DOCUMENT |
No tiene texto (un PDF escaneado, por ejemplo) |
422 |
DOCUMENT_UNREADABLE |
No se pudo leer como lo que dice ser, o tardó demasiado en abrirse |
Y los de un texto (NO_TARGET, TEXT_TOO_LONG, NO_CREDIT…) con lo que salga de dentro.
Que te avisemos cuando un trabajo termine
Abre el trabajo con "webhook_url": "https://…" —o ponlo después con
PUT /api/v1/jobs/{id}/webhook— y te mandamos un POST cuando termine, bien o mal:
{
"event": "job.done",
"delivery_id": 812,
"job_id": 4711,
"kind": "audio",
"state": "done",
"name": "pleno.mp3",
"job": "https://<nuestra-api>/api/v1/jobs/4711",
"result": "https://<nuestra-api>/api/v1/jobs/4711/result",
"attempt": 1,
"at": 1790000000000
}
eventesjob.doneojob.failed; en el segundo,statedicefailedorejected, y una grabación rechazada trae sucode. Un trabajo que cancelas tú no avisa.- Va firmado.
Liora-Timestampson los segundos Unix yLiora-Signatureessha256=más el HMAC-SHA256 en hexadecimal detimestamp + "." + cuerpo, con tu secreto:GET /api/v1/webhooks/secret. Calcúlalo sobre los bytes tal como llegan y rechaza una hora a más de cinco minutos de tu reloj.POST /api/v1/webhooks/secret/rotatelo cambia, y el viejo deja de valer en ese momento. - Se reintenta hasta que contestes un
2xx: al minuto, a los 5, a los 30, a las 2 horas, a las 6 y a las 12 — siete intentos en unas 21 horas. Un410lo para. El mismo aviso puede llegar dos veces (un reintento tras una respuesta que se perdió):delivery_id—también en la cabeceraLiora-Delivery— es el mismo, así que descarta el repetido. - Sólo a direcciones
httpspúblicas, y sin seguir redirecciones.GET /api/v1/jobs/{id}/webhookdice cómo fue:waiting,due,sentogave_up, con el último motivo.
Pedirlo dos veces sin hacerlo dos veces
POST /api/v1/jobs, POST /api/v1/jobs/document, POST /api/v1/jobs/{id}/summary y
POST /api/v1/rooms/{id}/token aceptan la cabecera Idempotency-Key (hasta 255 caracteres, la que tú elijas). Si la
primera respuesta se perdió y repites la petición con la misma llave, te contestamos lo
mismo que la primera vez —con Idempotent-Replayed: true— sin abrir otro trabajo ni
cobrar otra vez. Se recuerda 24 horas, por cuenta y por ruta.
- La misma llave con otro cuerpo se rechaza:
422 IDEMPOTENCY_KEY_REUSED. - Mientras la primera aún está en marcha:
409 IDEMPOTENCY_IN_PROGRESS. - Sólo se recuerda lo que salió bien. Un rechazo no escribe nada, así que puedes corregir la petición y repetirla con la misma llave.
- Si en ese momento no podemos garantizarlo:
503 IDEMPOTENCY_UNAVAILABLE, y no se hace nada. Vuelve a intentarlo con la misma llave.
Cuando algo va mal
Siempre la misma forma, y el que manda es error — el message está para leerlo
una persona y puede cambiar:
{ "success": false, "error": "API_KEY_INVALID", "message": "The API key is not valid" }
Identificarse
| Código | error |
Cuándo |
|---|---|---|
401 |
API_KEY_MISSING |
No mandaste llave por ninguna de las tres formas |
401 |
API_KEY_INVALID |
La llave no existe o está revocada |
500 |
AUTH_ERROR |
No pudimos comprobar la llave en ese momento: vuelve a intentarlo |
429 |
RATE_LIMITED |
Demasiadas peticiones; retry_after (y la cabecera Retry-After) dice cuántos segundos esperar |
Salas
| Código | error |
Cuándo |
|---|---|---|
404 |
NOT_FOUND |
Esa sala no existe o no es de tu cuenta — la misma respuesta a propósito: decir «existe pero no es tuya» ya es contar algo de otro |
409 |
ROOM_NOT_CONFIGURED |
La sala aún no tiene ajustes; pásale los idiomas antes |
409 |
APP_ID_TAKEN |
Al crear, el nombre que le diste ya lo lleva otra sala tuya |
400 |
SITE_REQUIRED |
Al crear, tu cuenta tiene varios sitios y no dijiste en cuál: manda site_id |
404 |
NOT_FOUND |
Al crear, el site_id no es de tu cuenta |
Trabajos
| Código | error |
Cuándo |
|---|---|---|
413 |
TOO_LONG |
Se pasa de los límites de arriba (al cerrar una subida, por lo que pesa el fichero) |
413 |
TEXT_TOO_LONG |
Un texto de más de 100.000 caracteres |
400 |
EMPTY_TEXT |
Un texto vacío: no hay nada que traducir |
400 |
NO_TARGET |
Ningún idioma de destino en targets |
400 |
BAD_WEBHOOK |
webhook_url no es una dirección https pública |
503 |
NO_WEBHOOKS |
Este despliegue no manda avisos de trabajos |
429 |
QUEUE_FULL |
Ya tienes tres en cola, contando las grabaciones en checking. Espera a que termine uno |
402 |
NO_CREDIT |
Un texto: no hay saldo para lo que va a costar. Lo dice antes de empezar. En una grabación llega como rechazo (abajo) |
409 |
NOT_DONE |
Pides el resultado y el trabajo aún no ha terminado |
410 |
RESULT_GONE |
Ha pasado el plazo de guardado y el resultado ya no está |
409 |
TOO_LATE |
Intentas cancelar algo que ya se estaba haciendo |
409 |
OFFSET_MISMATCH |
Al reanudar una subida, el byte por el que ibas no cuadra. La respuesta trae por cuál seguir |
💡Los que se arreglan solos y los que no
QUEUE_FULL se arregla esperando y merece reintento. NO_CREDIT,
TOO_LONG y NO_SPEECH no: reintentarlos da exactamente el mismo resultado
y sólo gasta viajes.
Cerrar la subida de una grabación contesta al momento · 27/09/2026
POST /api/v1/jobs/{id}/complete contesta 202 con el trabajo en
state: "checking" y sin precio: la grabación se comprueba después, en el
servidor (convertirla, buscar la voz, medirla y ponerle precio). Llamarlo otra vez
cuando el trabajo ya no se está subiendo contesta 200 con el trabajo como
esté, sea cual sea su estado; nunca un error por llegar tarde.
uploading → checking → queued → running → done
├──→ rejected (NO_CREDIT y CHECK_FAILED: se puede reintentar 24 h)
└──→ cancelled (DELETE /api/v1/jobs/{id}; final)
Consulta GET /api/v1/jobs/{id} (o el listado) hasta que salga de checking. Mientras
está ahí, el trabajo trae check:
"check": { "position": 1 } // esperando turno en la tarjeta: 1 delante (0 = el siguiente)
"check": { "running": true } // la tarjeta ya está con él
position es una estimación: la cola reparte por turnos entre cuentas y lo corto va
primero, así que puede subir si entra algo más corto o de una cuenta que está usando
menos la tarjeta. check sólo viene mientras el trabajo está en checking.
Topes: esperando turno, hasta 12 horas; con la tarjeta ya en ello, 1 hora (4 horas de
audio tardan unos 23 minutos). Pasado cualquiera, rejected con CHECK_FAILED.
rejected es final: no se ha cobrado nada, y trae el motivo:
{ "id": 242, "state": "rejected",
"refusal": { "code": "NO_CREDIT", "detail": "no hay saldo suficiente para este trabajo",
"credits_seconds": 14335, "retry_until": "2026-09-28T21:04:00Z" } }
refusal.code |
Qué pasó | ¿Se puede reintentar? |
|---|---|---|
NO_SPEECH |
En esa grabación no se ha encontrado voz | No: el mismo fichero volvería a fallar |
TOO_LONG |
Dura más del máximo, medido de verdad. El detail dice qué hacer |
No |
NO_CREDIT |
No hay saldo; credits_seconds es lo que costaba |
Sí, 24 horas |
UNREADABLE |
El fichero no se puede leer como audio | No |
CHECK_FAILED |
No se pudo comprobar a tiempo | Sí, 24 horas |
Reintentar sin volver a subir
Con NO_CREDIT y CHECK_FAILED guardamos el audio 24 horas (lo dice retry_until) y
el trabajo se puede volver a comprobar sin subir nada:
POST /api/v1/jobs/{id}/retry
→ 202 el trabajo, otra vez en checking
→ 409 NOT_RETRYABLE: otro motivo de rechazo, pasado retry_until, o el trabajo no está rechazado
→ 429 QUEUE_FULL: ya tienes tres en cola (cuentan los que están en checking)
Con cualquier otro motivo el audio se borra al rechazar. Llamar a retry dos veces seguidas
deja un solo checking.
El que manda es refusal.code; el texto de detail puede cambiar de palabras. Si
prefieres que te avisemos en vez de consultar, abre el trabajo con webhook_url (abajo).
Límites
Están en su propia página: límites de la API.
La referencia completa
Esto es lo que hace falta entender. Cada parámetro, cada respuesta y cada código, uno a uno, están en la referencia, que se puede leer y probar desde el navegador:
Está en inglés, como la propia especificación de la que se genera.