Guía de integración

Una guía, tres lectores. La aplicación, quien integra desde fuera y quien lleva el motor usan las mismas puertas, y tenerlas en tres documentos garantiza que dos se queden viejos. Aquí están juntas.

Escrita el 31/08/2026 desde docs/encargo-guia-de-integracion.md del backend, y partiendo de lo que ya existía, no rehaciéndolo: guia-para-el-front.md, para-integrar.md, API_limites.md y borrar-la-cuenta-para-la-app.md.

Todas las cifras de esta guía salen del código y llevan su origen escrito —apartado 10—. Si una no cuadra, gana el código y hay que corregir aquí.

1. Quién eres y por dónde entras

LectorQué usaCómo entra
La aplicación — iOS, Android, escritorio/api/**testigo de la persona
Quien integra desde sus sistemas/api/v1/**llave de API de su empresa
La pasarela y el motor/api/rtc/**testigo de servicio

Las tres puertas no son intercambiables. Una llave de API no vale en /api/**, y el testigo de servicio no sale de nuestra red. Si una llamada contesta 401 estando el testigo bien, lo primero que hay que mirar es si la ruta es la de tu puerta.

La llave de tu empresa abre todo /api/v1/**: sitios, salas y también la cola de trabajos (/api/v1/jobs, apartado 4).


2. Los tres interruptores de una sesión

Es lo más nuevo y lo que más se va a malinterpretar. Una reunión o una sala tiene tres cosas distintas que se confunden constantemente:

CampoQué hace
translation_enabledproduce voz y texto en otros idiomas
transcription_enabledproduce el texto de lo que se dice, en su idioma
acta_enabledguarda ese texto para quien organiza

Las tres reglas entre ellos

1 · Traducir ya implica transcribir. No se puede traducir sin reconocer antes lo que se dice, así que una reunión que traduce ya está produciendo el texto. No hace falta pedir las dos.

2 · Transcribir no implica guardar. Se pueden querer subtítulos en directo sin que quede nada después. Son dos decisiones y se toman por separado.

3 · Sin traducción ni transcripción no puede haber acta. Se rechaza con 409 ACTA_NEEDS_TEXT. Y no es sólo el manejador: la base lo impide con un CHECK, así que no hay forma de dejar una sesión que diga que guarda y no guarde nada.

El caso que conviene tener en la cabeza

Un ayuntamiento que quiere el acta de su pleno y no quiere traducirlo a ningún idioma. El pleno se celebra entero en un idioma: no hay a quién traducirle, y el texto hace falta igual.

``jsonc { "translation_enabled": false, "transcription_enabled": true, "acta_enabled": true } ``


3. Crear y cambiar una reunión

```jsonc POST /api/scheduled-meetings { "title": "Pleno", "starts_at": 1788172800000, "ends_at": 1788176400000, "translation_enabled": false, "transcription_enabled": true, "acta_enabled": true }

PATCH /api/scheduled-meetings/{id} { "acta_enabled": false } ```

Los tres son opcionales y son punteros, y los dos verbos NO contestan igual a la ausencia. Es la confusión fácil, así que va dicha entera:

  • Al editar (PATCH), el campo que no viene deja lo que hubiera. Un PATCH que sólo cambia el título no apaga nada.
  • Al crear (POST) no hay «lo que hubiera», así que cada uno nace en su valor por defecto, y los tres nacen apagados. Desde el 04/09/2026 eso incluye translation_enabled, que hasta esa fecha nacía encendido cuando no se mandaba.

Aviso de compatibilidad, y es para quien está leyendo esto: si creabas reuniones sin mandar translation_enabled y contabas con que se tradujeran, dejan de traducirse. Mándalo: {"translation_enabled": true}.

Para que nadie se entere por la factura, la respuesta del alta lo dice cuando el campo no viajaba:

``jsonc { "translation_enabled": false, "translation_defaulted": true } ``

Dos avisos que hay que dar

translation_enabled: true puede contestar 402 si la cuenta no tiene plan ni saldo. Y una cuenta sin saldo crea la reunión con la traducción apagada en silencio: hay que leer lo que contesta, no suponerlo. Una pantalla que da por hecho que se creó como se pidió enseñará una reunión que no traduce sin que nadie lo haya visto.

Quien no organiza sólo puede tocar translation_enabled, y sólo si translation_control lo permite. Transcribir y guardar son de quien organiza: son decisiones sobre lo que pasa con lo que dicen los demás.

translation_control: cuatro valores

Lo pone quien crea la reunión, y el que no sea uno de los cuatro se rechaza: una reunión con un control inventado no se sabe si abre o cierra.

ValorQuién puede tocar la traducción
everyonecualquiera de dentro
anyoneel mismo, con el nombre viejo — se acepta y se guarda tal cual
accounts_onlylos que tienen cuenta; los invitados sólo leen
organizersólo quien organiza y quien él delegue

Por qué anyone sigue vivo, que es la lección de todo esto. El campo existía y se ignoraba. El día que se empezó a validar, anyone —que las aplicaciones llevaban mandando desde siempre— pasó a contestar 400 y rompió el formulario de crear una reunión, que no tenía nada que ver con el cambio.

Un campo que pasa de ignorado a validado sin avisar rompe a todos los que ya lo mandaban. Si vais a empezar a validar algo que hasta hoy se tiraba, contadlo antes y aceptad lo viejo una temporada.


4. Subir una grabación

/api/v1/jobs se abre con la llave de API de tu cuenta, igual que sitios y salas.

Di cuánto va a pesar al abrir

``jsonc POST /api/v1/jobs { "kind": "audio", "filename": "pleno.m4a", "bytes": 314572800, "source": "es", "targets": ["en"] } ``

bytes es opcional para no romper lo que ya funciona, pero mándalo: con él, un fichero que no cabe se rechaza antes de subir un solo byte en vez de después de media hora, y es lo único que permite dar un porcentaje.

La respuesta trae los tres límites. No los escribas a mano: dependen del fichero.

``jsonc "upload": { "url": "…", "method": "PATCH", "complete": "…", "chunk_bytes": 4194304, "max_bytes": 838860800, "max_seconds": 14400 } ``

Un vídeo tiene una hora; el audio, cuatro

max_seconds viene por fichero: 14400 para audio y 3600 si la extensión es de vídeo. De un vídeo sólo se usa la pista de sonido, y una hora de móvil son gigas de imagen que nadie va a mirar.

Cuando se rechace por largo, el mensaje ya dice qué hacer —sacar el audio y subirlo, que así entran cuatro horas—. Enséñalo tal cual: es lo único que convierte un «no» en algo que la persona puede resolver.

Cada bloque dice por dónde va

``jsonc PATCH /api/v1/jobs/{id}/upload?offset=4194304 → 200 { "offset": 8388608, "total": 314572800, "percent": 2 } ``

  • percent es para la barra. Sólo sale si mandaste bytes.
  • 409 no es un error: es un bloque que ya había llegado. Trae el offset bueno y hay que seguir desde ahí, no empezar de cero. Es lo que convierte una conexión cortada en un bloque repetido en vez de cuatro horas de audio otra vez.
  • El bloque sugerido son cuatro megas. Era medio mega; a cuatro horas, aquello eran 1.600 peticiones.

Y al cerrar

``jsonc POST /api/v1/jobs/{id}/complete → 503 { "error": "TRY_AGAIN", … } ``

503 TRY_AGAIN no es un fallo del fichero. No se pudo cerrar ahora mismo y lo subido se conserva: hay que volver a llamar a complete dentro de un momento, sin subir nada otra vez. Si la pantalla lo trata como un error normal, la persona repite la subida entera para nada.


4.bis Lo que sólo le importa a quien integra

Traído de para-integrar.md, porque sin esto la guía sirve a dos de sus tres lectores. Lo demás de este documento vale para los tres; esto es lo que sólo usa quien conecta sus sistemas.

Antes de nada: el modo integrador viene apagado

Se enciende en Cuenta, marcando la casilla del aviso. Mientras está apagado, en el panel no existen Sitios, ni Salas, ni la referencia de la API — ni sus pantallas ni sus direcciones.

Y el aviso es literal: esto puede costar dinero. Al otro lado hay botones que crean salas y llaves. Una sala encendida traduce, y traducir se factura por minutos. Por eso sólo lo puede encender quien administra la cuenta: es su dinero.

Una llave de API abre la cuenta entera

Y de ahí sale la regla que más se salta: la emite quien administra, no quien paga. Una llave permite crear salas, así que si la pudiera emitir quien lleva las facturas, acabaría teniendo por otra puerta todo lo que no se le había dado.

PapelGenteFacturarEmitir llavesSalasVer consumo
ownersísísísísí
payernosínonosí
operatornononosísí
viewernonononosí
miembrononononono

De qué monedero sale cada minuto

Paga el más cercano al sitio donde se habla: la sala sube a su sede y la sede a la empresa, y paga el primero que tenga monedero.

De ahí una consecuencia útil: cambiar quién paga es dar o quitar un monedero, sin migrar nada y sin esperar a nadie.

Y repartir no es regalar. Los minutos ya están comprados y sólo cambian de sitio: de 1.000 de la cuenta, si se le dan 300 a una sede, en el bote común quedan 700. El total no cambia, no se puede repartir hacia otra cuenta, ni más de lo que hay.

Para que una sala no se coma los minutos de las demás

FormaQué hace¿Corta?
Bote comúngasta de la cuenta, sin límite — es lo de por defectono
Monedero propiotiene su saldoal acabarse
Topedel bote común, pero no más de X al messí
Porcentajedel bote común, pero no más del X %sí
Suscripcióncuota mensual con minutos incluidossegún el plan

5. Borrar la cuenta

Sin esto no se publica en Google Play: exigen poder borrar la cuenta desde dentro de la aplicación y desde una dirección web.

``jsonc POST /api/users/me/deletion // pide; manda un correo con el enlace GET /api/users/me/deletion // ¿hay uno esperando? DELETE /api/users/me/deletion // cancelar, mientras no se haya ejecutado ``

POST contesta 202, no 200, y la pantalla tiene que decirlo así:

``jsonc 202 { "success": true, "status": "confirmation_sent", "expires_in_hours": 24 } ``

No se ha borrado nada: va un correo de camino. Si la pantalla dice «cuenta borrada» aquí, la persona cierra la aplicación creyendo que ya está y a las 24 horas sigue teniendo cuenta.

Los tres «no» que hay que pintar

CuándoQué decir
409 LAST_OWNERes el único dueño de una empresaque le pase la propiedad a otra persona y vuelva a pedirlo
409 NO_EMAILla cuenta no tiene dirección de correono hay con qué confirmar
503 MAIL_FAILEDel correo no salióno se ha borrado nada; que lo intente otra vez

El enlace del correo no borra

Abre una página con un botón, y sólo el botón lo gasta. Los clientes de correo piden los enlaces para previsualizarlos y los antivirus de empresa abren todos los de todos los mensajes: si el enlace borrara, un antivirus borraría cuentas.

Si algún día quieres confirmar dentro de la aplicación:

``jsonc POST /api/account-deletion/confirm token=… // EN EL CUERPO, no en la barra → 200 { "success": true, "status": "deleted" } → 410 // el enlace ya no vale ``

El testigo va en el cuerpo. En la barra se queda en el historial del navegador y en el registro de cualquier intermediario, y éste borra una cuenta.


6. Denunciar una mala traducción

Sin esto tampoco se publica en Google Play. Y sirve para más que cumplir: hoy, cuando el motor traduce mal una palabra, no hay forma de enterarse.

``jsonc POST /api/translation-reports { "source": "la disnea paroxística nocturna", "output": "la disnea para oxística nocturna", "from": "es", "to": "en", "via": "voz", // "voz" | "texto" "product": "sala", // "app" | "sala" | "reunion" | "teams" | "zoom" | "trabajo" "comment": "sale rarísimo" } // opcional → 201 { "success": true, "id": "…" } ``

Vale con cuenta y con pase de invitado

Y es lo que más importa de esta puerta: en una sala a la que se entra con un código, quien escucha no tiene cuenta — y es justo quien más va a oír una traducción rara. Pon el botón también en esa pantalla.

Lo que NO se manda, y por qué el cuerpo no lo pide

No hay campo para la sala, la reunión, la conversación ni con quién hablaba. Y no es que se rechace: es que no se pide, que es la única forma de que un campo así no se cuele más adelante.

Una denuncia tiene que servir para reproducir el fallo, no para reconstruir de qué hablaban dos personas. Tampoco se manda el audio.


7. El aviso de que la sala se está traduciendo

No hay que hacer nada: llega solo. Está aquí para que sepas qué es ese mensaje y no lo trates como uno más.

En cuanto alguien entra en una reunión con traducción, aparece en el chat un mensaje del bot, con is_system: true y sender_id: "liora-bot".

  • No lo escondas. Ese mensaje es lo que cumple el artículo 50.1 del Reglamento de IA —informar de que se está tratando con un sistema de IA desde el principio de la primera interacción— y esa obligación es nuestra, no del cliente.
  • Sale una sola vez por reunión, no una por persona que entra. Si ves dos, es un fallo y hay que decirlo.
  • No sale donde no se traduce.
  • No lo caches ni lo traduzcas tú. El texto no es una constante: se compone de lo que esté encendido en esa sala, y llega ya en el idioma de quien entró. El día que una sala guarde su transcripción, la frase lo dirá también.

7.bis Bajar el acta de una reunión

Existen desde el 31/08/2026 y sólo para reuniones. Una sala SRT todavía no —ver el apartado 12—.

``jsonc GET /api/scheduled-meetings/{id}/acta // qué hay GET /api/scheduled-meetings/{id}/acta/{formato} // txt | md | pdf DELETE /api/scheduled-meetings/{id}/acta // borrarla ``

Sólo el organizador. Cualquier otro recibe 404, no 403: un 403 le confirmaría que esa acta existe.

formats no es una lista fija, y hay que leerla

Viene en la respuesta de la primera llamada y cambia según el texto. El motivo es concreto y está medido: un PDF sólo dibuja lo que su letra sabe dibujar, y un PDF cuya letra no tiene esos dibujos sale en blanco y al extraerle el texto lo devuelve perfecto. O sea: un 200 de 800 KB que al abrirlo son cuadrados, y ninguna comprobación automática lo caza.

Por eso el backend mira carácter a carácter contra el fichero de la fuente y, si algo no se puede escribir, pdf no sale en formats y pedirlo da 409 NO_PDF_FOR_THAT_TEXT.

No lo pintéis como un fallo nuestro. Lo que hay que decir es «ese idioma se baja en txt o en md».

Quince días, y el documento no está guardado

Se compone al pedirlo, no vive en ningún sitio. La fecha exacta en la que deja de poder pedirse va en expires_at.


8. La puerta del motor

POST /api/rtc/transcript, detrás del testigo de servicio. Es de quien lleva el motor y de la pasarela, no de un cliente.

``jsonc POST /api/rtc/transcript X-API-Key: <testigo de servicio> { "session": "reunion", "session_id": "…", "sentences": [ { "order": 1, "start_ms": 0, "end_ms": 900, "text": "…", "lang": "es", "speaker": 1, "confidence": 870, "translations": { "en": "…" } } ] } → 200 { "success": true, "stored": 2, "acta": true } → 200 { "success": true, "stored": 0, "acta": false } ``

session sólo acepta dos valores

ValorQué esDónde vive
"reunion"una reunión programadatabla reuniones
"sala"una sala SRTtabla rtc_rooms

Cualquier otra cosa recibe ErrNotASession. No hay llamada: una llamada uno a uno no guarda nada y no está previsto que lo haga.

Y "sala" hoy no sirve de nada todavía. rtc_rooms tiene sus columnas, pero no hay ninguna API que las encienda, así que una sala nunca tiene el interruptor a 1 y toda frase que llegue con session: "sala" se tira. El almacén está; la puerta para encenderlo, no.

Cuatro cosas más que no son obvias:

  1. acta: false con 200 no es un error. Apagada es el estado normal de todas las sesiones que existen; contestar un error haría que el motor reintentara algo que funciona como debe.
  2. confidence no es un adorno. La separación de voces acierta en torno al 85 %, y esa cifra es lo que permite que el documento diga «hablante probable» donde el sistema dudó. Un acta que afirma una certeza que no tiene es peor que una que admite la duda.
  3. Reenviar una frase con el mismo order no la duplica, para que un corte de conexión no meta la misma línea dos veces en el documento.
  4. No hay campo para el audio. No lo habrá.

9. Los códigos de error que hay que pintar

Cada uno existe en un manejador y hay una prueba que lo comprueba.

CódigoCuándo
LAST_OWNERborrarse siendo el único dueño de una empresa
NO_EMAILborrarse sin dirección con la que confirmar
MAIL_FAILEDel correo de confirmación no salió; no se borró nada
TRY_AGAINcerrar una subida falló ahora mismo; lo subido se conserva
ACTA_NEEDS_TEXTacta sin traducción ni transcripción
NOTHING_TO_REPRODUCEdenuncia sin origen o sin salida
BAD_REPORTdenuncia sin par de idiomas, vía o producto
TOO_MANYpasado el tope de denuncias

10. Las cifras, con su origen

Medidas contra el código el 31/08/2026. Cada una lleva dónde vive: si cambia allí, cambia aquí.

QuéValorDónde vive
grabación: duración14400 s (4 h)models.MaxJobAudioSeconds
grabación: peso838860800 (800 MiB)models.MaxJobAudioBytes
vídeo: duración3600 s (1 h)models.MaxJobVideoSeconds
bloque sugerido al subir4194304 (4 MiB)models.UploadChunkBytes
enlace de borrar la cuenta24 hrepository.TheLinkLasts
denuncias por persona y hora20repository.HowManyReportsPerPerson
denuncias: cuándo salta el aviso3 en 24 hrepository.WhenSomethingRepeats
denuncias: cuánto se guardan365 díasrepository.HowLongAReportIsKept
lo dicho en una sesión15 díasrepository.HowLongATranscriptLives → plazos.QuinceDias
todo lo demás que guarda algo dicho15 díasplazos.QuinceDias

El de vídeo es el que más se olvida, y estaba mal publicado en nuestra propia web hasta hoy: decíamos «cuatro horas por grabación» sin distinguir, en una página que incluye los formatos de vídeo.


11. Lo que NO se puede decir

  • No prometas un tiempo de entrega de un trabajo ni de un acta. Depende de lo larga que sea la sesión y de cómo estén los servidores. Lo que sí hay es estado —encolado, en curso, listo, fallido— y el aviso al terminar. Un trabajo que dice «listo en 4 minutos» y tarda veinte es peor que uno que no dice nada.
  • No digas que se guarda el audio en ningún caso. No se guarda: ni en un temporal, ni «mientras se procesa». Todo trabaja sobre el texto.
  • No digas que el acta está terminada. Ver el apartado 12.
  • No inventes el precio de transcribir sin traducir. No está decidido.
  • No nombres piezas internas —el motor, los modelos, la infraestructura— en nada que se publique.

12. Lo que falta, dicho por su nombre

Esto es la mitad útil de la guía: lo que no está.

  • El acta de una REUNIÓN se puede bajar —apartado 7.bis—, y la de una SALA también: GET /api/v1/rooms/{id}/acta, sólo para leer. Encenderla y borrarla se hace desde el panel.
  • El resumen —con decisiones, tareas, acuerdos de trámite y señales— se pide sobre un trabajo: POST /api/v1/jobs/{id}/summary. La lista de decisiones recoge la mayor parte de lo decidido, no cada cosa, y por eso sale marcado como borrador.
  • transcription_enabled saca los subtítulos de una reunión sin traducirlos, a 1,25 créditos por minuto hablado. Es excluyente con la traducción: quien quiera el texto apaga la traducción.
  • Transcribir una grabación sin traducirla, por la API: todavía no. Un trabajo pide al menos un idioma de destino (400 NO_TARGET); la transcripción en el idioma hablado viene siempre con él.
  • El Anexo G —ceder material para mejorar el modelo— está publicado en /terms/model-improvement.
  • Marcar el audio sintético: obligación del Reglamento de IA con fecha del 2 de diciembre de 2026, en marcha.

13. Cómo comprobar cada cosa

Todo lo de arriba está medido contra el servidor desplegado. Si dudas de una línea, vuelve a medirla en vez de copiarla. Los guiones corren contra el servidor de verdad, se limpian solos y cuentan que no dejaron nada.

Lo que quieras comprobarCon qué
los tres interruptores, de punta a puntanothing_is_kept_unless_it_is_switched_on.py
el aviso de la salathe_room_is_told_it_is_being_translated.py
borrar la cuentaa_person_can_delete_their_own_account.py
las denunciasa_bad_translation_can_be_reported.py
subir y reanudara_long_upload_resumes_and_says_how_much_is_left.py

(En scripts/ del repositorio del backend.)