# Panel de aplicaciones externas

Cómo se crea, se configura y se elimina una aplicación externa: estado, claves, permisos, avisos, usuarios, facturación y métricas, con los endpoints del panel.

Desde la pantalla **Aplicaciones externas** das de alta y gobiernas cada
aplicación (propia o de terceros) que usa la API externa de UTILIA OS
con una clave de API o un cliente OAuth: creas la aplicación, gestionas
sus credenciales, le concedes permisos, configuras sus avisos a tu
servidor, consultas sus usuarios externos y vigilas cómo usa la API.

Todo esto se apoya en la **API de administración de integraciones**
(`/api/integrations/apps/...`), que se llama con la sesión de un usuario
(`Authorization: Bearer <token>`) y se protege con los permisos
`EXTERNAL_APPS_*`. No la confundas con la **API externa**
(`/api/external/v1/...`), que es la que usa la propia aplicación con su
cabecera `X-Api-Key` y que documentan el resto de páginas de esta
sección.

**Poner en marcha una aplicación externa**

1. **Crear la aplicación** (Aplicaciones externas): La clave de API y la clave de firma de los avisos se muestran una sola vez.
2. [**Conceder permisos**](https://os.utilia.ai/docs/developers/api/scopes) (Configuración › Ajustes): Solo los que vaya a usar. Los protegidos los concede un Super Administrador.
3. [**Configurar los avisos**](https://os.utilia.ai/docs/developers/webhooks/overview) (Configuración · Opcional): Dirección HTTPS y eventos suscritos, solo si tu servidor necesita enterarse al momento.
4. [**Activar la facturación**](https://os.utilia.ai/docs/developers/api/billing-guide) (Dinero › Facturación · Opcional): Solo si la aplicación va a facturar o cobrar.
5. [**Probar la integración**](https://os.utilia.ai/docs/developers/getting-started/testing-quickstart) (Pruebas · Opcional): Diagnóstico, prueba de lectura, avisos de ejemplo y facturas sin emitir.
6. [**Llamar a la API**](https://os.utilia.ai/docs/developers/getting-started/quickstart) (Tu servidor): Con la cabecera X-Api-Key.

## Dónde está

La lista de aplicaciones está en **Ajustes › Creadas por tu empresa**,
para cualquier organización, sea cual sea su perfil de trabajo.

Hace falta el aplicativo **Integraciones** en el plan y el permiso
«Ver aplicaciones externas». Cada tarjeta enseña la salud de la
aplicación en una línea y abre su ficha.

## La ficha de una aplicación

La ficha agrupa sus pantallas en seis pestañas. Las que tienen varias
pantallas enseñan debajo una segunda fila, más discreta, con cada una;
en el teléfono, las dos filas se desplazan de lado a lado. Las
direcciones no cambian: un enlace guardado a `/integrations/:id/webhooks`
sigue abriendo **Avisos a tu servidor**, ahora dentro de
**Configuración**.

**Aplicaciones externas · Resumen**

Pestañas: Resumen, Pruebas, Actividad, Dinero, Usuarios y Configuración.

1. Seis pestañas. La que tiene más de una pantalla abre debajo una segunda fila con ellas.
2. Estado: el veredicto de la integración en una línea, con el enlace a Pruebas.
3. Información y clave de API: persona responsable, prefijo de la clave, cupo por hora y último uso.

_Las direcciones de cada pantalla son las de siempre: solo cambia cómo se agrupan en la tira._

| Pestaña | Pantallas | Qué contiene |
| - | - | - |
| Resumen | Resumen | Cifras de uso (o de facturación, si la aplicación factura), el bloque **Estado**, la **Información** con la persona responsable, la **Clave de API**, los eventos de los que avisa y los últimos tickets. |
| Pruebas | Pruebas | Si la integración funciona y por qué no: estado, diagnóstico con quién lo arregla, recorrido, pruebas que se lanzan sin la clave, llamadas en directo y qué puede llamar. |
| Actividad | Métricas y Errores | Tráfico, latencia y errores de la API, y los errores que ha enviado la aplicación (ver [Pestaña de errores](https://os.utilia.ai/docs/developers/api/errors-tab)). |
| Dinero | Facturación, Facturas, Cobros, Suscripciones y Autorizaciones de cobro | El interruptor de facturación, el tope por factura, el envío por correo, la cuenta de cobro y la fila de capacidades; y lo que ha facturado, cobrado y autorizado la aplicación. |
| Usuarios | Usuarios | Usuarios finales de la aplicación y su ficha de cliente en el CRM. |
| Configuración | Ajustes, Avisos a tu servidor e Inicio de sesión | **Ajustes**, con sus pestañas General, Permisos (los permisos de la API), Chat, Widget y Avanzado (dominios autorizados, cuota de almacenamiento y tamaño máximo de cada archivo); la dirección, los eventos, la clave de firma y el historial de los avisos; y el cliente OAuth, para que tus usuarios entren con su cuenta de UTILIA OS. |

En **Dinero**, **Facturación** está siempre; las otras cuatro pantallas
solo aparecen si la aplicación factura de verdad. Si el plan de tu
organización no incluye el aplicativo o la capacidad de una pantalla, no
desaparece: se muestra con un candado que dice qué falta. A quien no
tiene permiso para ver ese dato no se le ofrece la pantalla, y una
pestaña sin ninguna pantalla visible no sale. **Pruebas**, la segunda
pestaña, está siempre a la vista: es lo que se usa antes de que la
aplicación funcione y lo primero que se abre cuando deja de funcionar.

## El estado de la aplicación

El bloque **Estado** del **Resumen** es una sola línea: el veredicto de
la integración en una palabra (**Funciona**, **Funciona en parte**,
**Revisar**, **No funciona**, **Sin uso**, **Desactivada** o **Sin
comprobar**), la frase del servidor con el porqué y un enlace a
**Pruebas**, que lo cuenta entero con la acción de cada cosa. Es el mismo diagnóstico que pinta **Pruebas**, que lee la
aplicación con su clave en `GET /external/v1/diagnostics` y que resume
el MCP, así que las pantallas no se contradicen. Si la comprobación no
llega, la línea lo dice con **Reintentar**: nunca lo presenta como un
fallo de la integración.

## La pestaña Pruebas

Dice si la integración funciona y, si no, por qué y quién lo arregla,
sin emitir nada real. Todo sale del diagnóstico del servidor: la pestaña
no calcula nada por su cuenta. Las pruebas las ejecuta el servidor en
nombre de la aplicación, con su plan y sus permisos de la API, así que el
panel nunca te pide la clave ni la guarda. Cómo se prueba una
integración paso a paso está en
[Probar tu integración](https://os.utilia.ai/docs/developers/getting-started/testing-quickstart),
y cada hallazgo, en
[Hallazgos del diagnóstico](https://os.utilia.ai/docs/developers/getting-started/testing).

**Aplicaciones externas · Pruebas**

1. Pruebas es la segunda pestaña de la ficha, justo después de Resumen.
2. Instrucciones para tu asistente de IA: el texto para el asistente de quien programa, sin la clave.
3. El estado: el veredicto en una palabra, desde cuándo, una sola acción y el pulso de las últimas 24 horas.
4. Diagnóstico: un área por fila, lo que falla arriba, con quién lo arregla y su acción en la misma fila.
5. Recorrido: los primeros pasos de la integración, marcados solos con lo que observa el servidor.
6. En directo: las llamadas de la aplicación según llegan, sin recargar la página.

_Debajo del diagnóstico y del recorrido siguen, a todo el ancho, Lanzar una prueba, En directo y Qué puede llamar._

### El estado y el pulso

Arriba, el veredicto en una palabra, la frase del servidor con el
porqué, desde cuándo y, si hace falta, **una sola acción**: un enlace que
lleva al sitio exacto y te devuelve a esta pestaña, o un diálogo en el
sitio (nombrar a la persona responsable, renovar la clave). Si no puedes
hacerla, en su lugar va la frase de quién puede. Al lado, el pulso de las
últimas 24 horas: una barra por hora, con lo rechazado en color. Mientras
la aplicación no ha llamado nunca, en el sitio del pulso aparece **Haz tu
primera llamada**, con el fragmento mínimo para hacerla con la clave en
`UTILIA_API_KEY`.

### Diagnóstico y recorrido

**Diagnóstico** es una lista con un área por fila: aplicación, persona
responsable, clave de API, permisos de la API, facturación (si la
aplicación factura), avisos a tu servidor (si los tiene) y llamadas. Lo
que falla sube; cada fila dice quién lo arregla, sin culpar a nadie, y
lleva su acción. **Conceder** un permiso solo se ofrece a quien puede
concederlo; ante llamadas rechazadas por falta de permiso, la acción es
**Revisar el permiso**, porque unas llamadas rechazadas no prueban que la
integración lo necesite.

**Recorrido** lista los primeros pasos según para qué sirve la
aplicación y los marca solo, con la fecha de su evidencia: el progreso
es el mismo para todo el equipo y en cualquier aparato. Cada paso
pendiente dice cómo se hace y enlaza a la guía. **Probar aquí** solo
aparece en el paso del receptor de avisos, el único que una prueba del
panel puede marcar: la factura de prueba y la prueba activa no pasan
por la clave de la aplicación.

### Lanzar una prueba

Un selector con las pruebas que tienen sentido para esta aplicación y
para ti, y debajo, el panel de la elegida:

- **Comprobar ahora**: una lectura mínima con cada permiso de lectura, como lo haría la aplicación, sin escribir nada ni devolver datos. Si la aplicación factura, valida además una factura de ejemplo sin emitirla; ese bloque solo lo ve quien puede ver facturas. Hasta 6 por minuto y persona, sin gastar el cupo de la clave.
- **Comprobar una petición**: pegas el método, la ruta y el cuerpo que manda tu programa y el servidor dice, sin ejecutarla, si pasaría y por qué no, con los campos que se descartarían resaltados. La ruta se autocompleta con lo que la aplicación puede llamar. El cuerpo no se guarda.
- **Aviso de prueba**: envía a la dirección guardada una muestra del evento que elijas, firmada igual que los avisos reales y marcada como prueba (`livemode: false` y `meta.test: true`).
- **Receptor de avisos**: tres avisos de prueba que tu servidor tiene que aceptar, el bueno, y rechazar, el de firma falsa y el caducado. El resultado queda guardado para el diagnóstico y el recorrido.
- **Factura sin emitir**: el editor de una factura de ejemplo con **Probar aquí**, que la valida sin reservar número, y el PDF de muestra sin validez fiscal. Si el correo saldría al contacto de facturación del cliente del CRM, solo lo ve entero quien puede ver todos los clientes; si no, enmascarado. El borrador se guarda en esa pestaña del navegador mientras dura la sesión, sin datos personales.

La prueba abierta queda en la dirección de la página, así que un enlace
pegado en el chat abre la misma prueba, y al volver de arreglar algo
aterrizas en ella. Con teclado, el atajo `Ctrl+Intro` (`⌘↩` en Mac) (Cmd +
Intro en Mac, Ctrl + Intro en Windows y Linux) lanza la prueba abierta
desde cualquier campo de su panel.

### En directo y qué puede llamar

**En directo** enseña las últimas 50 llamadas de la aplicación según
llegan, sin sondear: método, ruta, estado, tiempo, si usó la clave
anterior y, si falla, qué significa. Filtra entre **Todas** y
**Rechazadas**, y **Pausar** deja la lista quieta. Una llamada que el
programa cortó antes de recibir la respuesta sale con el estado `499`.
En el ordenador, desde una llamada, **Comprobar esta ruta** abre
**Comprobar una petición** ya rellena.

**Qué puede llamar** agrupa las rutas de la API por recurso, con cuántas
puede llamar la aplicación hoy. Al desplegar cada grupo, la ruta que no
se puede llamar dice por qué: el permiso que falta, con **Conceder** si
puedes concederlo; un aplicativo o una función que el plan no incluye,
con su candado; o «Próximamente», sin venta. En el ordenador, cada ruta
lleva además **Comprobar esta ruta**. Lo que la aplicación no tiene
concedido no es un problema: va al final, plegado.

### Quién puede hacer cada cosa

Todo exige el aplicativo **Integraciones** en el plan, también a un
Super Administrador. Lo que no puedes hacer no aparece: sin permiso
para enviar avisos no ves esas dos pruebas, y una aplicación que no
factura no enseña nada de facturación. Cada pieza llama a su ruta del
panel, bajo `/api/integrations/apps/:id`:

| Pieza | Ruta | Permiso |
| - | - | - |
| Estado, diagnóstico y recorrido | `GET /diagnosis` | `EXTERNAL_APPS_VIEW` |
| En directo y llamadas rechazadas | `GET /diagnosis/calls`, con `view=recent` (una a una, hasta 100) o `view=rejected` (agrupadas, por defecto) y `window` `24h` o `7d` | `EXTERNAL_APPS_VIEW` |
| Instrucciones para tu asistente de IA | `GET /diagnosis/brief` | `EXTERNAL_APPS_VIEW` |
| Comprobar una petición | `POST /diagnosis/check-request`, 30 por minuto | `EXTERNAL_APPS_VIEW` |
| Comprobar ahora | `POST /diagnosis/run`, 6 por minuto y persona | `EXTERNAL_APPS_VIEW`; la factura de ejemplo, además `INVOICES_VIEW` y Finanzas |
| Factura sin emitir | `POST /diagnosis/invoice-validate` (20 por minuto) y `/diagnosis/invoice-validate/pdf` (10 por minuto) | `EXTERNAL_APPS_VIEW` e `INVOICES_VIEW`, con Finanzas en el plan |
| Aviso de prueba | `POST /webhook/test`, con `event` | `EXTERNAL_APPS_EDIT` o `INTEGRATIONS_WEBHOOKS_MANAGE` |
| Receptor de avisos | `POST /diagnosis/webhook-receiver-check` | `EXTERNAL_APPS_EDIT` o `INTEGRATIONS_WEBHOOKS_MANAGE` |

Los dos avisos de prueba exigen además el aplicativo del evento y
comparten con la clave de la aplicación un cupo de 5 por minuto. Una
aplicación de otra organización, o eliminada, responde
`404 APP_NOT_FOUND`, igual que una que no existe.

En directo no sondea: escucha `GET /diagnosis/stream`, un canal en
tiempo real que solo avisa de que hay llamadas nuevas o de que cambió la
salud, sin datos, y el panel vuelve a pedir lo que necesita. Se abre con
un pase de corta duración (`POST /auth/sse-token`) en `?token=`, admite 5
conexiones a la vez por persona y el servidor lo cierra a los 15
minutos; el panel vuelve a conectarse solo, pasando otra vez por tu
sesión, tu permiso y el plan. Además, cada 5 minutos comprueba que
sigues teniendo `EXTERNAL_APPS_VIEW`, que la organización sigue teniendo
Integraciones y que la aplicación existe; si algo falla, lo cierra.

### En el teléfono y en el escritorio

- **Teléfono**, en la aplicación móvil o en el navegador: las pruebas son una lista y cada una se abre en una hoja inferior. Lo que necesita un editor, **Comprobar una petición** y **Factura sin emitir**, no se programa en el teléfono: se ofrece **Enviar al ordenador**, que abre la hoja de compartir del sistema con el enlace a esa misma prueba. **Instrucciones para tu asistente de IA** también se comparte en lugar de copiarse, y el detalle de una llamada de **En directo** se abre en una hoja inferior.
- **Aplicación de escritorio**: mientras **En directo** escucha y la ventana está en segundo plano, una notificación del sistema te avisa de la primera llamada de la aplicación y de cada fallo nuevo, otra ruta u otro código en esa sesión. Respeta tu ajuste de notificaciones de escritorio y se apaga con **Pausar**. El PDF de muestra se descarga con su aviso nativo y **Mostrar en la carpeta**.

Cuando una aplicación deja de funcionar (su salud pasa a **No
funciona**), el aviso que recibes en la aplicación y por correo lleva a
esta pestaña. Cada persona decide por dónde le llega en **Mi perfil › Avisos**,
fila **Integraciones**, bloque **Salud de las aplicaciones**, aviso **Una aplicación deja de funcionar**. Qué
hace cambiar la salud está en [Salud de la aplicación](https://os.utilia.ai/docs/developers/getting-started/app-health).

## La pantalla de Facturación

Aquí se decide qué puede hacer la aplicación con el dinero.

- **Activar la facturación**: permite a la aplicación emitir facturas y
  cobrar. Solo puede encenderla un Super Administrador, y al guardar se le
  pide un motivo de al menos veinte caracteres que queda en el registro de
  auditoría. Apagarla no exige nada. La pantalla y su interruptor piden
  **Integraciones** y **Finanzas**, no la pasarela: una organización sin
  Stripe activa aquí la facturación igual, emite, envía y anota el cobro
  por transferencia. La pasarela solo gobierna los cobros con tarjeta.
- **Tope por factura**: importe máximo que la aplicación puede facturar de una
  vez. Por encima, la API responde con un error de límite.
- **Enviar las facturas por correo al emitirlas**: cuando está activo, UTILIA OS
  manda cada factura al usuario con la plantilla y el PDF de la organización.
  Es lo que decide cuando la llamada no dice nada: `sendEmail` manda sobre
  él en los dos sentidos (`true` la envía aunque esté apagado y `false` la
  retiene aunque esté encendido). El justificante de cobro y los avisos de
  vencimiento y recordatorio dependen solo de este interruptor.
- **Cuenta de cobro**: los cobros se procesan con la **cuenta Stripe de tu
  organización**; no hay cuenta por aplicación. La pantalla muestra su estado
  y enlaza a Finanzas › Ajustes › **Pagos con tarjeta (Stripe)**.
- **Fila de capacidades**: factura puntual, cobro con tarjeta, suscripción y
  autorización de cobro, cada una con su estado real. Cuando algo no se puede,
  distingue las dos razones, porque se arreglan en sitios distintos: **no
  incluida en tu plan** y **permiso no concedido a la aplicación**.

## Autenticación y permisos

Todos los endpoints de la API de administración están bajo `/api/integrations/apps` y exigen una sesión válida (Bearer JWT), el aplicativo Integraciones en el plan y el permiso correspondiente. Los de facturación exigen además Finanzas, y los de cobros con tarjeta, también Pagos con tarjeta (Stripe).

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `EXTERNAL_APPS_VIEW` | `permiso` | Sí | Ver el listado, el detalle, las estadísticas, los usuarios, las métricas, las entregas de avisos, la facturación, el catálogo de permisos de la API y el diagnóstico, y lanzar las pruebas de solo lectura de la pestaña Pruebas. |
| `EXTERNAL_APPS_CREATE` | `permiso` | Sí | Crear una aplicación externa nueva. |
| `EXTERNAL_APPS_EDIT` | `permiso` | Sí | Editar la aplicación, renovar sus claves de API y de firma, cambiar sus permisos, configurar y probar avisos, gestionar OAuth y facturación, activarla o desactivarla. |
| `EXTERNAL_APPS_DELETE` | `permiso` | Sí | Eliminar una aplicación externa (se puede restaurar), ver las eliminadas y restaurarlas, y eliminarla definitivamente. |

Con `EXTERNAL_APPS_EDIT` cambias la mayoría de los permisos de la API,
pero los 17 protegidos (datos personales críticos, como las notas
internas de un contacto o el NIF de los usuarios; críticos financieros,
como cobrar, programar cobros, devolver dinero o emitir rectificativas;
y la gestión de agentes de IA del chat)
exigen además ser Super Administrador y un motivo de al menos veinte caracteres, que queda en
el registro de auditoría. El catálogo llega **marcado según el plan**:
solo se pueden conceder los permisos que cubre tu plan, y conceder uno
fuera de plan responde `400 SCOPE_OUT_OF_PLAN`. Lo ya concedido no se
borra al bajar de plan; simplemente deja de funcionar.

## Listado de aplicaciones

La página principal lista las aplicaciones externas de tu organización.

**`GET /api/integrations/apps`**

Lista las aplicaciones externas. Por defecto, las activas que no están eliminadas; con includeInactive=true, también las desactivadas, y con deleted=only, solo las eliminadas. Permiso: EXTERNAL_APPS_VIEW.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `includeInactive` | `query (boolean)` | No | Si es 'true', incluye también las aplicaciones desactivadas que no están eliminadas. Por defecto solo se devuelven las activas. |
| `deleted` | `query ('exclude' \| 'only')` | No | 'exclude', el valor por defecto, deja fuera las eliminadas. 'only' devuelve solo las eliminadas, la vista «Eliminadas» del panel. Cualquier otro valor responde 400 VALIDATION_ERROR. |

Cada elemento trae los recuentos de tickets y de usuarios externos (`_count`), el proyecto de Gestión de proyectos vinculado (`devProject`, si lo hay), `oauthEnabled`, que indica si la aplicación tiene un cliente OAuth activo, y `deletedAt`, la fecha en que se eliminó o `null`. En la vista de eliminadas llega también quién la eliminó (`deletedBy`).

Trae además la salud de cada aplicación, la misma línea que pinta su tarjeta, sin recalcular nada:

- **`health`**: `status` (`unknown`, `idle`, `ok`, `attention`, `failing` o `paused`, los estados de [la salud de la aplicación](https://os.utilia.ai/docs/developers/getting-started/app-health)), `since` (desde cuándo está así, o `null` si nunca se ha evaluado) y `title`, el nombre corto del estado. Una aplicación desactivada sale como `paused` aunque el evaluador aún no haya pasado. En la vista de eliminadas llega a `null`.
- **`credentialAdvisoryCounts`**: cuántas recomendaciones de seguridad hay abiertas sobre sus credenciales, en `renew` (renovar la clave) y `updateSdk` (actualizar el SDK). El motivo de cada una solo se ve en la ficha.

## Crear una aplicación externa

El alta crea en una sola operación la aplicación, su clave de API y la clave de firma de sus avisos.

En el panel, la pantalla **Nueva aplicación externa** empieza por **¿Para qué la usarás?**, con tres opciones: **Tickets y soporte**, **Facturación y cobros** y **Datos del CRM**. Puedes marcar varias o ninguna, y la respuesta no se guarda en la aplicación: solo decide qué más pregunta el formulario y qué permisos te propone al terminar. Toda aplicación nace sin permisos; con usos marcados, la pantalla final te propone juntos los de todos ellos, y sin ninguno te lleva a la pestaña de permisos para que elijas tú. Los ajustes de tickets (cómo se comunica tu equipo con el usuario, la IA y el **Proyecto vinculado**, que es opcional) solo aparecen, y solo se envían, si marcas **Tickets y soporte**; si no, la aplicación nace con sus valores por defecto. Las opciones de un aplicativo que tu plan no incluye salen con un candado.

**`POST /api/integrations/apps`**

Crea una aplicación externa con su clave de API y la clave de firma de sus avisos. Permiso: EXTERNAL_APPS_CREATE.

1. **Define la identidad de la aplicación**

   `name` y `slug` son obligatorios. El `slug` solo admite minúsculas, números y guiones (`^[a-z0-9-]+$`) y tiene que ser único en tu organización. Si quieres, añade `description`, `logoUrl` y `websiteUrl`.
2. **Ajusta la conversación en los dos sentidos y la IA**

   Campos opcionales con valores por defecto: `allowBidirectional` (por defecto `true`), `showAgentNames` (por defecto `false`), `customAgentName`, `aiTranscriptionEnabled` (por defecto `true`) y `aiSuggestionsEnabled` (por defecto `true`).
3. **Configura orígenes y límites de archivos**

   `allowedOrigins` es la lista de orígenes CORS permitidos. `maxFileSize` (entre 1 KB y 100 MB) y `maxStorageBytes` (entre 1 MB y 10 GB) limitan la subida de archivos. Con `devProjectId` vinculas la aplicación a un proyecto de Gestión de proyectos.
4. **Guarda la clave de API y la clave de firma**

   La respuesta devuelve `apiKeyPlainText` y `webhookSecretPlainText`. Es la **única vez** que viajan en claro: cópialas y guárdalas en un sitio seguro. Después solo se mostrará el prefijo de la clave.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `name` | `string (máx. 100)` | Sí | Nombre visible de la aplicación. |
| `slug` | `string (máx. 50)` | Sí | Identificador único en tu organización. Solo minúsculas, números y guiones. |
| `description` | `string` | No | Descripción de la aplicación. |
| `logoUrl` | `url` | No | URL del logotipo. |
| `websiteUrl` | `url` | No | URL del sitio web de la aplicación. |
| `allowBidirectional` | `boolean` | No | Permite la conversación en los dos sentidos. Por defecto true. |
| `showAgentNames` | `boolean` | No | Muestra los nombres reales de las personas de soporte. Por defecto false. |
| `customAgentName` | `string (máx. 100)` | No | Nombre genérico que se muestra cuando no se enseñan los reales. |
| `aiTranscriptionEnabled` | `boolean` | No | Habilita la transcripción por IA. Por defecto true. |
| `aiSuggestionsEnabled` | `boolean` | No | Habilita las sugerencias por IA. Por defecto true. |
| `allowedOrigins` | `string[]` | No | Orígenes CORS permitidos. No se admite el comodín '\*' suelto. |
| `devProjectId` | `string \| null` | No | Proyecto de Gestión de proyectos al que se vincula la app. |
| `maxFileSize` | `int (bytes)` | No | Tamaño máximo por archivo. Entre 1024 (1 KB) y 104857600 (100 MB). |
| `maxStorageBytes` | `int (bytes)` | No | Almacenamiento total máximo. Entre 1048576 (1 MB) y 10737418240 (10 GB). |

El servidor rechaza `*` como valor de `allowedOrigins`. Se admiten comodines de subdominio (por ejemplo, `https://*.cliente.com`), pero nunca el comodín suelto, que abriría la aplicación a peticiones desde cualquier origen.

Con dominios autorizados, la clave de la aplicación va en páginas web y
solo admite los permisos que se pueden usar desde un navegador. Toda
aplicación nace sin permisos, así que el alta con dominios pasa; después,
añadir dominios a una aplicación con otros permisos, o concederle uno de
fuera, responde `400 SCOPE_NOT_ALLOWED_WITH_ALLOWED_ORIGINS`. Por qué
y cómo separar la integración en dos aplicaciones está en
[Claves dentro de una página web](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#claves-dentro-de-una-página-web).

## Detalle y estadísticas

**`GET /api/integrations/apps/:id`**

Detalle de la aplicación, con su clave de API (solo prefijo y datos de uso), el proyecto vinculado y el cliente OAuth si existe. Permiso: EXTERNAL_APPS_VIEW.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/stats`**

Estadísticas de la aplicación: tickets totales, abiertos y resueltos, usuarios externos, desglose por categoría y prioridad, actividad reciente y resumen de errores. Permiso: EXTERNAL_APPS_VIEW.

Parámetro de ruta: `id`.

Ninguna lectura devuelve credenciales. De la clave de API llegan el `keyPrefix`, el `rateLimit`, el `usageCount`, la fecha de último uso y si está activa, nunca su huella. De la clave de firma de los avisos llega solo `webhookSecretConfigured`, un booleano que dice si hay clave puesta: ni el valor en claro ni su versión cifrada viajan en el listado ni en el detalle. Para volver a tener una credencial completa hay que renovarla.

El detalle trae además la **persona responsable** de la aplicación y, si UTILIA OS recomienda renovar alguna de sus claves, esas recomendaciones (`credentialAdvisories`). El motivo de cada recomendación solo lo ve quien gestiona esa clave.

## Configuración de la aplicación

La pantalla **Configuración › Ajustes** cubre la edición general, la renovación de la clave de API y la activación o desactivación. La clave de firma se renueva en **Configuración › Avisos a tu servidor**.

**`PATCH /api/integrations/apps/:id`**

Actualiza la aplicación. Acepta los mismos campos que el alta, todos opcionales. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/regenerate-api-key`**

Renueva la clave de API y devuelve la nueva en claro una sola vez (apiKeyPlainText). Con graceDays, la anterior sigue valiendo esos días. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_APPS_MANAGE.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/api-key/retire-previous`**

La clave de API anterior deja de valer en el acto y se cierran las conexiones abiertas con ella. Sin transición en curso, 409 NO_TRANSITION_IN_PROGRESS. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_APPS_MANAGE.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/regenerate-webhook-secret`**

Renueva la clave de firma de los avisos y devuelve la nueva en claro una sola vez (webhookSecret). Con graceDays, cada aviso lleva también la firma de la anterior esos días. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_WEBHOOKS_MANAGE.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/webhook-secret/cutover`**

Mi servidor ya usa la nueva: desde ahora X-Utilia-Signature va firmada con la clave nueva y la anterior sigue en X-Utilia-Webhook-Signatures hasta el final de la transición. Sin transición, 409 NO_TRANSITION_IN_PROGRESS. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_WEBHOOKS_MANAGE.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/webhook-secret/retire-previous`**

UTILIA OS deja de firmar con la clave anterior en el acto. Sin transición, 409 NO_TRANSITION_IN_PROGRESS. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_WEBHOOKS_MANAGE.

Parámetro de ruta: `id`.

El cuerpo de las dos renovaciones admite:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `graceDays` | `0 \| 1 \| 7 \| 30` | No | Días que la clave actual sigue valiendo junto a la nueva: 1, 7 o 30, las opciones de la ficha. Con 0, el valor por defecto, deja de valer en el acto. Tiene que ser un número JSON. |
| `twoFactorCode` | `string` | No | Código de la aplicación de autenticación o de recuperación, si la persona tiene la verificación en dos pasos. |
| `twoFactorApprovalId` | `string` | No | Alternativa al código: la confirmación aprobada desde el móvil para esta aplicación, este tipo de clave y estos mismos graceDays. |

Las renovaciones no se encadenan: durante una transición, `graceDays` mayor que 0 responde `409 ROTATION_IN_PROGRESS`, y `graceDays: 0` hace que la actual y la anterior dejen de valer en el acto. La respuesta trae `credentialKind`, `graceDays`, `previousValidUntil` (hasta cuándo vale la anterior, o `null`) y `rotatedAt`, además de la clave nueva. Mientras la clave de API anterior sigue valiendo, las respuestas a las peticiones que la usan llevan la cabecera `X-Utilia-Api-Key-Expires` con esa fecha.

**`PATCH /api/integrations/apps/:id/active`**

Deja la aplicación activa o desactivada según el campo active del cuerpo (true o false). Repetir la misma petición no cambia nada. Desactivada, su clave responde 403 APP_DISABLED. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

**`PATCH /api/integrations/apps/:id/toggle-active`**

La forma antigua: cambia al estado contrario del que tenga. Se conserva por compatibilidad; dos peticiones seguidas la dejan como estaba, así que usa PATCH :id/active. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

Desactivar no es eliminar: la aplicación sigue en la lista y se vuelve a activar con el mismo interruptor. Para quitarla de en medio, mira [Eliminar una aplicación](#eliminar-una-aplicación).

> **Atención: Renovar una clave, con o sin transición**
>
> Al renovar la clave de API o la clave de firma eliges cuántos días sigue valiendo la anterior: 1, 7 o 30. Con «Deja de valer en el acto» (`graceDays: 0`, el valor por defecto en la API), la aplicación recibirá `401` (clave de API) o fallará al comprobar la firma (avisos) hasta que ponga la nueva. Desde la ficha, la opción recomendada es 7 días.

### Renovar una clave desde la ficha

La clave de API se renueva en **Configuración › Ajustes** de la ficha y la clave de firma, en **Configuración › Avisos a tu servidor**. El diálogo es el mismo para las dos:

1. **Elige cuánto sigue valiendo la actual**

   **1 día**, **7 días (recomendado)**, **30 días** o **Deja de valer en el acto**. Debajo ves hasta qué día y hora valen las dos.
2. **Confirma que eres tú**

   Si tienes la verificación en dos pasos, con el código de tu aplicación de autenticación o uno de recuperación. Confirmarlo desde el móvil todavía no está en la aplicación de iPhone de la App Store.
3. **Copia la clave nueva**

   Solo se muestra esta vez. El diálogo no se cierra hasta que pulsas **Ya la he guardado**.

Durante el periodo de transición no se puede volver a renovar la misma clave: primero hay que retirar la anterior.

- **Clave de API**: la tarjeta enseña los primeros caracteres de la anterior, hasta cuándo vale y su último uso. Si no se ha usado desde que la renovaste, o hace más de 24 horas, te dice que ya puedes retirarla; si se usó hace menos de una hora, que algún servidor tuyo todavía la usa. **Retirar la anterior** la deja sin valor en el acto, antes de su fecha.
- **Clave de firma**: mientras tu servidor sigue con la anterior, la tarjeta ofrece **Probar la clave nueva**, **Mi servidor ya usa la nueva** y **Retirar la anterior**. La prueba envía primero un aviso con una firma falsa y después otro firmado solo con la clave nueva, y dice qué contestó tu servidor a cada uno. El plegable **Qué lleva cada aviso y cómo comprobarlo** explica las dos cabeceras de firma; el detalle está en [Firma HMAC de los avisos](https://os.utilia.ai/docs/developers/webhooks/hmac-signature#cuando-renuevas-la-clave-de-firma).

Quienes gestionan esa clave reciben un aviso cuando otra persona la renueva o retira la anterior antes de su fecha, con la consecuencia para su integración ([Avisos de seguridad](https://os.utilia.ai/docs/guides/security/security-alerts#los-avisos-de-las-claves-de-aplicaciones-externas)). Las claves de una aplicación también aparecen en Ajustes › Claves de API, pero allí solo se ven: se gestionan desde la ficha de la aplicación.

### Persona responsable

Lo que la aplicación crea o cambia por la API queda a nombre de su persona responsable, que tiene que estar activa: si deja de estarlo y nadie la sustituye, todas las peticiones de la aplicación se rechazan con `403 APP_RESPONSIBLE_INACTIVE`.

Lo que la aplicación puede hacer lo deciden sus permisos de la API, no los permisos de esa persona. Los de la persona solo cuentan al emitir rectificativas, al gestionar suscripciones y cobros programados (si le falta el permiso, `403 APP_RESPONSIBLE_PERMISSION_DENIED`) y en las escrituras del chat. Por eso, con el permiso de la API, una aplicación lee y escribe los clientes, contactos y leads de toda la organización aunque su responsable solo vea los suyos. El detalle está en [La persona responsable](https://os.utilia.ai/docs/developers/getting-started/authentication#la-persona-responsable).

**`GET /api/integrations/apps/:id/responsible-candidates`**

Personas que se pueden nombrar. A un Super Administrador le lista las personas activas de su organización (mode: any); al resto, solo a sí mismo (mode: self). Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_APPS_MANAGE.

Parámetro de ruta: `id`.

**`PATCH /api/integrations/apps/:id/responsible`**

Nombra la persona responsable. Solo un Super Administrador nombra a otra persona (si no, 403 RESPONSIBLE_SELF_ONLY); la persona tiene que estar activa en la organización (si no, 409 RESPONSIBLE_NOT_ELIGIBLE). Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_APPS_MANAGE.

Parámetro de ruta: `id`.

En la ficha, el bloque **Información** del **Resumen** enseña quién es la persona responsable. Un Super Administrador elige a cualquiera con **Elegir responsable**; el resto solo puede **Nombrarme responsable**, y la ficha lo explica: «Solo un Super Administrador puede nombrar responsable a otra persona». La persona nombrada, la anterior y quienes gestionan la aplicación reciben un aviso.

### Recomendaciones de renovar una clave

Si UTILIA OS recomienda renovar una clave de la aplicación, la ficha lo muestra en una banda con el plazo recomendado y el botón para renovarla. Quien gestiona esa clave puede descartar la recomendación con un motivo:

**`POST /api/integrations/apps/:id/credential-advisories/:advisoryId/dismiss`**

Descarta la recomendación con un motivo de 20 a 500 caracteres, que queda en el registro de actividad y se envía a las demás personas que gestionan esa clave. No se puede deshacer. Las de actualizar el SDK no se descartan (409 CREDENTIAL_ADVISORY_NOT_DISMISSIBLE).

Parámetros de ruta: `id`, `advisoryId`.

### Permisos de la API

Los permisos de la API (scopes) deciden qué puede leer y hacer la aplicación con la API externa. La pestaña **Permisos** de **Configuración › Ajustes** se pinta con el catálogo que sirve el servidor, así que los textos, los distintivos y la exigencia de Super Administrador siempre coinciden con lo que el servidor aplica.

**`GET /api/integrations/apps/scopes/catalog`**

Catálogo de permisos de la API, con los que no cubre el plan marcados como outOfPlan, y con etiqueta, descripción, tipo y marcas de protección. Permiso: EXTERNAL_APPS_VIEW.

**`PATCH /api/integrations/apps/:id/scopes`**

Sustituye la lista completa de permisos de la aplicación. El servidor calcula qué cambia y lo deja en el registro de auditoría. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

Cada entrada del catálogo incluye:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `value` | `string` | Sí | Nombre técnico del permiso (por ejemplo, 'crm:contacts:read'). |
| `label` | `string` | Sí | Etiqueta corta para la interfaz. |
| `description` | `string` | Sí | Explicación del permiso. |
| `category` | `'basic' \| 'write' \| 'sensitive' \| 'rgpd-critical' \| 'financial-critical'` | Sí | Tipo del permiso. |
| `rgpdSensitive` | `boolean` | Sí | Si da acceso a datos personales en sentido amplio. |
| `rgpdCritical` | `boolean` | Sí | Si es un dato personal crítico. |
| `requiresSuperAdmin` | `boolean` | Sí | Si concederlo exige ser Super Administrador y dar un motivo. |
| `requiredModules / requiredCapabilities` | `string[]` | Sí | Aplicativos y capacidades del plan que necesita para funcionar. |
| `outOfPlan` | `boolean` | No | Si tu plan no lo cubre. |
| `publishableAllowed` | `boolean` | Sí | Si lo puede tener una aplicación cuya clave va en páginas web. Ver Claves dentro de una página web, en Límites de uso y CORS. |
| `conditionalRequirements` | `objeto[]` | Sí | Lo que pide el plan para una ruta o una opción concreta del permiso, con satisfied si tu organización ya lo tiene. Vacío si no hay. |

Sobre la actualización (`PATCH .../scopes`):

- `scopes` lleva la **lista completa** que debe quedar, no solo los cambios. El servidor calcula `added` y `removed`.
- `reason` (texto, hasta 500 caracteres) es obligatorio, con al menos 20, cuando se añade un permiso protegido.
- `expectedUpdatedAt` (ISO 8601) activa el bloqueo optimista: si la aplicación cambió después, la respuesta es `409`.
- Retirar un permiso **revoca** los tokens OAuth activos que lo incluían. La clave de firma no se rota.

Respuestas de error específicas de este endpoint:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `400 INVALID_SCOPE` | `error` | No | La lista scopes contiene un valor que no está en el catálogo. |
| `403 SUPER_ADMIN_REQUIRED_FOR_RGPD_SCOPE` | `error` | No | Hace falta ser Super Administrador para añadir un permiso protegido. |
| `400 SCOPE_OUT_OF_PLAN` | `error` | No | El plan de tu organización no cubre ese permiso. |
| `400 SCOPE_NOT_ALLOWED_WITH_ALLOWED_ORIGINS` | `error` | No | La clave de la aplicación está publicada en páginas web y el permiso no se puede usar con ella. notAllowedScopes dice cuáles. |
| `403 RESPONSIBLE_SELF_ONLY` | `error` | No | Ampliar los permisos solo lo puede hacer la persona responsable de la aplicación o un Super Administrador. Quitarlos, cualquiera con EXTERNAL_APPS_EDIT. |
| `404 EXTERNAL_APP_NOT_FOUND` | `error` | No | La aplicación no existe en tu organización. |
| `409 EXTERNAL_APP_SCOPES_CONFLICT` | `error` | No | Bloqueo optimista: expectedUpdatedAt no coincide. También 409 EXTERNAL_APP_REVOKED si la aplicación está desactivada. |
| `422 REASON_REQUIRED_FOR_RGPD_SCOPE` | `error` | No | Falta el motivo (mínimo 20 caracteres) al añadir un permiso protegido. |

El catálogo completo por áreas y sus reglas están en [Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes).

## Avisos a tu servidor

Cada aplicación puede recibir avisos (webhooks) cuando pasa algo en UTILIA OS. Desde el panel configuras la dirección de destino y los eventos, pruebas la entrega y consultas el historial.

**`PATCH /api/integrations/apps/:id/webhook`**

Configura los avisos: dirección de destino, activación y eventos suscritos. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `webhookUrl` | `url \| null` | No | Dirección HTTPS de destino. null para borrarla. |
| `webhookEnabled` | `boolean` | Sí | Activa o desactiva la entrega de avisos. |
| `webhookEvents` | `string[]` | Sí | Eventos a los que se suscribe la aplicación. Solo recibe los que declare aquí. |

**`POST /api/integrations/apps/:id/webhook/test`**

Envía un aviso de prueba a la dirección configurada, después de comprobar que no apunta a una red privada. Con secret igual a current, prueba la clave nueva; con event, envía una muestra de ese evento. Permiso: EXTERNAL_APPS_EDIT o INTEGRATIONS_WEBHOOKS_MANAGE.

Parámetro de ruta: `id`.

El cuerpo es opcional y decide qué prueba se hace:

- **Sin cuerpo**, o con `{"event": "WEBHOOK_TEST"}`: **Probar la conexión**, un aviso de prueba firmado igual que los de verdad.
- **`{"secret": "current"}`**: **Probar la clave nueva**. Primero un aviso con una firma falsa, que tu servidor debería rechazar, y después otro firmado solo con la clave vigente. La respuesta trae el resultado de los dos y un veredicto (`verdict`): si tu servidor acepta el de la firma falsa, el mensaje lo dice, porque significa que no está comprobando la firma.
- **`{"event": "INVOICE_CREATED"}`** (o cualquier otro evento del catálogo que el plan de tu organización permita suscribir): una **muestra** de ese evento, firmada igual que los avisos reales, con `livemode: false`, `meta.test: true` e identificadores inventados que llevan `test`. Las de los eventos de facturación, de métodos de pago (`PAYMENT_METHOD_ADDED` y `PAYMENT_METHOD_REMOVED`) y de salud (`APP_HEALTH_CHANGED`) tienen la forma exacta de los reales. La respuesta añade el evento, si la aplicación está suscrita a él (`subscribed`), si la muestra es exacta o genérica (`sampleShape`), el cuerpo enviado (`payload`) y el principio de la respuesta de tu servidor (`responseSnippet`). Es lo que usa la pestaña **Pruebas**.

Si llegan `secret` y `event`, manda `secret`. Ninguna prueba queda en el historial de entregas ni se reintenta, y cada una espera la respuesta de tu servidor como mucho 10 segundos (la entrega real espera 30).

La prueba responde `400 WEBHOOK_NO_URL` si la aplicación no tiene dirección configurada, `400 WEBHOOK_URL_INVALID` si la dirección es una IP privada o se resuelve a una red privada (protección frente a SSRF y DNS rebinding), `400 VALIDATION_ERROR` si `event` no está en el catálogo, `403 MODULE_NOT_ENABLED` si el plan no incluye el aplicativo del evento y, al probar la clave nueva, `409 WEBHOOK_SECRET_NOT_CONFIGURED` si no hay clave. Con los avisos desactivados no sale nada: el resultado lo dice con `success: false` (al probar la clave nueva, con un `409`). Si tu servidor responde con un código de error, cuenta como intento válido y el código se devuelve en el resultado.

### Historial de entregas

**`GET /api/integrations/apps/:id/webhook-deliveries`**

Historial paginado de entregas de avisos, con filtro por estado y por evento. Permiso: EXTERNAL_APPS_VIEW.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/webhook-deliveries/:deliveryId`**

Detalle de una entrega, con el contenido enviado y la respuesta de tu servidor. Permiso: EXTERNAL_APPS_VIEW.

Parámetros de ruta: `id`, `deliveryId`.

**`POST /api/integrations/apps/:id/webhook-deliveries/:deliveryId/retry`**

Reintenta una entrega a mano. Responde 204 No Content. Permiso: EXTERNAL_APPS_EDIT.

Parámetros de ruta: `id`, `deliveryId`.

Filtros admitidos en el listado:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `status` | `WebhookDeliveryStatus` | No | Filtra por estado de la entrega. |
| `event` | `string` | No | Filtra por nombre de evento. |
| `page` | `int (≥1)` | No | Página. Por defecto 1. |
| `limit` | `int (1-100)` | No | Tamaño de página. Por defecto 20. |

El detalle de cada entrega trae `event`, `status`, `statusCode`, `errorMessage`, `attemptCount`, `payload` (el JSON enviado), `responseBody`, `lastAttemptAt`, `nextRetryAt` y `completedAt`.

Hay eventos de tickets, usuarios, archivos, errores, presupuestos, facturas, métodos de pago, autorizaciones de cobro, suscripciones, cobros, contactos, leads, clientes y chat. Cada uno tiene su descripción, su versión de contenido y su forma de envío (a todas las aplicaciones suscritas o solo a la implicada). El catálogo completo está en [Avisos a tu servidor](https://os.utilia.ai/docs/developers/webhooks/overview).

## Usuarios externos

Lista los usuarios finales que la aplicación ha identificado.

**`GET /api/integrations/apps/:id/users`**

Lista paginada de usuarios externos de la aplicación. Permiso: EXTERNAL_APPS_VIEW.

Parámetro de ruta: `id`.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `page` | `query (int)` | No | Página. Por defecto 1. |
| `limit` | `query (int)` | No | Tamaño de página. Por defecto 20. |

Para unir un usuario de la aplicación con una ficha de cliente que ya
existía en el CRM (por ejemplo, creada por tu equipo), el panel usa
`POST /api/integrations/apps/:id/users/:userId/link-client`. Exige los
permisos `EXTERNAL_APPS_EDIT` y `CLIENTS_VIEW`, el CRM en el plan y
acceso a esa ficha; es la única forma de enlazar con una ficha nativa,
que la API externa nunca toca (ver [Usuarios externos](https://os.utilia.ai/docs/developers/api/users)).

## Métricas de la API

La sección Métricas resume cómo usa la aplicación la API externa. Todas las métricas admiten `period` con los valores `24h`, `7d`, `30d` o `90d` (por defecto, `24h`).

**`GET /api/integrations/apps/:id/metrics`**

Resumen: total de peticiones, tiempo de respuesta medio, tasa de error, veces que se alcanzó el límite y variación frente al periodo anterior.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/metrics/requests`**

Peticiones a lo largo del periodo.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/metrics/latency`**

Latencia a lo largo del periodo: p50, p95, p99 y media.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/metrics/errors`**

Errores por código de estado HTTP y tipo de error.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/metrics/endpoints`**

Endpoints más usados, con latencia media y tasa de error.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/metrics/webhooks`**

Entregas de avisos correctas y fallidas, con desglose por evento.

Parámetro de ruta: `id`.

El resumen (`/metrics`) devuelve:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `totalRequests` | `number` | Sí | Total de peticiones en el periodo. |
| `avgResponseTimeMs` | `number` | Sí | Tiempo de respuesta medio en milisegundos. |
| `errorRate` | `number` | Sí | Tasa de error en porcentaje. |
| `rateLimitHits` | `number` | Sí | Veces que se alcanzó el límite de peticiones. |
| `trends` | `object` | Sí | Variación de requests, responseTime, errorRate y rateLimitHits frente al periodo anterior. |
| `period` | `string` | Sí | Periodo consultado. |

## OAuth

Además de la clave de API, una aplicación puede tener un cliente OAuth 2.0 para actuar en nombre de una persona que la autoriza.

**`POST /api/integrations/apps/:id/enable-oauth`**

Activa OAuth para la aplicación. Exige al menos una redirect_uri. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

**`PATCH /api/integrations/apps/:id/oauth`**

Actualiza la configuración OAuth de la aplicación. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/oauth`**

Datos del cliente OAuth de la aplicación. Permiso: EXTERNAL_APPS_VIEW.

Parámetro de ruta: `id`.

**`DELETE /api/integrations/apps/:id/disable-oauth`**

Desactiva OAuth en la aplicación. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

Parámetros principales al habilitar OAuth:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `redirect_uris` | `url[]` | Sí | Direcciones de redirección autorizadas del cliente OAuth. |
| `allowed_scopes` | `string` | No | Scopes que puede pedir el cliente OAuth. |
| `token_endpoint_auth_method` | `'none' \| 'client_secret_basic' \| 'client_secret_post'` | No | Método de autenticación en el endpoint de token. |
| `tos_uri` | `url` | No | Dirección de las condiciones del servicio. |
| `policy_uri` | `url` | No | Dirección de la política de privacidad. |
| `contact_email` | `email` | No | Correo de contacto del cliente OAuth. |

Las marcas `is_first_party` (aplicación propia, que se salta la pantalla
de consentimiento) y `verified` (insignia de aplicación verificada en esa
pantalla) solo puede ponerlas UTILIA OS. Si las envías a `true`, la
respuesta es `403`; a `false` se ignoran y no cambian nada.

El flujo de autorización, el intercambio de tokens y la pantalla de consentimiento están en [Iniciar sesión con UTILIA](https://os.utilia.ai/docs/developers/oauth/overview).

## Facturación de la aplicación

Si la aplicación emite facturas o cobra por la API, el panel permite consultarlo y configurarlo. Estos endpoints exigen además Finanzas en el plan, y los de cobros, también Pagos con tarjeta (Stripe).

**`GET /api/integrations/apps/:id/invoices`**

Facturas emitidas por la aplicación (origen EXTERNAL_APP), con filtros por usuario externo, estado y fechas. Permisos: EXTERNAL_APPS_VIEW e INVOICES_VIEW.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/invoices/stats`**

Totales de las facturas emitidas por la aplicación. Permisos: EXTERNAL_APPS_VIEW e INVOICES_VIEW.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/invoices/:invoiceId/issuance-review`**

Da por revisada una factura de la aplicación cuya emisión se interrumpió antes de terminar su cierre fiscal (issuanceDeliveryStatus: review_required). Es el botón «Dar por revisada» de la ficha de la factura. Permisos: EXTERNAL_APPS_EDIT e INVOICES_ISSUE.

Parámetros de ruta: `id`, `invoiceId`.

La revisión solo se acepta si la factura tiene número y su PDF original
(y su código QR, si lo lleva) está generado y verificado; si no, o si la
emisión sigue en curso, responde `409 INVOICE_STATE_INVALID`. No reemite
ni envía nada: deja la emisión en `complete`, queda en el registro de
auditoría con quien la hizo y responde `{"status": "complete"}`, también
si ya estaba completa. Desde ese momento la aplicación puede rectificar o
anular la factura. Ver [Estado de la emisión](https://os.utilia.ai/docs/developers/api/invoices#estado-de-la-emisión).

**`GET /api/integrations/apps/:id/payments`**

Cobros (PaymentIntents) procesados por la aplicación sobre sus facturas. Permisos: EXTERNAL_APPS_VIEW y PAYMENTS_VIEW.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/payments/stats`**

Totales de los cobros procesados por la aplicación. Permisos: EXTERNAL_APPS_VIEW y PAYMENTS_VIEW.

Parámetro de ruta: `id`.

**`GET /api/integrations/apps/:id/billing-config`**

Configuración de facturación de la aplicación, con sus series por defecto (defaultStandardSeriesId y defaultSimplifiedSeriesId) y las series activas reservadas a ella (availableSeries). Permiso: EXTERNAL_APPS_VIEW.

Parámetro de ruta: `id`.

**`PATCH /api/integrations/apps/:id/billing-config`**

Activa o desactiva la facturación por API, fija el tope por factura en céntimos, decide si se envían por correo al emitirlas y fija las series por defecto de la aplicación. Activarla exige ser Super Administrador y un motivo. Permiso: EXTERNAL_APPS_EDIT.

Parámetro de ruta: `id`.

### Series por defecto de la aplicación

Una aplicación puede numerar sus facturas en una serie propia (por
ejemplo, `APP-202600001`), separada de la de las facturas del equipo. La
serie se crea en [Ajustes › Organización](https://os.utilia.ai/dashboard/settings/organization),
pestaña **Facturas**, bloque **Series de facturación**, con el permiso
«Editar organización», eligiendo la aplicación a la que se reserva.
Después, en la pantalla **Facturación** de la aplicación (o con
`PATCH :id/billing-config`), se fija la serie por defecto:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `defaultStandardSeriesId` | `string \| null` | No | Serie de las facturas ordinarias que la aplicación emite sin seriesCode: una serie de ordinarias, activa y reservada a esta aplicación. null vuelve a la serie general de siempre. |
| `defaultSimplifiedSeriesId` | `string \| null` | No | Serie de las simplificadas que la aplicación emite sin seriesCode: una serie de simplificadas, activa y reservada a esta aplicación. null vuelve a la de siempre (T-). |
| `availableSeries` | `array` | No | Solo en la lectura (GET): las series activas reservadas a la aplicación, con id, code, prefix, documentType y description. Son las únicas que se pueden fijar por defecto. |

Una serie que no es de la organización, de otra aplicación o del equipo
responde `400 INVOICE_SERIES_NOT_AVAILABLE`; una de otro tipo,
`400 INVOICE_SERIES_TYPE_MISMATCH`, y una desactivada,
`409 INVOICE_SERIES_INACTIVE`. El cambio queda en el registro de
auditoría con el antes y el después. Fijar la serie no exige ser Super
Administrador: no amplía lo que la aplicación puede hacer, solo dónde se
numera. Mientras sea la serie por defecto de una aplicación, la serie no
se puede desactivar (`409 INVOICE_SERIES_IN_USE`): primero hay que
cambiar la de la aplicación. La aplicación ve sus series con
[`GET /external/v1/invoices/series`](https://os.utilia.ai/docs/developers/api/invoices#series-disponibles),
pero la clave de API no puede crearlas ni cambiarlas.

El recorrido completo, de la primera factura al cobro recurrente, está en [Facturar y cobrar desde tu aplicación](https://os.utilia.ai/docs/developers/api/billing-guide).

## Eliminar una aplicación

Hay dos formas de quitar una aplicación, las dos con el permiso «Eliminar aplicaciones externas» (`EXTERNAL_APPS_DELETE`) y las dos al final del menú de su ficha, en rojo y separadas del resto:

**Dos formas de eliminar**

### Eliminar (Se puede deshacer)

Deja de funcionar en el acto y desaparece de la lista, pero no se borra nada. Se restaura desde **Eliminadas** y vuelve desactivada.

### Eliminar definitivamente (No se puede deshacer)

Borra la aplicación y lo que solo es suyo. Lo que la ley obliga a guardar se queda, ya sin el vínculo. Se confirma escribiendo su nombre.

**Estados de una aplicación externa**

1. **Activa**. Su clave funciona y recibe avisos.
   - **Eliminada**. Se llega desde Activa cuando: DELETE :id. No funciona ni sale en la lista; se conserva todo.
     - **Eliminada definitivamente** (estado final). Se llega desde Eliminada cuando: POST :id/permanent-delete. Ya no existe; lo que obliga la ley se queda.
2. **Desactivada**. Se llega desde Activa cuando: PATCH :id/active. Sigue en la lista; su clave responde 403 APP_DISABLED.

Otras transiciones:

- Desactivada → Activa: PATCH :id/active la vuelve a activar.
- Desactivada → Eliminada: DELETE :id también elimina una desactivada.
- Eliminada → Desactivada: POST :id/restore la devuelve, siempre desactivada.
- Activa → Eliminada definitivamente: POST :id/permanent-delete no exige eliminarla antes.

_El borrado definitivo vale para una aplicación activa, desactivada o eliminada._

### Eliminar

**`DELETE /api/integrations/apps/:id`**

Elimina la aplicación sin borrar nada: deja de funcionar, sale de la lista y se puede restaurar. Responde la aplicación con deletedAt. Permiso: EXTERNAL_APPS_DELETE.

Parámetro de ruta: `id`.

Al eliminarla, en el acto:

- su clave de API deja de valer y la API externa responde `401 API_KEY_INVALID`, y se cierran las conexiones abiertas con ella;
- deja de recibir avisos nuevos en tu servidor;
- se apagan su inicio de sesión con OAuth y su ficha del catálogo de aplicaciones, si la tenía;
- su identificador (`slug`) queda reservado, porque sus tickets lo llevan en el código: dar de alta o renombrar otra aplicación con él responde `409 EXTERNAL_APP_SLUG_HELD_BY_DELETED`.

No se puede eliminar mientras cobra: con suscripciones activas o en pausa o con cobros programados pendientes, la respuesta es `409 EXTERNAL_APP_HAS_ACTIVE_BILLING`, con `details.activeSubscriptions` y `details.pendingScheduledCharges`. Primero hay que dar de baja esas suscripciones y cancelar esos cobros. Una aplicación de otra organización o que ya estaba eliminada responde `404`.

### Ver las eliminadas y restaurar

En el listado del panel, la pestaña **Eliminadas** (solo para quien puede eliminar) enseña cada aplicación eliminada con la fecha y dos botones, **Restaurar** y **Eliminar definitivamente**. Por la API es `GET /api/integrations/apps?deleted=only`.

**`POST /api/integrations/apps/:id/restore`**

Restaura una aplicación eliminada. Vuelve desactivada (isActive: false, deletedAt: null). 404 si no está eliminada o no es de tu organización. Permiso: EXTERNAL_APPS_DELETE.

Parámetro de ruta: `id`.

Al restaurarla vuelven su clave de API y su ficha del catálogo, pero la aplicación queda **desactivada**: hasta que alguien la active (`PATCH :id/active` con `{"active": true}`), su clave responde `403 APP_DISABLED`. El inicio de sesión con OAuth no se enciende solo, para que no vuelvan a valer los tokens que había antes de eliminarla: si la aplicación lo usaba, vuelve a activarlo en **Configuración › Inicio de sesión**.

### Eliminar definitivamente

Antes de borrar, el panel te enseña qué se borra, qué se conserva y qué lo impide:

**`GET /api/integrations/apps/:id/permanent-deletion-preview`**

Qué impide eliminarla definitivamente (blockers), qué se borra (willDelete) y qué se conserva (willKeep), con canDelete. Vale para una aplicación activa, desactivada o eliminada. Permiso: EXTERNAL_APPS_DELETE.

Parámetro de ruta: `id`.

**`POST /api/integrations/apps/:id/permanent-delete`**

Borra la aplicación para siempre. Exige confirmName con el nombre exacto. Responde deleted: true y appId. Permiso: EXTERNAL_APPS_DELETE.

Parámetro de ruta: `id`.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `confirmName` | `string (1 a 200)` | Sí | El nombre de la aplicación, escrito tal cual para confirmar. Se compara sin los espacios del principio y del final; si no coincide, 400 CONFIRMATION_MISMATCH. |

**Se borra**: la aplicación, sus usuarios (y sus accesos a facturas y tarjetas cuando no tienen ficha de cliente), los mensajes que publicó en el chat (las respuestas de las personas se quedan), sus accesos a canales, el historial de avisos enviados, sus métricas de uso, su cliente OAuth con sus tokens y su clave de API.

**Se conserva**, sin el vínculo con la aplicación: las facturas y las rectificativas, las suscripciones, las autorizaciones de cobro, los tickets, las fichas del CRM que creó, los archivos que subió y sus series de facturación, que quedan desactivadas con su código y su numeración.

Si algo lo impide, la respuesta es `409 EXTERNAL_APP_PERMANENT_DELETE_BLOCKED` y `details.blockers` trae cada motivo con su `code`, su `message` y, si se sabe, su `count`:

| code | Qué lo impide y cómo se resuelve |
| - | - |
| `ACTIVE_SUBSCRIPTIONS` | Suscripciones activas o en pausa de la aplicación, o que facturan con una de sus series. Dalas de baja. |
| `PENDING_SCHEDULED_CHARGES` | Cobros programados pendientes de sus facturas o suscripciones. Cancélalos. |
| `CATALOG_INSTALLS_IN_OTHER_ORGS` | Está o ha estado en el catálogo de aplicaciones y otras organizaciones pueden tenerla instalada. Si solo quieres que deje de funcionar, elimínala sin más. |
| `PAYMENT_METHODS_IN_USE_WITHOUT_CLIENT` | Tarjetas de usuarios de la aplicación sin ficha de cliente que usa una autorización o un cobro programado. Vincula a esos usuarios con su ficha de cliente. |

> **Importante: Eliminar definitivamente no tiene vuelta atrás**
>
> No pide la verificación en dos pasos: la confirmación es escribir el nombre. Antes de hacerlo, revisa la previsualización, y si solo quieres que la aplicación deje de funcionar, elimínala sin más: se conserva todo y se restaura cuando quieras.

### Por MCP

La herramienta `integrations` hace lo mismo con `apps_delete`, `apps_restore` y `apps_delete_permanently`, con el permiso `EXTERNAL_APPS_DELETE` y el scope `integrations:apps:manage`. Las tres piden `confirm: true`, y el borrado definitivo, además, `confirmName`. Las eliminadas se listan con `apps_list` y `deletedOnly: true`. Detalle en la [referencia MCP](https://os.utilia.ai/docs/developers/mcp/reference#integrations).

## Errores frecuentes

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `401` | `error` | No | Falta la sesión o no es válida (Bearer JWT). |
| `403` | `error` | No | Falta el permiso EXTERNAL_APPS\_\*, hace falta ser Super Administrador para un permiso protegido o el plan no incluye el aplicativo. |
| `400 CONFIRMATION_MISMATCH` | `error` | No | Al eliminarla definitivamente, confirmName no es el nombre exacto de la aplicación. |
| `404` | `error` | No | La aplicación no existe en tu organización o, para casi todo salvo restaurarla y eliminarla definitivamente, está eliminada. |
| `409` | `error` | No | Slug repetido al crear o reservado por una aplicación eliminada, conflicto de bloqueo optimista en los permisos, aplicación desactivada, o algo que impide eliminarla (EXTERNAL_APP_HAS_ACTIVE_BILLING) o eliminarla definitivamente (EXTERNAL_APP_PERMANENT_DELETE_BLOCKED). |

Todo cambio sensible (permisos, credenciales, facturación) queda en el registro de auditoría. Las métricas y el historial de entregas son el primer sitio donde mirar ante latencia, errores o avisos que no llegan.

## Siguiente paso

- [Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes): Catálogo completo por áreas y reglas de concesión.
- [Avisos a tu servidor](https://os.utilia.ai/docs/developers/webhooks/overview): Catálogo de eventos, contenido y firma.
- [Probar y diagnosticar](https://os.utilia.ai/docs/developers/getting-started/testing): El diagnóstico, la prueba de lectura y la salud de tu integración.
- [Iniciar sesión con UTILIA](https://os.utilia.ai/docs/developers/oauth/overview): OAuth para actuar en nombre de una persona.
