The manual pages are written in Spanish.

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: el pdf depende 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
}
  • event es job.done o job.failed; en el segundo, state dice failed o rejected, y una grabación rechazada trae su code. Un trabajo que cancelas tú no avisa.
  • Va firmado. Liora-Timestamp son los segundos Unix y Liora-Signature es sha256= más el HMAC-SHA256 en hexadecimal de timestamp + "." + 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/rotate lo 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. Un 410 lo para. El mismo aviso puede llegar dos veces (un reintento tras una respuesta que se perdió): delivery_id —también en la cabecera Liora-Delivery— es el mismo, así que descarta el repetido.
  • Sólo a direcciones https públicas, y sin seguir redirecciones. GET /api/v1/jobs/{id}/webhook dice cómo fue: waiting, due, sent o gave_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:

La referencia de la API →

Está en inglés, como la propia especificación de la que se genera.