# Cambios de las integraciones

Historial de cambios de la API externa, los SDK, MCP y el widget de UTILIA OS, con los cambios incompatibles destacados.

Este es el registro de cambios de todo lo que usa tu integración: la API externa, los SDK de JavaScript y Python, las herramientas MCP, OAuth y el widget. Los cambios que te obligan a tocar tu código o a conceder un permiso nuevo llevan la marca **CAMBIO QUE ROMPE** o **Cambio incompatible**. Las versiones vigentes están al final de la página.

Si vienes de una versión anterior, estos son los cambios de octubre que te obligan a actuar. El detalle de cada uno está en su fecha, más abajo.

**Cambios de octubre que exigen tocar tu integración**

1. **5 oct**: Enlaces a la documentación. create_share_token exige el ámbito dev:docs:share.
2. **9 oct**: Introspección OAuth. Solo para clientes confidenciales, y solo sobre sus propios tokens.
3. **12 oct**: Aviso BUDGET_VIEWED. view\.ipAddress va siempre a null; la IP enmascarada llega en view\.ipAddressMasked.
4. **13 oct**: SDK 4.2.0 y responsable inactivo. Responsable inactivo: 403 APP_RESPONSIBLE_INACTIVE; SignerAuditEvent pierde ipAddress y userAgent.
5. **14 oct**: Cinco apartados en «Próximamente». Tesorería, informes salvo el resumen, comisiones, impuestos y contabilidad dejan de estar disponibles por MCP hasta su validación.
6. **15 oct**: Presupuestos y fecha de expedición. convert_to_project exige crm:projects:create; convert_to_invoice y el cobro recurrente desde un presupuesto, Finanzas; issueDate ya no puede ser futura ni anterior a la última factura de la serie.
7. **19 oct**: Estado de la emisión de facturas. review_required ya no avisa de un correo o un cobro fallidos: el correo se lee en emailDelivery.
8. **21 oct**: Permisos de la API y SDK 4.6.0. El NIF de los usuarios pide su permiso, reintentar un cobro pide «Programar cobros» y una clave en una página solo admite los permisos del navegador. En el SDK 4.6.0, un 409 llega como CONFLICT.
9. **22 oct**: Respuesta del envío de errores. Un error con un 4xx en el contexto responde accepted: false con reason: discarded_expected, en vez de parecer registrado con el hash vacío.
10. **22 oct**: Buscar por NIF. El NIF de clientes, proveedores y documentos digitalizados solo empareja con el permiso de datos fiscales y, por MCP, con su ámbito sensible.

_Los de antes de octubre están en la lista de debajo._

Antes de octubre:

- [27 de mayo](#2026-05-27): se firma `timestamp.rawBody`; los receptores que firmaban solo el cuerpo rechazan las entregas.
- [2 de septiembre](#2026-09-02): `dev:write` y `crm:write` dejan de borrar archivos por MCP; hace falta un ámbito propio.
- [3 de septiembre](#2026-09-03): `portal_access` exige `crm:portal:view` y `crm:portal:manage`.
- [5 de septiembre](#2026-09-05): SDK 3.0.0; `identify` exige `crm:users:write` y desaparece el periodo de gracia de los permisos.
- [7 de septiembre](#2026-09-07): SDK 4.0.0; se retira el correo al proveedor y cobrar exige Pagos con tarjeta (Stripe) en el plan.

## 2026-10-23

### Catálogo de aplicaciones: la suscripción de las aplicaciones de pago

- `GET /external/v1/app-installations` y `GET /external/v1/app-installations/:id` traen `subscription` en las instalaciones de una ficha «De pago en UTILIA»: plan, precio sin impuestos, estado, prueba, periodo, cancelación y cambio programado, sin impuestos, tarjeta, facturas ni datos fiscales de la empresa. `null` en las gratuitas. Filtro nuevo `subscriptionStatus`. Ver [La suscripción de una aplicación de pago](https://os.utilia.ai/docs/developers/apps-platform/installations-api#la-suscripción-de-una-aplicación-de-pago).
- Doce avisos nuevos, que llegan solos a las aplicaciones de pago del catálogo, firmados con la clave de la instalación: `APP_SUBSCRIPTION_STARTED`, `_TRIAL_ENDING`, `_RENEWED`, `_CHANGED`, `_PRICE_CHANGE_SCHEDULED`, `_PAST_DUE`, `_PAUSED`, `_RESUMED`, `_CANCELED`, `_REACTIVATED`, `_REFUNDED` y `_ENDED`. El catálogo de avisos pasa a **133**. Ver [Los avisos de la suscripción](https://os.utilia.ai/docs/developers/apps-platform/webhooks#los-avisos-de-la-suscripción).
- Mientras UTILIA tenga suspendida una aplicación en una empresa, los avisos de su suscripción siguen llegando; `APP_SUBSCRIPTION_ENDED` y `APP_SUBSCRIPTION_PAUSED` llegan aunque la empresa haya perdido Integraciones.

### MCP: la suscripción en las instaladas

- `installations_list` e `installations_get` traen `subscription` en las aplicaciones de pago; el importe, solo con el permiso de ver el plan de la empresa (`SETTINGS_ORGANIZATION_VIEW`).
- `installations_cancel_subscription`, con `confirm: true`: cancela la suscripción de una aplicación de pago al final del periodo pagado o de la prueba. Lo puede hacer quien instala aplicaciones o quien gestiona el plan (`INTEGRATIONS_APPS_INSTALL` o `SETTINGS_ORGANIZATION_EDIT`), con el ámbito `integrations:apps:manage`. Comprar, cambiar de plan, reactivar o pagar no se hace por MCP.
- Salen `listings_list` y `listings_get`: las fichas que publica una cuenta de desarrollador se gestionan en el servidor de desarrolladores. La herramienta `integrations` pasa de 62 a **61** operaciones.

### SDK 4.8.0 (sin publicar)

`subscription`, el filtro `subscriptionStatus`, `isAccessAllowed` (`is_access_allowed`) y los tipos de los avisos `APP_SUBSCRIPTION_*`. Ver [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-480).

### Las aplicaciones de la empresa, en Ajustes

- En el panel, las aplicaciones de una empresa viven ahora en Ajustes: **Ajustes › Aplicaciones** (con **Instaladas** y **Explorar**) y **Ajustes › Creadas por tu empresa**. Desaparece el área aparte de Integraciones.
- Las direcciones cambian: la ficha del catálogo es `/dashboard/settings/apps/explore/<slug>`, el detalle de una instalada `/dashboard/settings/apps/installed/<id>` y la ficha de una aplicación propia `/dashboard/settings/custom-apps/<id>`. Los campos `url` y `href` de las herramientas MCP del catálogo y de las instaladas ya traen las nuevas.
- Las direcciones de antes (`/dashboard/integrations/...`) siguen funcionando: llevan a las nuevas conservando la consulta. El botón «Instalar en UTILIA OS» de tu web no tiene que cambiar, aunque conviene que use la dirección nueva.

## 2026-10-22

### Catálogo de aplicaciones: instalaciones y credenciales por empresa

Una aplicación del catálogo recibe en su servidor unas credenciales propias de cada empresa que la instala. Todo está en la sección [Plataforma de aplicaciones](https://os.utilia.ai/docs/developers/apps-platform/overview).

- Rutas nuevas, con la clave de la aplicación que publica la ficha: `POST /external/v1/app-installations/exchange` (canjea el código de una instalación por su clave de API y su clave de firma, una sola vez y solo desde un servidor), `GET /external/v1/app-installations` y `GET /external/v1/app-installations/:id`. Ver [Referencia de las instalaciones](https://os.utilia.ai/docs/developers/apps-platform/installations-api).
- Dos permisos de la API nuevos, de la aplicación que publica la ficha: `apps:installations:read` y `apps:installations:exchange`. Con `analytics:errors:manage` (más abajo), el catálogo pasa de 58 a **61** permisos.
- Códigos de error nuevos: `EXCHANGE_FROM_BROWSER`, `INSTALLATION_CODE_WRONG_APP`, `INSTALLATION_CODE_INVALID`, `INSTALLATION_CODE_USED`, `INSTALLATION_CODE_EXPIRED` y `APP_INSTALLATION_NOT_FOUND`, cada uno con su apartado en [Errores y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#instalaciones-del-catálogo).
- Avisos nuevos, que llegan solos a las aplicaciones del catálogo: `APP_INSTALLED`, `APP_UNINSTALLED`, `APP_ACCESS_CHANGED`, `APP_SUSPENDED` y `APP_REINSTATED`. Los avisos de una instalación llevan `installation` en el sobre y la cabecera `X-Utilia-Installation-Id`, y se firman con la clave de esa instalación. En las aplicaciones de siempre el sobre no cambia. Ver [Avisos de cada instalación](https://os.utilia.ai/docs/developers/apps-platform/webhooks).

### Widget: verificación de identidad

- El servidor del integrador firma el `userId` de cada persona con el **secreto del widget** (HMAC-SHA256 en hexadecimal) y el widget manda la firma en la cabecera `X-Utilia-User-Signature`. Tres modos por aplicación: **Desactivada**, **Solo avisar** y **Obligatoria**. Ver [Verificar la identidad en el widget](https://os.utilia.ai/docs/developers/widget/identity-verification).
- **Cambio incompatible para las aplicaciones nuevas:** nacen en **Obligatoria**. Sin la firma, las peticiones del widget con un `userId` responden `403 WIDGET_IDENTITY_REQUIRED`, `WIDGET_IDENTITY_INVALID` o `WIDGET_IDENTITY_SECRET_MISSING`. Las aplicaciones anteriores quedan en **Desactivada**.
- Hallazgos nuevos del diagnóstico: [`widget-identity-not-verified`](https://os.utilia.ai/docs/developers/getting-started/testing#widget-identity-not-verified) y [`widget-identity-secret-missing`](https://os.utilia.ai/docs/developers/getting-started/testing#widget-identity-secret-missing).
- Widget 1.2.0 (sin publicar): la opción `userSignature` y los atributos `data-user-signature` y `user-signature`.

### MCP: el catálogo y el widget, en `integrations`

La herramienta `integrations` gana doce operaciones: `apps_widget_identity_get`, `apps_widget_identity_update` y `apps_widget_identity_regenerate_secret`; `catalog_list`, `catalog_get` y `catalog_request_install`; `installations_list`, `installations_get`, `installations_uninstall` e `installations_change_responsible`; y `listings_list` y `listings_get`. Instalar no se hace por MCP. Con las nueve `apps_errors_*` (más abajo), pasa de 41 a **62**. Ver la [referencia MCP](https://os.utilia.ai/docs/developers/mcp/reference#integrations).

### SDK 4.7.0 (sin publicar)

`appInstallations.exchange`, `list` y `get` (`app_installations` en Python), sobre todo lo de la 4.6.0 (más abajo). Ver [Referencia de métodos](https://os.utilia.ai/docs/developers/sdk/reference#appinstallations).

### API externa: el registro de errores dice qué pasa con cada error

- **Respuesta veraz en `POST /external/v1/errors/report`**: `accepted`, `status` (`created`, `updated` o `regressed`) o `reason` (`rate_limited`, `quota_exhausted`, `overloaded` o `discarded_expected`), y `count`. Si UTILIA OS no puede guardar el error, `503 ERROR_STORAGE_UNAVAILABLE` con `Retry-After: 30`, el único caso que se repite.
- **Cambio de comportamiento**: un error con un 4xx en el contexto se descartaba, pero respondía igual que uno registrado, con el `hash` vacío. Ahora responde `accepted: false` con `reason: discarded_expected`. Para registrar un 4xx que sí importa, ponlo en `context.upstreamStatus`.
- **`POST /external/v1/errors/batch`**: de 1 a 25 errores, con el resultado de cada uno por su posición. El que no se pudo guardar trae `reason: storage_failed` y se repite solo ese.
- **Campos nuevos al enviar**: `release`, `environment`, `fingerprint` (con `"{{default}}"` para combinar la agrupación propia con la automática) y `tags`. `POST /external/v1/tickets/client-errors` admite `release` y `environment`, y responde `received`, `accepted` y `discarded` cuando ha procesado el lote.
- **Consultar**: `GET /external/v1/errors/:hash` con la pila, el contexto saneado y lo vinculado. El listado gana `status`, `silencedUntil`, `environment`, `firstRelease`, `lastRelease`, `regressionCount` y `lastRegressedAt`, y los filtros `status` y `firstRelease`; `severity` admite un valor, varios separados por comas o el parámetro repetido. Las estadísticas ganan `silenced`.
- **Gestionar, con el permiso nuevo `analytics:errors:manage`**: `POST /:hash/resolve`, `/reopen`, `/silence` (de 1 a 672 horas) y `DELETE /:hash/silence`. Actúan en nombre de la persona responsable de la aplicación, que necesita el permiso de gestionar los errores de las aplicaciones externas. Con los dos de las instalaciones del catálogo (más arriba), el catálogo pasa de 58 a **61** permisos. Ver [Consultar y gestionar los errores](https://os.utilia.ai/docs/developers/api/errors-manage).
- **Regresiones**: un error resuelto que vuelve a ocurrir se reabre solo. `ERROR_REOPENED` gana `reason` (`manual` o `regression`) y `release`; `ERROR_RESOLVED`, `reason` y `release`; `ERROR_REPORTED`, `release`, `environment` y `count`. Cerrar el ticket o la tarea vinculados también resuelve el error.
- **Límites**: las rutas de errores tienen su propio límite, 120 peticiones por minuto en cada una, y las de reporte (`/errors/report`, `/errors/batch` y `/tickets/client-errors`) ya no gastan el cupo por hora de la clave (por eso no llevan las cabeceras `X-Utilia-Quota-*`); las de consulta y gestión sí, como el resto de la API. Por error, 100 ocurrencias por minuto (las que pasan se suman al contador); por aplicación, 200 errores distintos al día; cada grupo se conserva 14 días desde su última ocurrencia. El contexto admite 64 KB y se guarda saneado y recortado a 16 KB.
- **Agrupación**: la gravedad ya no parte un grupo (guarda la más alta), cuenta el primer marco de la pila que es de tu código y se normalizan mejor las fechas, las IP y las direcciones. Los grupos que ya existían siguen recibiendo sus ocurrencias con el mismo `hash`.

Todo está en [Capturar los errores de tu aplicación](https://os.utilia.ai/docs/developers/sdk/error-reporting) y [Enviar los errores de tu aplicación](https://os.utilia.ai/docs/developers/api/errors).

### API externa: filtros de varios valores en leads, contactos y clientes

- **Corregido**: los filtros de varios valores de los listados (`status`, `stage`, `priority`, `leadType` y `temperature` en leads; `status` en contactos; `status`, `type` y `size` en clientes) respondían `400` con los valores separados por comas, que es como los mandan los SDK y como lo explica esta documentación. Ahora valen las comas (`?status=NEW,CONTACTED`), el parámetro repetido y la forma `?status[]=NEW` de axios, que antes se ignoraba sin avisar. Un valor que no existe sigue respondiendo `400`.

### MCP: los errores de las aplicaciones, en `integrations`

La herramienta `integrations` gana nueve operaciones `apps_errors_*` (con las doce del catálogo y del widget, más arriba, pasa de 41 a **62**): `apps_errors_list`, `apps_errors_get` y `apps_errors_stats` (de una aplicación o de todas), `apps_errors_resolve`, `apps_errors_reopen`, `apps_errors_silence`, `apps_errors_unsilence`, `apps_errors_bulk` y `apps_errors_link`, que crea el ticket o la tarea. Piden los mismos permisos que la pestaña Errores (ver los errores, o gestionarlos) y dos scopes OAuth nuevos, `integrations:apps:errors:view` e `integrations:apps:errors:manage`, incluidos en `integrations:full`. Ver [Referencia de herramientas MCP](https://os.utilia.ai/docs/developers/mcp/reference#integrations).

### MCP: lo eliminado en el gestor de archivos se puede restaurar

- **Cambio de comportamiento**: `delete` y `batch_delete` de `fm_files_manage`, `delete` de `fm_folders_manage` y quitar un archivo de su última carpeta (`remove_from_folder`) ya no destruyen nada: lo mandan 30 días a «Eliminados recientemente» y responden `outcome: "TRASHED"` con `purgeAfter`. `physicallyDeleted` llega siempre a `false`.
- **Operaciones nuevas**: `fm_files: list_trash`, `fm_files_manage: restore` y `fm_files_manage: purge` (con `confirm: true`), con un elemento o un lote de hasta 50. «Eliminar todo» no se publica por MCP.
- **Permiso nuevo** `FILE_MANAGER_MANAGE_TRASH` («Gestionar lo que eliminan otros»): ver, restaurar y borrar lo que eliminaron otras personas, dentro de su alcance. Lo propio no lo necesita.

Ver [Gestor de archivos: Eliminados recientemente](https://os.utilia.ai/docs/developers/mcp/reference#gestor-de-archivos-eliminados-recientemente). La API externa no cambia: sigue sin borrar archivos.

### SDK 4.6.0 (JavaScript y Python)

La **4.6.0** de los dos SDK, sin publicar, suma la captura de errores en dos líneas:

- `errors.install`, `capture` (`capture_exception`), `captureMessage` (`capture_message`) y `flush`: capturan lo que nadie trata y lo que atrapas, nunca lanzan y envían en lotes que respetan `Retry-After` (`retryAfterMs`, `retry_after`).
- `errors.reportBatch` (`report_batch`), `get`, `resolve`, `reopen`, `silence` y `unsilence`.
- Corregido en JavaScript: `errors.list` con varias gravedades enviaba `severity[]=` y el servidor ignoraba el filtro.
- El widget sigue en la 1.1.0.

El detalle, con la tabla de lo que cambia si vienes de la 4.5, está en [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-460).

### Buscador global (`search`)

- **Relevancia y respuesta.** Ordena por parecido con el nombre, entiende palabras vacías, formas societarias, códigos sin ceros y teléfonos con cualquier formato, y repite tolerando erratas si no encuentra nada (`meta.approximate`). `meta` trae además `hasMoreByType`, `moreTypes`, `intentTypes` y `appliedFilters`, y cada resultado puede llevar `stateLabel` («Borrador», «Vencida»). `meta.totalMatches` cuenta las filas leídas, no las que existen: para saber si hay más, usa `hasMoreByType`. Detalle en [Buscador global y avisos por MCP](https://os.utilia.ai/docs/developers/mcp/search-and-notifications).
- **NIF.** Un NIF, NIE o CIF encuentra su ficha sin importar los separadores ni el «ES» del NIF-IVA, pero solo donde puedes leerlo: en facturas y gastos, con el permiso del documento; en clientes, proveedores y documentos digitalizados, con el permiso de datos fiscales y, en el token, `crm:clients:view-sensitive` o `finance:suppliers:view-sensitive`. Nunca viaja en el resultado.
- **Leads ganados.** Salen marcados (`inactive: true`, `inactiveLabel: "Ganado"`), por detrás del cliente en el que continúan. Los archivados siguen sin salir.
- **Borradores.** Presupuestos, facturas, gastos, pedidos, recepciones, albaranes, campañas, automatizaciones, formularios de captación y cursos en borrador llevan `stateLabel: "Borrador"` y ya no salen como inactivos.
- **«Incidencias».** Nombra los tickets de soporte; los elementos de trabajo se nombran con «issues».

### Listados: el NIF solo empareja con el permiso de datos fiscales

**Cambio incompatible.** Buscar o filtrar por el NIF confirmaba que existía una ficha con ese NIF aunque la respuesta lo ocultara. Desde hoy:

- **Documentos digitalizados** (`GET /api/finance/payments` y `finance_payments`: `list`, `stats`, `export` y descarga por filtros): `supplierNif` y la búsqueda por NIF solo cuentan con `SUPPLIERS_VIEW_SENSITIVE` y, por MCP, con `finance:suppliers:view-sensitive`. Sin ellos, el filtro `supplierNif` se ignora, igual que `userId` sin `PAYMENTS_VIEW_ALL`.
- **Clientes** (`GET /api/crm/clients` y `crm_clients: list`): la búsqueda por NIF exige `CLIENTS_VIEW_SENSITIVE` y, por MCP, `crm:clients:view-sensitive`.
- **Proveedores** (`suppliers: list`): por MCP, la búsqueda por NIF exige además `finance:suppliers:view-sensitive`; por REST ya exigía `SUPPLIERS_VIEW_SENSITIVE`.

Los gastos no cambian: su NIF va impreso en la factura recibida y se busca con el permiso del gasto.

## 2026-10-21

### API externa: comprobar tu integración sin preguntar a nadie

Rutas nuevas con la clave de tu aplicación (`X-Api-Key`), sin ningún permiso de la API y sin exigir Integraciones en el plan, porque su trabajo es explicar los bloqueos:

- **`GET /external/v1/me`**: la aplicación, la organización (solo su nombre y su estado), la clave con la que llamas, tu cupo por hora y lo que te queda, y tus scopes.
- **`GET /external/v1/diagnostics`**: qué puedes y qué no puedes hacer, por qué, quién lo arregla y el siguiente paso. Con `?format=markdown`, las instrucciones para tu asistente de IA, sin la clave.
- **`GET /external/v1/diagnostics/calls`**: tus llamadas rechazadas agrupadas por ruta, estado y `errorCode`, en las últimas 24 horas o 7 días. Desde este cambio quedan registradas también las que rechazan el plan y el origen.
- **`POST /external/v1/diagnostics/check-request`**: dice si una petición pasaría, sin ejecutarla: ruta, plan, permiso, facturación, cuerpo y campos que se descartarían.
- **`POST /external/v1/diagnostics/run`**: una lectura mínima por cada permiso de lectura y la validación en seco de una factura de ejemplo, sin devolver datos ni escribir nada.
- **`GET /external/v1/diagnostics/openapi.json`**: el contrato OpenAPI con solo lo que tu clave puede llamar hoy.
- **`POST /external/v1/diagnostics/webhook-sample`** y **`webhook-receiver-check`**, con Integraciones en el plan: una muestra firmada de un evento y la comprobación de que tu servidor acepta la firma buena y rechaza la falsa y la caducada.

Cada ruta admite hasta 30 llamadas por minuto y aplicación, y todas cuentan en el cupo por hora de la clave. La prueba activa tiene además su propio tope de 6 por minuto, y los avisos de prueba, de 5 por minuto entre la clave y el panel: por encima, `429 DIAGNOSTICS_RATE_LIMITED` con `Retry-After`. Todo está en [Rutas del diagnóstico](https://os.utilia.ai/docs/developers/getting-started/diagnostics) y en [Integrar con un asistente de IA](https://os.utilia.ai/docs/developers/getting-started/ai-assistants).

### API externa: errores que dicen qué hacer

- **`fixedBy` y `docsUrl`** en el cuerpo de todo error de `/api/external/v1`: quién lo arregla (`developer`, `organization` o `utilia`) y el apartado de la documentación que lo explica. Son aditivos.
- **El 403 `INSUFFICIENT_SCOPE` nombra el permiso que falta**, en el mensaje y en `requiredScopes`. Antes no lo decía.
- **`Retry-After` real.** El 429 del cupo por hora de la clave lleva los segundos que de verdad le quedan a la ventana, en lugar de 3600 fijos. Todas las respuestas traen además `X-Utilia-Quota-Limit`, `X-Utilia-Quota-Remaining` y `X-Utilia-Quota-Reset` (en segundos UNIX) con ese cupo; las `X-RateLimit-*` siguen siendo las del freno por minuto.
- **`X-Utilia-Ignored-Fields`** en las escrituras: los campos del cuerpo que la ruta no admite y que se descartan sin error, casi siempre un nombre mal escrito. Solo los nombres, hasta 20.
- **Códigos nuevos donde antes había otro o ninguno**: `API_KEY_WRONG_KIND` (una clave de la organización `utk_` contra la API externa, antes `API_KEY_INVALID` o `API_KEY_REQUIRED`), `API_KEY_WRONG_HEADER` (la clave de la aplicación en `Authorization` en lugar de `X-Api-Key`, antes `API_KEY_REQUIRED`), `AUTH_TOKEN_INVALID` (el 401 de las rutas del panel, antes sin código y en inglés), `BUDGET_NOT_FOUND`, `BUDGET_SIGNATURE_NOT_FOUND` y `BUDGET_COMMENT_VISIBILITY_NOT_ALLOWED` (antes 404 y 403 sin código), e `INVOICE_NUMBERING_UNAVAILABLE`, `INVOICE_ISSUANCE_REVIEW_REQUIRED` y `RECTIFICATION_OPERATION_INCOMPLETE` (antes 500 sin código, ahora con `retryable`).

Cada código está en [Errores, identificador de petición y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries).

### Avisos: tarjetas a punto de caducar y salud de tu aplicación

- **`PAYMENT_METHOD_EXPIRING` se emite.** Estaba en el catálogo, con su contenido documentado, pero no salía nunca. Llega a la aplicación dueña al entrar en la ventana de 30 días antes de que caduque la tarjeta y otra vez en la de 7, solo para las tarjetas que respaldan una suscripción con cobro automático o un cobro programado. Ver [PAYMENT_METHOD_EXPIRING](https://os.utilia.ai/docs/developers/webhooks/events#payment_method_expiring).
- **Aviso nuevo `APP_HEALTH_CHANGED`**: llega cuando cambia el estado de salud de tu aplicación, que UTILIA OS vigila sola con su tráfico real y sus cambios de configuración. No se envía en la primera comprobación ni al pasar a desactivada; al reactivar la aplicación, el primer aviso trae `previousStatus: paused`. Ver [Salud de tu aplicación](https://os.utilia.ai/docs/developers/webhooks/events#salud-de-tu-aplicación).

### Pruebas: claves publicadas, cupos y permisos

- **Claves publicadas en una web.** Con la clave de una aplicación con dominios autorizados, `/me` y el diagnóstico llegan recortados, con `credential.publishable: true`, y las llamadas rechazadas, la prueba activa y los avisos de prueba responden `403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY`: se lanzan desde la pestaña **Pruebas** del panel. Ver [Claves publicadas en una web](https://os.utilia.ai/docs/developers/getting-started/diagnostics#claves-publicadas-en-una-web).
- **Cupos que no se saltan.** Si su contador no responde, la prueba activa y los avisos de prueba responden `503 DIAGNOSTICS_UNAVAILABLE`, que se puede repetir, en vez de lanzarse sin contar.
- **Rechazos por permiso: un aviso.** `SCOPE_MISSING_FOR_OBSERVED_CALLS` deja de bloquear, solo cuenta la última hora y no lleva la salud a **No funciona**. El veredicto distingue «Funciona en parte» de «Revisar».
- **Recorrido.** La primera llamada correcta cuenta cualquier respuesta correcta con la clave, también `/me` y el diagnóstico. Una llamada que tu programa corta antes de la respuesta queda con el estado `499` y no cuenta como correcta.
- **`check-request`** acota la consulta (100 parámetros y 16 KB), el anidamiento del cuerpo (32 niveles) y la petición entera (300 KB), con `400 VALIDATION_ERROR`.
- **Cambio de comportamiento: contactos dados de baja.** Leer el detalle de un contacto dado de baja o inactivo sin `crm:contacts:read:opted-out` responde `404 CONTACT_NOT_FOUND`, como uno que no existe, en lugar de `403 INSUFFICIENT_SCOPE`. Ver [Consultar un contacto](https://os.utilia.ai/docs/developers/api/contacts#consultar-un-contacto).
- **Permisos críticos sin transición.** Los de datos personales sensibles y los financieros se exigen siempre, aunque haya un periodo de transición abierto para otros permisos de su dominio.
- **MCP:** `integrations_overview` gana la operación `app_diagnosis`, el diagnóstico de una aplicación, con `EXTERNAL_APPS_VIEW` y el scope `integrations:overview:view`. Ver [`integrations_overview`](https://os.utilia.ai/docs/developers/mcp/reference#integrations_overview).
- **Panel:** la pestaña **Pruebas** de cada aplicación, rehecha sobre el mismo diagnóstico, pasa a ser la segunda. Ver [La pestaña Pruebas](https://os.utilia.ai/docs/developers/api/apps-panel#la-pestaña-pruebas).

### API externa: datos fiscales, claves en páginas web y cobros

- **CAMBIO QUE ROMPE: el NIF de los usuarios pide su permiso.** Permiso nuevo `crm:users:read:fiscal-data`, de datos personales críticos (lo concede un Super Administrador con motivo). Sin él, `GET /external/v1/users` ya no trae `taxId` ni `billingAddress`, e `identify` solo devuelve los que mandas en esa misma llamada. Además, `identify` solo devuelve lo guardado del usuario con `crm:users:read` y una clave que no esté publicada en páginas web; si no, el eco de lo que mandas. El catálogo pasa a **58** permisos. Ver [Usuarios externos](https://os.utilia.ai/docs/developers/api/users#qué-devuelve-identify).
- **CAMBIO QUE ROMPE: reintentar el cobro de una cuota.** `POST /external/v1/subscriptions/:id/invoices/:invoiceId/retry-charge` exige `billing:scheduled-charges:write` en lugar de `billing:subscriptions:write`, admite 3 llamadas por minuto y aplicación y pide una persona responsable activa. `generate-now` sobre una suscripción que se cobra sola pide además `billing:scheduled-charges:write` o `billing:charge`. Y `billing:scheduled-charges:write` pasa a crítico financiero: concederlo exige ser Super Administrador y un motivo (las aplicaciones que ya lo tenían lo conservan). Los protegidos pasan a **17**. Ver [Suscripciones](https://os.utilia.ai/docs/developers/api/subscriptions#plan-y-permisos).
- **CAMBIO QUE ROMPE: rectificativas de prueba.** `POST /external/v1/invoices/:id/rectifications/validate` exige `crm:invoices:rectify`: sin él, `403 INSUFFICIENT_SCOPE` en lugar de un `200` con el bloqueo.
- **Claves dentro de una página web.** Una clave cuenta como publicada si su aplicación tiene dominios autorizados o si se ha usado desde un navegador desde su última renovación, y solo admite siete permisos, los del widget. Desde un navegador, una ruta con otro permiso responde `403 SCOPE_NOT_ALLOWED_FROM_BROWSER`; al configurar, `400 SCOPE_NOT_ALLOWED_WITH_ALLOWED_ORIGINS`. Las aplicaciones que ya tenían otros permisos siguen funcionando desde su servidor, con el aviso `PUBLISHABLE_KEY_SENSITIVE_SCOPES`. El catálogo de permisos gana `publishableAllowed` y `conditionalRequirements`. Ver [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).
- **Panel:** la ficha de una aplicación agrupa sus pantallas en seis pestañas (Resumen, Pruebas, Actividad, Dinero, Usuarios y Configuración) sin cambiar ninguna dirección; «General» pasa a ser **Resumen** y «OAuth», **Configuración › Inicio de sesión**. **Ajustes › Integraciones › Aplicaciones externas** enseña la misma lista que Gestión de proyectos. Ver [La ficha de una aplicación](https://os.utilia.ai/docs/developers/api/apps-panel#la-ficha-de-una-aplicación).
- **Avisos de salud.** El aviso de que una aplicación no funciona se repite en cada caída nueva, y cada persona lo configura en **Mi perfil › Avisos**, fila **Integraciones**, bloque **Salud de las aplicaciones**. Ver [Salud de la aplicación](https://os.utilia.ai/docs/developers/getting-started/app-health).

### SDK 4.6.0 (sin publicar)

La 4.6.0 de los dos SDK está en el repositorio y sin publicar; en npm sigue la 4.1.0 y en PyPI, la 4.2.0. Trae `sdk.diagnostics` (con `getMarkdown()` y el diagnóstico de las claves de la organización) y la orden `doctor` (`npx @utilia-os/sdk-js doctor` o `utilia-doctor`, que sale con `0`, `1` o `2`), `sdk.aiAnalytics`, `budgetComments` con clave de API, `onIgnoredFields`, el catálogo público de errores y los campos `fixedBy`, `docsUrl`, `requiredScopes`, `retryAfterMs` y `retryable` en los errores.

**Cambio de comportamiento:** un `POST` sin clave de idempotencia ya no se repite solo, ante un 429 el SDK espera lo que pide `Retry-After` (o no reintenta si pasa de `maxRetryAfterMs`), `X-Utilia-Quota-Reset` solo cuenta en el 429 del cupo por hora agotado, un 409 llega como `ErrorCode.CONFLICT` en lugar de `UNKNOWN` y la validación local habla en castellano. `doctor` no manda la clave por `http://` salvo a este mismo equipo (`--allow-insecure-http` para un laboratorio aislado) y, con una clave publicada en una web, no lanza la prueba activa. **Cambio en los tipos:** los de `sdk.mcp.integrationsOverview` pasan a ser los que manda el servidor, así que el código que leía los nombres viejos deja de compilar. La tabla de qué cambiar está en [Novedades de la versión 4.6.0](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-460).

### SDK 4.5.0

La 4.5.0 de los dos SDK, del 21 de octubre y sin publicar todavía, trae los métodos de las claves de API de la organización por MCP (`orgKeys*`) y los del ensayo de facturas del 20 de octubre. **Cambio incompatible:** se retiran métodos que ninguna organización cliente podía usar porque respondían siempre «prohibido». La lista y la tabla de qué cambiar están en [Novedades de la versión 4.5.0](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-450).

### Claves de API de la organización

Credencial nueva para los programas y las automatizaciones de tu empresa: con `Authorization: Bearer utk_…` llamas a la misma API que usa la aplicación (`https://os.utilia.ai/api/...`), con los permisos de una persona responsable recortados por la clave (todos, solo lectura o módulo a módulo) y viendo las mismas filas que ella. Se crean en Ajustes › Claves de API y necesitan Integraciones en el plan. No abren la API externa, la API de leads ni los servidores MCP, y se rechazan si llegan desde un navegador. Todo está en la sección [Claves de API de la organización](https://os.utilia.ai/docs/developers/organization-api-keys/overview).

- **Aviso antes de que caduque.** Una clave con caducidad avisa 7 días antes y el día antes (si se crea con menos de 7 días por delante, el primer aviso sale enseguida) a su persona responsable y, si es otra, a quien la creó, por los canales que cada una tenga activados en la fila «Caducidad de las claves de API» de sus avisos de seguridad. Si cambias la caducidad, los avisos se vuelven a programar. Para que no se corte, se alarga con `PATCH /api/v2/organization-api-keys/:id` y `expiresAt`, o se crea otra; una clave caducada no se renueva. Detalle en [El aviso antes de que caduque](https://os.utilia.ai/docs/developers/organization-api-keys/expiry-renewal-revocation#el-aviso-antes-de-que-caduque).
- **Baja de una persona.** Al desactivarla o archivarla se revocan las claves de las que es persona responsable. Las que creó para otra persona siguen funcionando, porque una clave actúa con los permisos de su responsable. Reactivarla no devuelve ninguna clave.

### Panel de aplicaciones externas: eliminar, restaurar y eliminar definitivamente

- **`DELETE /api/integrations/apps/:id` ya no desactiva: elimina.** La aplicación deja de funcionar y sale de la lista, pero no se borra nada y se restaura con `POST /api/integrations/apps/:id/restore`, que la devuelve desactivada. La clave de una aplicación eliminada responde `401 API_KEY_INVALID`. Con suscripciones o cobros programados en marcha, eliminarla responde `409 EXTERNAL_APP_HAS_ACTIVE_BILLING`.
- **Las eliminadas** se listan con `GET /api/integrations/apps?deleted=only` y conservan su identificador: reutilizarlo responde `409 EXTERNAL_APP_SLUG_HELD_BY_DELETED`.
- **Rutas nuevas** `GET /api/integrations/apps/:id/permanent-deletion-preview` y `POST /api/integrations/apps/:id/permanent-delete`, que borra la aplicación para siempre con `confirmName` y conserva lo que obliga la ley: facturas, suscripciones, autorizaciones de cobro, tickets, fichas del CRM y archivos.
- **Cambio incompatible (MCP):** `apps_delete` elimina en lugar de desactivar y solo lo permite `EXTERNAL_APPS_DELETE`; ya no basta `INTEGRATIONS_APPS_MANAGE`. Llegan `apps_restore` y `apps_delete_permanently`, y `integrations` pasa de 39 a 41 operaciones.

Detalle en [Eliminar una aplicación](https://os.utilia.ai/docs/developers/api/apps-panel#eliminar-una-aplicación).

### SDK 4.5.0 (JavaScript y Python)

Los dos SDK pasan a la **4.5.0**: los métodos `orgKeys*` de las claves de API de la organización en `sdk.mcp.apiCredentials`, las pruebas sin emitir facturas (`invoices.validate`, `validatePdf`, `rectifications.validate` y `webhooks.signTestPayload`) y la retirada de métodos de `sdk.mcp` que ninguna organización cliente podía usar. Ver [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-450).

### Alta de una aplicación: varios usos o ninguno

En la pantalla **Nueva aplicación externa**, **¿Para qué la usarás?** admite varias respuestas o ninguna, y los ajustes de tickets, también el **Proyecto vinculado**, solo se piden si marcas **Tickets y soporte**. La API del alta (`POST /api/integrations/apps`) no cambia.

## 2026-10-20

### API externa: `displayNumber` lleva el guion del número impreso

El número de factura se presenta, como siempre, con un guion entre el
año y el número de orden (`T-2026-00001`): en el PDF, en los correos,
en el panel y en `displayNumber`, tanto en las respuestas de la API como
en los avisos `INVOICE_*` y en las referencias de las rectificativas. Corrige
lo publicado el 19 de octubre, que lo daba sin guion e igual que
`invoiceNumber`. El número registrado no cambia: `invoiceNumber`
(`T-202600001`) es el de los libros registro y, si la factura lo lleva,
el de su código QR; guarda, busca y concilia con él. En la validación
en seco, `displayNumberFormat` es `T-AAAA-00000` y `numberFormat`,
`T-AAAA00000`.

## 2026-10-19

### API externa: el estado de la emisión ya no depende del correo ni del cobro

**Cambio de comportamiento** en `issuanceDeliveryStatus`, que llevan el
alta (`POST /external/v1/invoices`) y todas las lecturas de una factura
emitida por la API:

- `processing`: la emisión sigue en curso. Repite la misma petición, con la misma clave.
- `review_required`: **solo** cuando la ejecución se interrumpió antes de terminar el cierre fiscal (número, sello, registro y efectos del documento). Lo revisa el equipo de la organización en la ficha de la factura, con el botón nuevo **Dar por revisada**, que exige el número y el PDF original verificado y no reemite ni reenvía nada.
- `complete`: en todos los demás casos.

Hasta ahora, un correo que no salía o un cobro del bloque `payment` que
no se registraba dejaban la factura en `review_required` para siempre,
y su rectificativa y su anulación respondían `409` sin que nada pudiera
completarla. Ya no: la factura queda `complete` y el resultado viaja
aparte. **Si tu integración usaba `issuanceDeliveryStatus:
review_required` para detectar un correo fallido, lee ahora
`emailDelivery`.** `issuancePending` solo vale `true` con `processing` o
`review_required`, y las facturas anteriores que estaban en revisión
solo por el correo o el cobro se leen ya como `complete`. Ver
[Estado de la emisión](https://os.utilia.ai/docs/developers/api/invoices#estado-de-la-emisión).

### API externa: campos nuevos en la respuesta de las facturas

Son aditivos:

- **`emailDelivery`**: si la factura ha llegado al cliente por correo, por qué no y qué hacer. `status` (`sent`, `retrying`, `failed` o `withheld`), `reason` (`SEND_SCOPE_MISSING`, `PAYMENT_NOT_REGISTERED`, `SEND_QUOTA_EXCEEDED`, `NO_RECIPIENT`, `PDF_NOT_READY`, `STORAGE_UNAVAILABLE`, `MAIL_SERVER_UNAVAILABLE`, `SEND_FAILED`, `RETRIES_EXHAUSTED` o `INTERRUPTED`), `message` en castellano listo para mostrar, `attempts`, `lastAttemptAt` y `sentAt`. Solo viaja si se pidió el correo (`sendEmail: true` o, sin `sendEmail`, el interruptor de la aplicación encendido), y se actualiza solo. Ver [El correo de la factura](https://os.utilia.ai/docs/developers/api/invoices#el-correo-de-la-factura).
- **`displayNumber`**: el número tal como se imprime, con guion entre el año y el número de orden (`T-2026-00001`). Viaja también en las referencias de las rectificativas.
- **`emailSent`** sigue siendo el resumen en un booleano y ahora viaja también en las repeticiones con la misma clave.
- **`paymentRegistered`** pasa a `true` cuando el cobro que falló al emitir se registra después, con `mark-paid` o desde la ficha de la factura.
- **Avisos**: todos los bloques de factura de los avisos `INVOICE_*` (`invoice`, `originalInvoice` y `rectifyingInvoice`) llevan `displayNumber`.

### API externa: el correo al emitir se reintenta solo

- **`sendEmail` manda sobre el interruptor** «Enviar las facturas por correo al emitirlas» en los dos sentidos: `true` la envía aunque esté apagado y `false` la retiene aunque esté encendido. Sin `sendEmail`, decide el interruptor.
- **El intento dentro de la petición** espera al PDF original unos 8 segundos como mucho. Si no está listo, si el almacén de documentos o el servidor de correo fallan de forma pasajera o si se agota el cupo de 100 envíos por hora de la aplicación (el mismo de `POST /send`), `emailDelivery.status` queda en `retrying` y UTILIA OS la reenvía sola: un reintento rápido a los 45 segundos, más o menos, y después la recuperación de envíos automáticos cada 10 minutos, hasta 12 intentos (unas dos horas). Si se agotan, `failed` con `RETRIES_EXHAUSTED` y un aviso en UTILIA OS para la persona que figura como creadora de la factura. No la reenvíes mientras esté en `retrying`.
- **Todo queda a la vista**: cada intento, y cada decisión de no enviarla (falta el permiso de envío, el cobro no se registró o no hay destinatario), queda en el historial de comunicaciones de la factura.
- **Nunca sale dos veces**: si alguien la envía después desde la ficha o con `POST /send`, `emailDelivery` pasa a `sent` y el reintento automático se detiene.

El correo de la primera factura real de un cliente no salía porque el PDF original tardaba entre 87 y 118 segundos en estar listo, mientras el antivirus lo analizaba en diferido; ahora el original que genera el servidor se analiza al subirlo y está listo en un segundo, más o menos.

### API externa: tiempo de espera del alta de facturas

`POST /external/v1/invoices` puede tardar varios segundos. Usa un tiempo
de espera de al menos 30 segundos y, si se corta, repite con la
**misma** `idempotencyKey`: si la primera ejecución sigue en marcha, la
repetición devuelve la misma factura con `processing`, nunca una
segunda. Ver [Reintentar sin duplicar la factura](https://os.utilia.ai/docs/developers/api/invoices#reintentar-sin-duplicar-la-factura).

### API externa: rectificativas

- **`RECTIFICATION_NOT_ALLOWED` dice cuándo basta con esperar.** Sus motivos son ahora: borrador, anulada, ya rectificada por completo, es una rectificativa, emisión todavía en curso (`409` con `retryable: true` y `retryAfterSeconds: 5`), cierre fiscal interrumpido (el equipo tiene que darla por revisada), PDF original todavía preparándose (`retryable: true`, `retryAfterSeconds: 30`) o pendiente de revisión, otra rectificación en curso y periodo cerrado. El correo y el cobro con que se emitió la factura no la impiden nunca, y tampoco un asiento contable pendiente de revisión. Ver [Cuándo se puede rectificar](https://os.utilia.ai/docs/developers/api/invoice-rectifications#cuándo-se-puede-rectificar).
- **Ruta nueva `POST /external/v1/invoices/:id/rectifications/validate`**: con el mismo cuerpo, responde `200` con `valid`, `livemode: false`, `blockers`, `warnings` y `preview` (`cause`, `rectificationCode`, `totalAmount`, `remainingAfter`, `issueDate`, `refund` y `willSendEmail`) sin numerar ni registrar nada. Avisa con `IDEMPOTENT_REPLAY` si la clave ya tiene una operación.
- **Una sola vía por devolución.** Si tu equipo registra la devolución a mano en la ficha (**Reembolsar › Registrar reembolso ya emitido**, con la misma referencia de la pasarela), una rectificativa posterior por la API con esa referencia responde `409 REFUND_REFERENCE_ALREADY_USED` y, si aquella acción ya rectificó la factura entera, `409 RECTIFICATION_NOT_ALLOWED`. Si una petición se cortó, repítela con la misma `idempotencyKey`.

### API externa: errores del envío de facturas

`POST /external/v1/invoices/:id/send` dice ahora la causa real cuando el
correo no sale, y la factura sigue siempre emitida:

- `503 INVOICE_PDF_NOT_AVAILABLE` con `retryable: true`: el PDF original se está terminando de preparar (`retryAfterSeconds: 15`) o el almacén de documentos no responde (`retryAfterSeconds: 120`).
- `409 INVOICE_PDF_NOT_AVAILABLE` con `retryable: false`: el PDF original necesita que lo revise el equipo.
- `502 INVOICE_EMAIL_NOT_SENT`, con `retryable: true` y `retryAfterSeconds: 120` si el servidor de correo no responde, o con `retryable: false` si el servidor de salida rechazó el envío.

Antes todos salían como `502 INVOICE_EMAIL_NOT_SENT`.

### API externa: `retryable` y `retryAfterSeconds` en los errores

Algunos errores de negocio llevan en la raíz del cuerpo `retryable` y,
si basta con esperar, `retryAfterSeconds`. Cuando vienen, mandan sobre
el código HTTP: un `409` con `retryable: true` se repite tal cual. Ver
[Errores, identificador de petición y reintentos](https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#cuándo-reintentar).

### Probar la integración sin facturas reales

- **`POST /external/v1/invoices/validate`** y **`/validate/pdf`**: el mismo cuerpo que el alta, sin numerar ni guardar nada. La primera responde el veredicto y la vista previa; la segunda, un PDF de muestra sin validez fiscal. Bastan con cualquier permiso `crm:invoices:*` y no exigen la facturación activada: la informan como bloqueo `BILLING_NOT_ENABLED`.
- **Avisos de ejemplo**: `POST /api/integrations/apps/:id/webhook/test` admite `event` en el cuerpo y envía una muestra de ese evento, firmada como los avisos reales, con `livemode: false` y `meta.test: true`. La misma opción tiene `apps_webhooks_test` del MCP.
- **Pestaña nueva «Pruebas»** en la ficha de cada aplicación: lo que falta antes de facturar de verdad, las peticiones de prueba listas para copiar, los avisos de ejemplo y el recorrido hasta la primera factura real.

Todo está ahora en [Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing).

### Panel de aplicaciones externas

- **Estado de la General**: con permisos de facturación y la facturación apagada, la fila **Activar la facturación** aparece como paso pendiente (solo un Super Administrador puede encenderla). Los avisos a tu servidor y el inicio de sesión con UTILIA OS (OAuth) se muestran como opcionales y ya no cuentan como pasos que faltan, y una fila nueva, **Correo de las facturas**, dice si UTILIA OS las envía al emitirlas. Ver [El estado de la aplicación](https://os.utilia.ai/docs/developers/api/apps-panel#el-estado-de-la-aplicación).
- **Ruta nueva `POST /api/integrations/apps/:id/invoices/:invoiceId/issuance-review`** (permisos `EXTERNAL_APPS_EDIT` e `INVOICES_ISSUE`): es el botón **Dar por revisada**.
- **Ficha de la factura**: las facturas de una aplicación muestran un aviso cuando algo no ha terminado («Correo de la factura pendiente», «La factura no ha llegado al cliente», «Cobro sin registrar» o «Emisión pendiente de revisión»), con **Enviar ahora**, **Registrar cobro** o **Dar por revisada**. Ver [Facturas emitidas por aplicaciones externas](https://os.utilia.ai/docs/guides/finance/external-app-invoices#el-aviso-de-la-ficha-cuando-algo-no-ha-terminado).

### MCP: `integrations_overview` distingue lo pendiente de lo recomendable

`quickstart` cuenta como configuración pendiente (`missingConfig`) cada
aplicación activa con permisos de facturas y la facturación apagada, y
la propone como siguiente acción. Los avisos a tu servidor pasan al
campo nuevo `recommendations`, junto con las aplicaciones con permisos
de facturas cuando el plan no incluye Finanzas: ya no se presentan como
pasos que faltan. Ver [`integrations_overview`](https://os.utilia.ai/docs/developers/mcp/reference#integrations_overview).

### PDF y correos de las facturas

- **El PDF original no imprime el estado** («Estado: Emitida»): el documento queda fijado al emitirlo y el estado cambia después. El estado vigente está en el panel.
- **Simplificadas sin destinatario**: «Consumidor final», sin líneas de guiones en la dirección. Desaparece el sello «FACTURA SIMPLIFICADA» repetido; se mantienen el título y la mención legal.
- **Pie de protección de datos**: si la organización no ha escrito su **Pie legal**, se construye con los datos del emisor fijados en la factura (responsable, NIF, dirección, correo de contacto, derechos y reclamación ante la AEPD). El **Pie legal** propio lo sustituye.
- **Texto adicional al pie** del editor de la marca de las facturas: ahora se imprime.
- **Correos de una factura**: la cabecera y el pie usan el color principal del tema de la factura, con el contraste del texto calculado solo; el tema azul por defecto conserva la cabecera oscura. El saludo es «Hola,» cuando no hay un nombre real, nunca un correo electrónico ni «Consumidor final».

Ver [Qué imprime el PDF de la factura](https://os.utilia.ai/docs/guides/finance/invoicing#qué-imprime-el-pdf-de-la-factura).

### SDK

La 4.5.0 de los dos SDK, sin publicar, trae `invoices.validate`,
`invoices.validatePdf` (`validate_pdf`), `invoices.rectifications.validate`,
`webhooks.signTestPayload` (`sign_test_payload`) para firmar avisos en tus
pruebas, y los campos `displayNumber` y `emailDelivery` en los tipos de las
facturas. Las rutas ya funcionan sin ellos. Ver [SDK 4.5.0](#sdk-450).

## 2026-10-15

### API externa: series de facturación de las aplicaciones y fecha de expedición acotada

Tu organización puede reservar a tu aplicación una o varias series de
facturación (por ejemplo, `APP-202600001`), separadas de las facturas del
equipo, y fijar cuál se usa por defecto. Sin serie propia ni `seriesCode`,
todo se numera exactamente igual que antes.

- **`GET /external/v1/invoices/series`** (permiso `crm:invoices:read`): las series en las que la aplicación puede numerar, con su código, tipo, prefijo, un número de ejemplo y cuál se usa por defecto. No devuelve el siguiente número. Ver [Series disponibles](https://os.utilia.ai/docs/developers/api/invoices#series-disponibles).
- **`seriesCode`** en `POST /external/v1/invoices`: el código de una serie de tu aplicación o `DEFAULT` para la de siempre. Sin él, la serie por defecto de la aplicación para el tipo o, si no tiene, la de siempre. Errores, todos antes de numerar: `400 INVOICE_SERIES_NOT_AVAILABLE`, `400 INVOICE_SERIES_TYPE_MISMATCH` y `409 INVOICE_SERIES_INACTIVE`.
- **Suscripciones**: `type` (ordinarias o simplificadas) y `seriesCode` en `POST /external/v1/subscriptions` y `PATCH /external/v1/subscriptions/:id`; la respuesta los trae.
- **Respuestas y avisos**: cada factura trae `seriesCode` (`DEFAULT` en la serie de siempre y en las rectificativas, que van siempre a `R-`), también en el objeto `invoice` de los avisos `INVOICE_*`. `GET /external/v1/invoices` filtra por `seriesCode`.
- **Panel de aplicaciones**: `billing-config` lee y fija las series por defecto de la aplicación (`defaultStandardSeriesId`, `defaultSimplifiedSeriesId` y `availableSeries`). Ver [Series por defecto de la aplicación](https://os.utilia.ai/docs/developers/api/apps-panel#series-por-defecto-de-la-aplicación).
- **Sin permisos nuevos.** La descripción de `crm:invoices:read` dice ahora que también cubre la consulta de series. La clave de API no crea ni cambia series.

**Cambio incompatible**, solo si tu aplicación envía `issueDate`: la fecha de expedición ya no puede ser futura (en la zona horaria de la organización) ni anterior a la de la última factura ya expedida en la misma serie, y su año es el de la numeración. Si no se cumple, la respuesta es `400 INVOICE_ISSUE_DATE_OUT_OF_ORDER` y no se numera nada. Sin `issueDate`, se usa el día de hoy de la organización, como hasta ahora.

### API externa: contrato de la ventanilla única del IVA (Próximamente)

La ventanilla única del IVA está en «Próximamente»: está terminada y en
validación, y todavía no está disponible para las organizaciones. Su
contrato ya está publicado para que puedas prepararte, sin que cambie
nada de lo que ya haces:

- `POST /external/v1/invoices` y las suscripciones admiten `taxSystem: "EU_OSS"`, líneas `EU_VAT` con `vatRateClass` y `ossSupplyType`, y `consumerResidence.evidenceItems`, la prueba estructurada de la ubicación del consumidor.
- `GET /external/v1/invoices/eu-vat-rates` (permiso `crm:invoices:read`) devuelve los tipos de IVA vigentes de los Estados miembros.
- Las respuestas y los avisos `INVOICE_*` traen `taxSystem`, `consumerCountryCode` y `ossScheme`; las líneas, `vatRateClass` y `ossSupplyType`.
- Mientras la función no esté disponible para tu organización, cualquier dato suyo responde `403 FEATURE_COMING_SOON` con `featureKey: "finance.oss"`. No es un bloqueo de plan.
- La ventanilla única del IVA está en «Próximamente»: sus reglas y sus errores `OSS_*` se documentarán cuando esté disponible.

### SDK 4.4.0 (JavaScript y Python)

Los dos SDK pasan a la **4.4.0**:

- `invoices.listSeries()` (`list_series()`) y `seriesCode` (`series_code`) en el alta, en el listado y en las suscripciones.
- `type` en las suscripciones y `seriesCode` en las respuestas.
- Los códigos de error de las series y de la fecha de expedición.
- Las series por defecto en `appsBillingGetConfig` y `appsBillingUpdateConfig` del MCP.
- El contrato de la ventanilla única del IVA: `EU_OSS`, `getEuVatRates()`, los códigos `OSS_*` y `FEATURE_COMING_SOON`.

El detalle está en [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-440).

### MCP: `org_plan` dice si tienes el precio de la lista de espera

La operación `get` de `org_plan` devuelve un bloque nuevo, `pricing`, con la cuota que se cobra cada periodo. Dentro, `pricing.waitlistPrice` indica si la organización tiene el precio de la lista de espera, cuánto cuesta al mes con él y sin él (`effectiveMonthlyCents` y `listMonthlyCents`, en céntimos de euro) y, con `losesOnCancel`, si darse de baja haría perderlo. Sin ese precio llega a `null`, y si la cuota no se puede leer, `pricing` también.

Es aditivo: una integración que no lea `pricing` sigue funcionando igual. El detalle está en [MCP: plan, peticiones y llamadas](https://os.utilia.ai/docs/developers/mcp/plan-and-calls).

### El análisis con IA de tickets avisa cuando recorta imágenes

Sin cambios incompatibles: las peticiones de siempre reciben la respuesta de
siempre.

- **`POST /api/external/v1/tickets/ai-suggest` analiza como máximo las 5
  primeras imágenes** de `images`, en el orden del array. Con más, la petición
  se sigue aceptando (`201`), pero la respuesta trae además
  `imagesTruncated: true`, `imagesUsed` e `imagesReceived`. Antes el resto se
  descartaba sin que nada lo indicara.
- **Son campos aditivos**: solo aparecen cuando hay recorte. Detalle en
  [Sugerencias con IA en tickets](https://os.utilia.ai/docs/developers/api/ticket-ai-suggestions).

### Convertir un presupuesto en proyecto exige poder crear proyectos

**Cambio incompatible** para las integraciones con scopes granulares que
convierten presupuestos en proyectos.

- **REST**: `POST /api/crm/budgets/:id/convert-to-project` exige los permisos
  `BUDGETS_EDIT` y `PROJECTS_CREATE`, los dos. Antes bastaba con el primero, y
  quien no podía crear proyectos los creaba por esta vía. Es la misma regla que
  ya tenía crear el proyecto de una oportunidad ganada.
- **MCP**: `crm_budgets(operation: 'convert_to_project')` y
  `crm_budgets_workflow(operation: 'convert_to_project')` exigen al token
  `crm:budgets:update` y `crm:projects:create`, y al usuario los dos permisos.
  `crm:write` y `crm:full` ya incluyen los dos scopes; `crm:read` no permite
  convertir.

### Convertir un presupuesto en factura por MCP exige Finanzas en el plan

**Cambio incompatible** para las organizaciones que convierten presupuestos en
facturas por MCP sin tener Finanzas en su plan.

- **MCP**: `crm_budgets(operation: 'convert_to_invoice')` y
  `crm_budgets_workflow(operation: 'convert_to_invoice')` exigen el aplicativo
  Finanzas en el plan de la organización, como ya hacía
  `POST /api/crm/budgets/:id/convert-to-invoice`. Antes, una organización con
  CRM y sin Finanzas creaba facturas por esta vía. Ahora la llamada se rechaza
  con el mismo error que el resto de herramientas cuando falta un aplicativo.
- Los permisos y los scopes no cambian: al usuario, `BUDGETS_EDIT`,
  `INVOICES_VIEW` e `INVOICES_CREATE`; al token, `crm:budgets:update`,
  `crm:invoices:view` y `crm:invoices:create`, más `crm:invoices:issue` para
  emitir directamente. Ningún scope de lectura permite convertir.

### Crear un cobro recurrente desde un presupuesto exige Finanzas y su permiso

**Cambio incompatible** para las integraciones que crean cobros recurrentes
desde un presupuesto sin tener Finanzas en el plan o sin poder crear facturas
recurrentes.

- **REST**: `POST /api/crm/budgets/:id/convert-to-recurring-invoice` pide lo
  mismo que `POST /api/finance/recurring-invoices`: el aplicativo Finanzas y la
  capacidad «Cobros recurrentes» en el plan de la organización, y el permiso
  `RECURRING_INVOICES_CREATE`, además de `BUDGETS_EDIT`. Antes bastaba
  `BUDGETS_EDIT`, y se creaban cobros recurrentes sin Finanzas en el plan.
- **MCP**: no hay operación equivalente; no cambia nada.

## 2026-10-14

### MCP y scopes: tesorería, informes, comisiones, impuestos y contabilidad pasan a «Próximamente»

**Cambio incompatible** para las integraciones que usan estas herramientas.

Las estamos validando con especialistas antes de ponerlas a tu disposición.
Hasta entonces dejan de estar disponibles para las organizaciones, también por
MCP y con cualquier plan:

- **Tesorería**: `treasury`.
- **Impuestos**: `fiscal` y `fiscal_capture`. Siguen disponibles la configuración fiscal y las retenciones (`fiscal: config_get`, `retention_rates_get` y `retention_rates_update`) y toda la herramienta `finance_settings`.
- **Contabilidad**: `accounting`, `accounting_journal`, `accounting_ledger`, `accounting_assets`, `accounting_close` y `suppliers: accounting_accounts`.
- **Comisiones**: `commissions` y `commissions_admin`.
- **Informes**: `finance_reports`, salvo `summary`, el resumen de Finanzas, que sigue disponible.
- **Las copias del servidor `utilia-crm`** de estas herramientas se comportan igual.

Los scopes `crm:treasury:*`, `crm:fiscal:*` (salvo para la configuración fiscal y las retenciones), `finance:accounting:*` y `finance:commissions:*`, y `finance:reports:view` fuera del resumen, siguen existiendo, pero mientras tanto no dan acceso a esas funciones. Facturas, cobros, cobros recurrentes, gastos, proveedores y pagos no cambian.

- **Qué recibe tu integración:** la respuesta dice que la función estará disponible próximamente, con el mismo texto que la aplicación (por ejemplo, «El apartado Tesorería estará disponible próximamente.»). Una petición HTTP a esas funciones recibe un 403 con `errorCode: "FEATURE_COMING_SOON"`, `featureKey` y `featureLabel`. No es un bloqueo de plan ni de permisos: no se resuelve ampliando el plan ni concediendo scopes.
- **En los SDK** afecta a `sdk.mcp.fiscalCapture` (`sdk.mcp.fiscal_capture` en Python) y a las llamadas MCP genéricas a esas herramientas. El resto del SDK no cambia.
- **Qué hacer:** trátalo como un estado esperado, no lo reintentes y desactiva en tu integración lo que dependa de esas herramientas. Detalle en la [referencia de herramientas MCP](https://os.utilia.ai/docs/developers/mcp/reference#finanzas-servidor-utilia-finance) y en [Errores de plan](https://os.utilia.ai/docs/developers/sdk/plan-errors#funciones-que-llegan-próximamente).

## 2026-10-13

### API externa: factura simplificada, rectificativa y régimen del pequeño empresario del IGIC

Todo es aditivo: una integración que no use los campos nuevos sigue
funcionando igual.

- **Factura simplificada.** `POST /external/v1/invoices` admite `type: "SIMPLIFIED"`, con su propia serie (`T-`), un tope de 400 € impuestos incluidos (`422 SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED`) y destinatario opcional. `recipient` pasa a ser opcional en el contrato; una ordinaria lo sigue exigiendo completo (`400 INVOICE_RECIPIENT_REQUIRED` o `INVOICE_RECIPIENT_INCOMPLETE`). Ver [Facturas](https://os.utilia.ai/docs/developers/api/invoices#ordinaria-o-simplificada).
- **`pricesIncludeTax`**, el bloque **`payment`** (factura cobrada en el acto con tu propia pasarela) y **`consumerResidence`** (residencia del consumidor, cotejada con el sistema fiscal).
- **Régimen del pequeño empresario del IGIC (REPEP).** Causa de exención `EXEMPT_IGIC_SMALL_BUSINESS` y mención «Exención franquicia fiscal», que UTILIA OS pone sola en las líneas exentas de una factura IGIC dentro de un periodo del régimen. Errores `IGIC_SMALL_BUSINESS_TAXED_LINE`, `IGIC_SMALL_BUSINESS_REQUIRES_IGIC` e `IGIC_SMALL_BUSINESS_NOT_ACTIVE`.
- **Rectificativa independiente.** `POST` y `GET /external/v1/invoices/:id/rectifications`: tu aplicación da la causa (`REFUND` o `CHARGEBACK`) y UTILIA OS decide el código legal (R5 o R1), siempre por diferencias. El bloque `refund` registra la devolución hecha en tu pasarela como «Pasarela de pago externa», sin mover dinero. Ver [Facturas rectificativas](https://os.utilia.ai/docs/developers/api/invoice-rectifications).
- **Permiso nuevo `crm:invoices:rectify`**, crítico financiero: solo lo concede un Super Administrador. El catálogo pasa a **57** permisos y los protegidos a **15**.
- **Respuestas ampliadas**: `type`, `legalMentions`, `originalInvoiceId`, `pricesIncludeTax`, `paymentRegistered`, `rectificationCode` y `rectificationType`; en el detalle, `exemptionReason` y `exempt` por línea, `rectifications` y `effectiveTotal`. `GET /external/v1/invoices` filtra por `type`.
- **El reembolso con `createRectifyingInvoice: true`** emite ahora la rectificativa por diferencias, con el código que decide UTILIA OS, y la calcula antes de mover el dinero: si no se puede documentar al céntimo, el reembolso se rechaza con `422`.
- **Avisos**: el objeto `invoice` de los avisos de facturas incluye `type`; `INVOICE_REFUNDED` admite `refund.source: "EXTERNAL_GATEWAY"`; las rectificativas de las facturas de una aplicación llegan solo a esa aplicación.

### SDK 4.3.0 (JavaScript y Python)

Los dos SDK pasan a la **4.3.0**, que cubre todo lo anterior: `type: 'SIMPLIFIED'`, `pricesIncludeTax`, `payment`, `consumerResidence`, `invoices.rectifications.create` y `list`, los códigos de error nuevos (`INVOICE_ERROR_CODES`, `InvoiceErrorCode` en Python) y los datos de los errores de negocio en `error.details`. El detalle y la tabla de cambios desde la 4.2 están en [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-430).

**Cambio incompatible menor**: `recipient` y sus campos de dirección son opcionales en el tipo, y un 403 cuyo mensaje contenía «origen» ya no se reescribe como «Origen no permitido por CORS». Si decidías por ese texto, decide por `errorCode`.

### SDK 4.2.0 (JavaScript y Python)

Los dos SDK pasan a la **4.2.0**, que incluye también la 4.1.1 del 11 de octubre, no publicada por separado. El detalle está en [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-420).

- **La clave no viaja en ninguna dirección.** `tickets.createStreamToken` (`create_stream_token`) pide un pase de conexión de un solo uso, y las conexiones en tiempo real del SDK de JavaScript se abren con él; las de Python llevan la clave en la cabecera. `getPdfUrl`, `getCertificateUrl` y `streamGenerateItemsStatus` lanzan `METHOD_NOT_AVAILABLE_WITH_API_KEY` con clave de API.
- **Avisos con varias claves.** `sdk.webhooks.verify` y `parse` leen `X-Utilia-Webhook-Signatures` y aceptan la clave nueva y la anterior, con fecha de caducidad.
- **Aviso de clave anterior** con `onApiKeyExpires` (`on_api_key_expires`), cabecera `X-Utilia-SDK` en las peticiones, códigos de credenciales tipados y `getApprovalChallenge` para las operaciones MCP que se confirman en el móvil.
- **`budgetSignatures` con clave de API**: `list`, `get` y `verify` funcionan; el resto lanza `METHOD_NOT_AVAILABLE_WITH_API_KEY` antes de hacer la petición.
- **MCP**: los métodos de las siete operaciones nuevas de `integrations` y `appsWebhooksTestSigningKey` (`apps_webhooks_test_signing_key`).

**Cambio incompatible** si tu código leía `SignerAuditEvent.ipAddress` o `userAgent` (ya no llegan: usa `ipAddressMasked` y `deviceLabel`) o abría su propia conexión con `buildSseUrl` sobre `/external/v1/tickets/*` (usa el `url` del pase).

### API externa: pase de conexión y responsable inactivo

- **`POST /api/external/v1/tickets/stream-token`** devuelve un pase para abrir las conexiones en tiempo real con `?streamToken=` en lugar de la clave. Errores nuevos: `STREAM_TOKEN_INVALID` (401), `STREAM_TOKEN_USER_REQUIRED` (400) y `STREAM_TOKEN_UNAVAILABLE` (503). Ver [Endpoints de tickets](https://os.utilia.ai/docs/developers/api/tickets#pase-de-conexión-sin-la-clave-en-la-dirección).
- **La clave en `?apiKey=`** se sigue admitiendo de momento en las conexiones en tiempo real; cuando la API no la admite, responde 401 `API_KEY_IN_URL_NOT_ALLOWED`.
- **Cambio incompatible si decidías por el código:** una aplicación cuya persona responsable no está activa recibe `403 APP_RESPONSIBLE_INACTIVE`, en lugar del `401 API_KEY_INVALID` o el `422 EXTERNAL_APP_CREATOR_MISSING` de antes. `EXTERNAL_APP_CREATOR_MISSING` queda solo para una aplicación sin organización vinculada.

## 2026-10-12

### El aviso `BUDGET_VIEWED` deja de llevar la IP completa

**Cambio incompatible** para las aplicaciones suscritas a `BUDGET_VIEWED`.

- **`view.ipAddress` va siempre a `null`.** Hasta ahora llevaba la dirección IP completa de quien abría el presupuesto.
- **`view.ipAddressMasked` es nuevo**: la misma IP enmascarada con el formato de la API de firmas (`***.***.113.45` en IPv4; en IPv6, los dos primeros grupos y `***`), o `null` en el portal y cuando no hay dirección.
- **Las entregas ya guardadas también se han enmascarado**, y un reintento manual de una entrega antigua sale ya sin la IP completa.
- **Qué hacer:** si tu integración leía `view.ipAddress`, lee `view.ipAddressMasked`. Ejemplo completo en [Avisos: contenido y catálogo de eventos](https://os.utilia.ai/docs/developers/webhooks/events#budget_viewed).

### La IP de quien firma o abre un presupuesto sale enmascarada en todas partes

La IP completa de quien firma o abre un presupuesto solo aparece ya en el **certificado PDF de la firma**, que se descarga desde UTILIA OS con el permiso «Verificar firmas de presupuestos». En el resto de respuestas sale enmascarada (`ipAddressMasked`):

- **MCP:** `crm_budgets` con `get` deja de devolver la IP completa en el historial de firma. `crm_budget_signers` y `crm_budget_files` ya la enmascaraban.
- **Rutas del panel** (`/api/crm/budgets/:id`, `/signatures`, `/signatures/:sigId` y `/signing-links/audit`): se quita `ipAddress` y el agente de usuario completo, y llegan `ipAddressMasked` y `deviceLabel` («Chrome · macOS»). Estas rutas solo aceptan la sesión del panel.
- **API externa v1** (`/external/v1/budgets/:budgetId/signatures`): sin cambios; ya devolvía la IP enmascarada.

### Renovar las claves de una aplicación externa sin cortes

Sin cambios incompatibles: sin cuerpo, las renovaciones se comportan como hasta ahora.

- **Periodo de transición.** `POST /api/integrations/apps/:id/regenerate-api-key` y `.../regenerate-webhook-secret` admiten `graceDays` (1, 7 o 30): la clave anterior sigue valiendo esos días. Con 0, el valor por defecto, deja de valer en el acto. Detalle en [Panel de aplicaciones externas](https://os.utilia.ai/docs/developers/api/apps-panel#configuración-de-la-aplicación).
- **Doble firma de los avisos.** Todo aviso lleva la cabecera nueva `X-Utilia-Webhook-Signatures`, con la lista de firmas y la de la clave vigente primero. Durante una renovación incluye también la firma de la anterior. `X-Utilia-Signature` sigue llevando una sola firma. Cómo comprobarla, en [Firma HMAC de los avisos](https://os.utilia.ai/docs/developers/webhooks/hmac-signature#cuando-renuevas-la-clave-de-firma).
- **Cabecera `X-Utilia-Api-Key-Expires`.** Las peticiones a la API externa con la clave de API anterior, durante su transición, la reciben con la fecha en que dejará de valer.
- **Rutas nuevas del panel:** `webhook-secret/cutover`, `webhook-secret/retire-previous`, `api-key/retire-previous`, `responsible`, `responsible-candidates` y `credential-advisories/:advisoryId/dismiss`. `webhook/test` admite `{"secret": "current"}` para probar la clave nueva, primero con una firma falsa.
- **Persona responsable.** Cada aplicación actúa con los permisos de su persona responsable. Si deja de estar activa y nadie la sustituye, la API externa responde `403 APP_RESPONSIBLE_INACTIVE`.
- **MCP:** `integrations` pasa de 32 a 39 operaciones (`apps_webhooks_secret_cutover`, `apps_webhooks_secret_retire_previous`, `apps_webhooks_secret_advisory_dismiss`, `apps_api_key_retire_previous`, `apps_api_key_advisory_dismiss`, `apps_set_responsible` y `apps_list_responsible_candidates`), y las dos renovaciones aceptan `graceDays` y `twoFactorApprovalId`. `observability` gana `webdav_app_passwords_list_user` y `webdav_app_passwords_revoke_user`. Detalle en la [referencia MCP](https://os.utilia.ai/docs/developers/mcp/reference).

### OAuth: `error_uri` y `WWW-Authenticate` en `invalid_client`

Todo `invalid_client` lleva `error_uri`, la misma dirección siempre; «Missing client_id» pasa al texto común `Client authentication failed`; y el `401` de `POST /api/oauth/token` y `POST /api/oauth/introspect` lleva `WWW-Authenticate: Basic realm="UTILIA OS"` si el cliente se autenticó con la cabecera Basic. Detalle en [Cambios de OAuth](https://os.utilia.ai/docs/developers/oauth/changelog).

## 2026-10-09

### La introspección OAuth, solo para clientes confidenciales

**Cambio incompatible** para quien comprobaba tokens con `POST /api/oauth/introspect` desde un cliente público.

- **Exige un cliente confidencial** autenticado con su secreto. Un cliente público recibe `401 invalid_client`.
- **Solo responde sobre los tokens de ese mismo cliente**: los de otro cliente salen `active: false`.
- **Qué hacer:** usa un cliente de tipo **Con servidor propio** y su secreto. Una aplicación pública no necesita la introspección: llama a la API con el token y renuévalo si responde `401`. Detalle en [Cambios de OAuth](https://os.utilia.ai/docs/developers/oauth/changelog).

## 2026-10-05

### Los enlaces a la documentación tienen scope y permiso propios

**Cambio incompatible** para las integraciones que crean enlaces de
documentación.

- **`documentation(operation: 'create_share_token')` exige el scope
  `dev:docs:share`** y el permiso `DEV_DOCS_SHARE` («Compartir documentación
  con enlace»). Antes bastaba con `dev:docs:update` y con poder ver la
  documentación.
- **`dev:docs:share` se pide por su nombre**: no viaja en `dev:read`,
  `dev:write`, `dev:full` ni `admin:full`. Añádelo a los scopes que pide tu
  aplicación. Los tokens emitidos antes no lo llevan y hay que volver a
  autorizarlos.
- **REST**: `POST /api/dev-management/documentation/:id/share` exige
  `DEV_DOCS_VIEW` y `DEV_DOCS_SHARE`.
- **Los enlaces `PRIVATE` piden sesión**: `GET /api/share/:token` responde 401
  con `errorCode: SHARE_LOGIN_REQUIRED` si no hay sesión, y 404 si la sesión es
  de otra organización o no puede leer el documento. Los `PUBLIC` y `TEMPORARY`
  no cambian.
- **Adjuntos de un enlace `PRIVATE`**: `GET /api/share/:token/file/:fileId?format=json`
  devuelve `{ "url": "…" }` en lugar de redirigir, para pedir la descarga con la
  sesión en la cabecera.

### El usuario puede conceder menos scopes de los que pides

- **La pantalla de consentimiento deja desactivar grupos de permisos** antes de
  autorizar. El token lleva solo los scopes que quedan activados, y las
  renovaciones conservan ese conjunto reducido. Los de identidad (`openid`,
  `profile`, `email`) no se pueden desactivar.
- **Lee el `scope` de la respuesta del token** en vez de dar por hecho el
  conjunto pedido, y degrada con elegancia lo que dependa de un scope no
  concedido.
- **Sin `scope` en la solicitud**, la pantalla enseña el conjunto por defecto
  que se concede (los `allowedScopes` del registro o, sin ellos, los de
  identidad), y también se puede estrechar.
- Detalle en [Pantalla de consentimiento](https://os.utilia.ai/docs/developers/oauth/consent-screen).

## 2026-09-12

### SDK JavaScript 4.1.0 y SDK Python 4.1.0, publicados en npm y PyPI

Superficie nueva, sin cambios incompatibles: actualizar desde la 4.0.1 no exige
tocar nada.

- **El calendario laboral de la empresa, por MCP.** `sdk.mcp.attendance.holidays`
  (sobre `POST /api/mcp/rrhh`) ofrece `getScope()`, `setScope()`, `import(year)`
  y `list()` en JavaScript, y `get_scope()`, `set_scope()`, `import_year(year)` y
  `list()` en Python, en las variantes asíncrona y síncrona.
- **El ámbito decide qué festivos cuentan.** Sin comunidad elegida solo cuentan
  los nacionales y los propios de la empresa, así que lo primero es consultar el
  ámbito y solo después cargar el año. Cambiar el ámbito no borra festivos.
- **Los municipales no se cargan**: los publica cada ayuntamiento y la respuesta
  lo avisa en `notice`.
- **Permisos**: `hr:attendance:view` para consultar y listar;
  `hr:attendance:manage` para fijar el ámbito y cargar el año.

## 2026-09-09

### SDK JavaScript 4.0.1 y SDK Python 4.0.1, publicados en npm y PyPI

Versión **solo de documentación**: no cambia ni el comportamiento ni la firma de
ningún método, así que actualizar desde la 4.0.0 no exige tocar nada.

- **`AnnouncementsService` documenta sus reglas**: qué ámbitos exige
  (`dev:announcements:view`, `:manage` y `:publish`) y que antes de preparar o
  publicar un anuncio hay que consultar `instructions()`.
- **El número de versión del paquete de Python queda alineado**: arrastraba una
  cifra antigua en su `package.json` auxiliar.

Quien venga de la 3.x debe leer antes los cambios incompatibles de la
[4.0.0](https://os.utilia.ai/docs/developers/sdk/versions): esa versión
retiró tres métodos sin sustituto.

## 2026-09-07

### Pedir más plan y reservar una llamada, también por MCP

La herramienta transversal `org_plan` pasa de dos operaciones a doce. Las diez nuevas cubren pedir que se amplíe el plan (`request_upgrade`, `list_my_requests`, `withdraw_upgrade_request`, `list_requests`, `dismiss_request`) y reservar quince minutos con el equipo de UTILIA (`list_sales_call_slots`, `book_sales_call`, `get_my_sales_call`, `cancel_sales_call`, `reschedule_sales_call`). Las tres escrituras de la reserva exigen `confirm: true`.

- **Dos ámbitos transversales nuevos.** `org:plan:request` lo expanden los conjuntos `:write` y `:full` de los siete dominios, nunca los `:read`; `org:plan:manage_requests`, solo los `:full`. La herramienta deja de anunciarse como de solo lectura, porque seis de sus doce operaciones escriben.
- **Categoría de avisos nueva** en la herramienta `notifications`: `planRequests` («Peticiones de plan»).
- **No entra en los SDK ni en la API externa**: una clave de API no es una persona, no tiene administrador a quien pedirle nada y no puede sentarse en una videollamada.

El detalle está en [MCP: pedir plan y reservar llamada](https://os.utilia.ai/docs/developers/mcp/plan-and-calls).

### Saber si la organización puede cobrar antes de intentarlo

Una aplicación externa ya no tiene que enterarse por un error de que la organización no puede cobrar con tarjeta.

- **Endpoint nuevo `GET /external/v1/payment-methods/readiness`.** Devuelve `platformAvailable`, `organizationCanCharge`, `reason` y `livemode`. Los seis motivos son estables: `PLATFORM_DISABLED`, `NO_ACCOUNT`, `ONBOARDING_INCOMPLETE`, `CHARGES_DISABLED`, `ACCOUNT_DEAUTHORIZED` y `READY`. Exige los aplicativos Finanzas y Stripe y el permiso `billing:payment-methods:read`; no estrena ningún permiso.
- **En los dos SDK**: `sdk.paymentMethods.getReadiness()` en JavaScript y `payment_methods.get_readiness()` en Python, en las variantes asíncrona y síncrona. Van en la **4.0.0** de los dos SDK, junto con la retirada del correo al proveedor que se describe abajo.
- **Es el mismo criterio que usa el panel.** Antes convivían tres formas distintas de decidir si se podía cobrar y se contradecían entre ellas. Ahora hay una, y la sirven este endpoint y las pantallas del panel.

### El cobro con tarjeta se gatea por su propio aplicativo

Cobrar una factura, confirmar el cobro, devolverlo por la pasarela y gestionar las tarjetas guardadas exigen ahora el aplicativo **«Pagos con tarjeta (Stripe)»** en el plan de la organización, además de Finanzas. Sin él, la respuesta es `403` con `errorCode: 'MODULE_NOT_ENABLED'`.

Alcanza también a las herramientas MCP equivalentes: la herramienta `payment_methods` entera, `crm_invoices_workflow` en `charge`, `charge_confirm`, `refund_create`, `payment_intent_cancel` y `payment_intents_list`, y `crm_clients_info:stripe_readiness`.

**No lo exigen**, y es deliberado: el reembolso registrado a mano (`refund_manual`), los listados que incluyen reembolsos manuales, y marcar una factura como cobrada o parcialmente cobrada. Registrar una devolución en efectivo no necesita pasarela y exigirla sería falso.

> **Atención: Los ámbitos del token no cambian**
>
> La puerta del plan es ortogonal al permiso del token: `crm:invoices:update` y `billing:payment-methods:*` siguen concediendo exactamente lo mismo que antes. Lo que cambia es que, además, la organización tiene que tener contratado el aplicativo. Un `403 MODULE_NOT_ENABLED` no se arregla pidiendo más permisos.

### MCP: el copiloto puede consultar el estado de los pagos con tarjeta

Operación nueva `finance_settings:get_stripe`, **solo lectura**. Devuelve la disponibilidad de la pasarela, el estado de la cuenta conectada y los requisitos pendientes ya traducidos al castellano. Ámbito `finance:settings:view`, el mismo que el resto de lecturas de ajustes de Finanzas; permiso del usuario: configurar o ver los pagos con tarjeta; aplicativos Finanzas y Stripe.

No hay ni habrá alta ni desconexión por MCP: el alta exige que una persona teclee en un formulario alojado por Stripe con verificación de identidad, y la desconexión es en la práctica irreversible.

### RETIRADO: `PATCH /finance/stripe/account/publishable-key` devuelve 410

Nunca tuvo efecto. La clave publicable que usa el producto es la de la plataforma UTILIA OS, no una de la cuenta conectada, así que no había nada que guardar. No lo llamaba ni el panel, ni los SDK, ni las herramientas MCP. Responde `410 Gone` de forma permanente y no tiene sustituto.

### SDK 4.0.0: el método de disposición de cobro y la retirada del correo al proveedor

Versión **MAYOR** en los dos SDK. Lo que obliga a subir de mayor no es el método nuevo, sino la retirada: los tres métodos que desaparecen se publicaron en la 2.28.0 y siguen vivos en la 3.1.0, que está en npm y en PyPI, así que quitarlos rompe a quien los use.

- **Añade** `sdk.paymentMethods.getReadiness()` (`payment_methods.get_readiness()` en Python, en las dos variantes), con los tipos `ChargingReadiness` y `ChargingReadinessReason`.
- **Retira sin sustituto** los métodos del correo al proveedor `getEmailRecipients`, `sendEmail` y `replyEmail`, y con ellos los tipos `SupplierEmailRecipients`, `SupplierSendEmailInput` y `SupplierReplyEmailInput`.

**Cómo actualizar.** Si solo dejabas constancia de un correo que enviabas tú, usa `logInteraction` con `type: 'EMAIL'`, que no cambia. Si de verdad enviabas el correo a través de UTILIA OS, esa capacidad ya no existe en el producto. No hay ninguna otra ruptura en esta versión: la tabla completa está en [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions#novedades-de-la-versión-400).

### RUPTURA: se retira el correo al proveedor

El envío de correo desde la actividad del proveedor **desaparece del producto**, y con él su superficie de integración. No hay sustituto: para dejar constancia de un correo que se mandó por otra vía sigue estando `log_interaction` con `type: 'EMAIL'`.

- **Se retiran tres rutas REST**: `GET /finance/suppliers/:supplierId/interactions/email-recipients`, `POST /finance/suppliers/:supplierId/interactions/send-email` y `POST /finance/suppliers/:supplierId/interactions/:interactionId/reply`.
- **Se retiran tres operaciones MCP** de la herramienta `suppliers`: `get_email_recipients`, `send_email` y `reply_email`, junto con su parámetro `emailMessage`. El resto de la actividad (registrar, conversación, historial, adjuntos, lote y reuniones) no cambia.
- **Se retiran de los dos SDK** los métodos del correo al proveedor `getEmailRecipients`, `sendEmail` y `replyEmail` (`get_email_recipients`, `send_email` y `reply_email` en Python) y sus tipos de entrada y salida.
- Los ámbitos `finance:suppliers:interactions:read` y `:manage` **siguen existiendo** y conservan todo lo demás: solo dejan de conceder lo relativo al correo.

## 2026-09-05

### SDK 3.0.0: la facturación y el cobro de una aplicación externa funcionan de verdad

Versión **mayor** en los dos SDK. La razón principal no es el volumen: es que cinco
servicios que existían y **devolvían 401 siempre** pasan a funcionar contra otra
ruta, y uno desaparece. La tabla de rupturas completa está en
[Versiones y compatibilidad del SDK](https://os.utilia.ai/docs/developers/sdk/versions).

- **`sdk.subscriptions` funciona**, sobre `/external/v1/subscriptions`. Hasta ahora atacaba una ruta de la API del panel. Firmas nuevas: `clientId` desaparece y `userId` es obligatorio. Se retiran `remove` (la baja es `cancel`) y `stats`.
- **`sdk.paymentAuthorizations` es nuevo** (`sdk.payment_authorizations` en Python), y ahora sí **crea** autorizaciones desde la aplicación, con su prueba legal. `sdk.mandates` queda como alias en desuso; `listForClient` se retira.
- **`sdk.invoices` gana cinco acciones**: `send`, `charge`, `markPaid`, `getPublicLink` y `regeneratePublicLink`.
- **`sdk.invoices.scheduledCharges` funciona** sobre la API externa. En Python se reubica bajo `sdk.invoices` con alias en la raíz, y se retira `retrieve`, cuya ruta no existía.
- **`sdk.invoices.refunds.create`** emite el reembolso. `cancelPreview` y `cancel` se retiran: deshacer un reembolso es del equipo.
- **`sdk.invoices.rectifications` se retira** con un error que dice qué usar. `sdk.invoices.getPdfUrl` también (sustituto: `regeneratePublicLink`). Si tu integración lo usó, regenera la clave de API de la aplicación.
- **`sdk.webhooks.verify` y `.parse`** verifican cualquier familia de avisos, con tipos por evento. `sdk.chat.webhooks.verify` queda como alias.
- **Enumerados derivados del esquema real**: `InvoiceStatus` pasa de 7 a 12 valores y `TaxSystem` suma `IPSI` e `INTERNATIONAL`. En Python, un enumerado incompleto reventaba con un error de validación en la primera factura reembolsada.

### API externa: suscripciones, autorizaciones de cobro, cobros programados y acciones sobre la factura

- **`POST /external/v1/subscriptions` y sus trece rutas.** Cuotas recurrentes de los usuarios de la aplicación, con generación, envío y cobro automáticos. Exige el aplicativo Finanzas y la capacidad de facturación recurrente.
- **`/external/v1/payment-authorizations`.** El consentimiento firmado que habilita cobrar sin el usuario delante, con texto canónico, versión, momento, IP y navegador. Exige Finanzas, Stripe y la capacidad de cobro automático.
- **`/external/v1/invoices/:id/scheduled-charges` y `/external/v1/scheduled-charges/:id`.** Cobros programados para una fecha futura, con cancelación y reintento adelantado.
- **Cinco acciones sobre una factura emitida**: `send` (envío por correo con su PDF), `mark-paid` (cobro fuera de la pasarela), `refund`, `public-link` (leer y emitir) y `charge` (cobro sin nadie delante).
- **`POST /external/v1/invoices` acepta `sendEmail`** y devuelve `emailSent`. Un `emailSent` en falso **no** es una factura fallida: está emitida y numerada, solo falló el correo.
- **Puente de identidad.** El usuario de la aplicación se materializa como ficha de cliente del CRM la primera vez que se factura, se guarda tarjeta o se crea una suscripción. `POST /users/identify` acepta `clientId` para vincularlo a una ficha creada antes.
- **`POST /users/identify` acepta los datos fiscales**: `taxId` y `billingAddress` (con `street`, `city`, `postalCode` y `country` obligatorios y `province` opcional). Se propagan a la ficha del CRM y a la de la pasarela. El alta de factura rellena con el receptor del documento los huecos que el usuario no tenga guardados, y lo que declare `identify` manda siempre.
- **`dayOfMonth` es opcional en una suscripción mensual**: sin él se toma el día de `startDate`, recortado a 28 cuando es posterior.

### CAMBIO QUE ROMPE: el puente solo alcanza las fichas de cliente que creó la propia aplicación

Una aplicación externa solo puede reutilizar, converger o vincular fichas que
haya creado ella, con `POST /external/v1/clients` o con el propio puente. Una
ficha nativa del CRM, dada de alta por el equipo, queda fuera de su alcance:
`identify({ clientId })` la rechaza con `404 CLIENT_NOT_ACCESSIBLE` y la
deduplicación automática por identificador fiscal o correo tampoco la
reutiliza. Cuando la coincidencia cae en una ficha nativa se crea una ficha
nueva y el hallazgo se registra en el histórico de auditoría.

Unir una ficha de la aplicación con una ficha nativa sigue siendo posible desde
el panel, con sesión iniciada, permiso y alcance.

### CAMBIO QUE ROMPE: `send` solo escribe a direcciones conocidas de la factura

En `POST /external/v1/invoices/:id/send`, `to` y `cc`
solo aceptan direcciones ya registradas de esa factura: contactos de facturación
activos del cliente puente, contactos del CRM marcados para recibir facturas, el
correo del usuario externo y el destinatario con el que se emitió. Cualquier otra
rechaza el envío entero con `422 INVOICE_EMAIL_RECIPIENT_NOT_ALLOWED`.

Hay además un cupo de **100 envíos por hora y aplicación**: superado, `429
RATE_LIMIT_EXCEEDED` con la cabecera `Retry-After`.

### CAMBIO QUE ROMPE: las lecturas de una aplicación ya no traen sus claves

`GET /integrations/apps` y `GET /integrations/apps/:id` ya no devuelven ninguna
clave: traen `webhookSecretConfigured`, un booleano, y de la clave de API solo su
prefijo, su nombre, su estado y sus datos de uso. Las claves se entregan solo al
crearlas y al regenerarlas.

### El enlace público que emite la aplicación solo permite ver y pagar

`allowedActions` es siempre `["VIEW", "PAY"]` en los enlaces emitidos por la API
externa, también cuando la factura viene de una suscripción. Firmar o retirar
autorizaciones de cobro no es posible desde ese enlace. El enlace que reparte el equipo desde
el panel no cambia. En un cobro con `REQUIRES_ACTION`, el enlace que se entrega
es adicional y **no revoca** el que el usuario ya tuviera.

### Dos códigos de error nuevos donde antes había uno confuso

- **`AUTO_CHARGE_DISABLED_FOR_ORGANIZATION`** (409, sin `nextStep`): la organización tiene apagado el cobro automático con tarjeta. Antes salía como `PAYMENT_AUTHORIZATION_REQUIRED`, que mandaba a la aplicación a recoger un consentimiento que no desbloqueaba nada. Lo enciende el equipo de la organización en los ajustes de Finanzas.
- **`SCHEDULED_CHARGE_RETRY_TOO_SOON`** (409) ya se emite. Estaba declarado y no lo lanzaba nadie: el tope de cinco reintentos manuales por factura cada 24 horas respondía con un 409 sin código.

### La configuración de facturación de una aplicación deja de exigir la pasarela

`GET` y `PATCH /integrations/apps/:id/billing-config` se gobiernan con
**Integraciones** y **Finanzas**, sin Stripe, igual que `billing-summary`.
Emitir facturas es capacidad de Finanzas y funciona sin pasarela: una
organización con Finanzas y sin Stripe factura por transferencia y registra el
cobro a mano. Antes, la pantalla le mostraba un candado que prometía un plan
superior para algo que su plan ya incluía. Los cobros con tarjeta siguen
exigiendo Stripe.

### El permiso «Emitir facturas» deja de anunciarse como reservado

El catálogo describía `crm:invoices:issue` como un alias reservado que «todavía
no es exigido por ningún endpoint» y remitía a `POST /external/v1/invoices/:id/issue`.
Las dos cosas eran falsas: ese endpoint no existe y `POST /external/v1/invoices`
exige el permiso desde el primer día. Crear una factura necesita `crm:invoices:write`
**y** `crm:invoices:issue`; el texto de «Crear facturas» lo advierte ahora.

### CAMBIO QUE ROMPE: `POST /external/v1/users/identify` exige permiso

Desde esta fecha exige **`crm:users:write`**. Si tu aplicación ya usaba
`identify`, concédele ese permiso **antes** de desplegar.

### CAMBIO QUE ROMPE: se retira el periodo de gracia de los permisos

El periodo de transición de los permisos **ha terminado**. El permiso se exige
en todas las llamadas, también en aplicaciones creadas hace tiempo.

### Permisos: 56, con una categoría nueva

- **Once permisos nuevos**: `crm:invoices:send`, `crm:users:write`, `billing:refund`, los tres de suscripciones, los tres de autorizaciones de cobro y los dos de cobros programados. El catálogo pasa de 45 a **56**.
- **Categoría `financial-critical`**, para lo que mueve dinero o corta un ingreso: `billing:charge`, `billing:refund`, `billing:subscriptions:cancel`, `billing:payment-authorizations:write` y `crm:invoices:cancel`. Mismas exigencias al conceder que los críticos de datos personales, con distinto motivo de blindaje, porque quien revisa la solicitud necesita saber por qué está protegido.
- **El catálogo se recorta por el plan.** Cada entrada declara qué aplicativos y capacidades exige, UTILIA OS lo recorta antes de servirlo y conceder un permiso fuera de plan responde `400 SCOPE_OUT_OF_PLAN`. Un cambio de plan nunca borra permisos ya concedidos: dejan de resolverse.

### Avisos: dieciséis eventos nuevos en dos dominios

- **Suscripciones** (ocho): `SUBSCRIPTION_CREATED`, `_UPDATED`, `_PAUSED`, `_RESUMED`, `_CANCELLED`, `_COMPLETED`, `_INVOICE_GENERATED` y `_PAYMENT_AUTHORIZATION_PENDING`.
- **Cobros programados** (ocho): `CHARGE_SCHEDULED`, `_STARTED`, `_SUCCEEDED`, `_FAILED`, `_REQUIRES_ACTION`, `_RETRY_SCHEDULED`, `_RETRIES_EXHAUSTED` y `_CANCELLED`.
- **Dos más en dominios existentes**: `PAYMENT_METHOD_EXPIRING` y `PAYMENT_AUTHORIZATION_CONSUMED`.
- **`INVOICE_SENT` ya se emite.** Estaba declarado desde el principio y nunca llegaba a nadie.
- **`PAYMENT_AUTHORIZATION_GRANTED` y `_REVOKED` pasan a dirigidos** cuando el recurso pertenece a un usuario de una aplicación externa. Antes viajaban siempre por difusión.
- **En `CHARGE_REQUIRES_ACTION`, la dirección del enlace solo viaja a veces**: sí cuando el cobro lo lanzó la aplicación, nulo cuando lo lanzó el motor de cobros programados, porque de un enlace emitido solo se guarda su huella. En los dos casos llega `publicLinkAvailable`.

## 2026-09-03

### Ámbitos: el portal del cliente sale de «Modificar contactos»

- **CAMBIO QUE ROMPE: `portal_access` exige ámbitos propios.** Sus lecturas exigen **`crm:portal:view`** y sus diez escrituras **`crm:portal:manage`**; ya no basta con `crm:contacts:update`.
- **Sin red de compatibilidad, a propósito.** Los tokens con `crm:full` siguen funcionando sin tocar nada; los demás se reautorizan pidiendo la familia nueva, que el descubrimiento del servidor de CRM ya publica.
- **La lectura queda fuera de `crm:read`** pese a terminar en `:view`, y la gestión fuera de `crm:write`. Mismo criterio que las personas de contacto del proveedor.

### Fusión de duplicados: cuatro ámbitos nuevos que ningún paquete expande

- **`crm:clients:merge`, `crm:contacts:merge` y `crm:leads:merge`.** Gobiernan las operaciones `deduplicate` y `merge` de clientes, contactos y leads. Fusionar archiva o borra fichas y reescribe sus punteros en medio centenar de tablas, sin vuelta atrás desde la aplicación.
- **Quedan fuera de `crm:read`, `crm:write` y también de `crm:full`**, aunque ese paquete expanda su dominio por prefijo. La exclusión es explícita: dejarlos entrar por prefijo se lo habría concedido, sin ningún acto nuevo del titular, a toda integración autorizada hace meses con `crm:full`. Se piden por su nombre.
- **Las previas por reglas siguen siendo baratas.** `deduplicate_preview` de clientes y contactos es lectura pura bajo el ámbito de ver, así que se puede enseñar el trabajo pendiente a una persona sin pedir el de fusionar. La previa de `merge`, en cambio, la ejecuta la misma operación destructiva y exige el ámbito de fusión.
- **`dryRun` es lo único que distingue una previa de una fusión aplicada**: las dos respuestas tienen la misma forma. Quien no lo mire dará por fusionado lo que sigue intacto.
- **`crm_clients: merge` añade dos banderas que exigen alcance total** sobre el permiso de fusionar: `allowDifferentTaxId` (dos identificadores fiscales distintos son la misma empresa, sin excepción si las dos fichas tienen facturas emitidas) y `allowPortalAccessMerge` (los interlocutores externos de una empresa pasan a ver los documentos de la otra).

### SDK JavaScript 2.27.0 y SDK Python 2.27.0

- **`sdk.mcp.crm` es un servicio nuevo**, contra `POST /api/mcp/crm`, con los sub-espacios `clients`, `contacts` y `leads`: altas y ediciones sueltas y en lotes de hasta 50, duplicados, fusión con su previa, búsqueda de datos de empresa en internet, consentimiento de correo del contacto y transiciones del lead.
- **El CRM gana la fusión de duplicados en los dos SDK**: `deduplicatePreview`, `deduplicate`, `mergePreview` y `merge` en clientes y contactos, y `mergePreview` / `merge` en leads. Los métodos de fusión envían la confirmación por ti; los de previa, no, y ahí está toda la diferencia.
- **Guía nueva de punta a punta**: [Poner al día una cartera de clientes](https://os.utilia.ai/docs/developers/mcp/client-portfolio), con lotes, logotipos, fusión con previa y espejo incremental por cursor, en los dos SDK.

## 2026-09-02

### SDK JavaScript 2.26.0 y SDK Python 2.26.0: buscador global y avisos

- **Dos herramientas MCP transversales nuevas.** `sdk.mcp.search` (buscador global del ecosistema, publicado en los servidores de gestión del trabajo, CRM y Finanzas) y `sdk.mcp.notifications` (avisos de la persona que autorizó el token y sus preferencias, en los siete servidores). Tienen [página propia](https://os.utilia.ai/docs/developers/mcp/search-and-notifications).
- **Los datos bancarios y fiscales dejan de viajar por defecto.** Ámbitos nuevos `finance:suppliers:view-sensitive` y `crm:clients:view-sensitive`, fuera de los conjuntos de lectura y escritura. Los tokens emitidos antes los conservan hasta el **2 de marzo de 2027**.
- **REGRESIÓN DECLARADA: `dev:write` y `crm:write` dejan de borrar** archivos, versiones y carpetas del gestor. Para borrar hace falta `dev:files:full` o `crm:files:delete`. En contrapartida, `dev:write` gana `dev:files:manage`.
- **Vincular archivos a una ficha exige `FILE_MANAGER_LINK_CRM`**, no `CLIENTS_EDIT`, y las herramientas de archivos admiten trece tipos de ficha en vez de siete.
- **El cliente MCP deja de tragarse el mensaje de los errores de una tool**: lee las tres formas que conviven, añade la pista al mensaje y conserva el código original en `errorCode`.

### SDK JavaScript 2.25.0 y SDK Python 2.25.0: fusión con superviviente elegido

- **La fusión de duplicados deja de imponer quién sobrevive**: `deduplicate({ survivorIds })` elige como mucho uno por grupo, y llamar sin argumentos se comporta exactamente igual que antes. Un identificador que no manda sobre nada es un **error**, no un valor descartado en silencio: la operación es todo o nada.
- **La previa devuelve por fin los números de todos los miembros**, el superviviente propuesto incluido: sin ellos no se puede saber qué se movería al elegir a otro.

## 2026-09-01

### SDK JavaScript 2.24.0 y SDK Python 2.24.0: las personas del proveedor dejan de ser contactos del CRM

- **CAMBIO DE SUPERFICIE.** Las personas de contacto de un proveedor pasan a ser una entidad propia de Finanzas que vive dentro del proveedor, con su propio identificador y sus propios permisos. El motivo es de producto: hasta entonces, para ver con quién se habla en un proveedor hacía falta tener contratado el CRM.
- **`linkContact` y `unlinkContact` desaparecen** y no se sustituyen por nada: ya no hay nada que atar ni que soltar. El equivalente es `createContact` para el alta y `deleteContact` para la baja.
- **`getContact` y `deleteContact` son nuevos**, y `updateContact` cambia de firma: direcciona la persona por su propio identificador, no por el par proveedor más contacto.

## 2026-08-05

### SDK JavaScript 2.23.0 y SDK Python 2.23.0: campañas, listas y entregabilidad de Marketing

- **`sdk.mcp.marketing` pasa a cubrir SEIS herramientas enteras.** Se añaden
  `campaigns` (`marketing_campaigns`, 35 operaciones), `lists`
  (`marketing_lists`, 17) y `deliverability` (`marketing_deliverability`, 10), y
  `reports` se completa con `complaints`, `linkHeatmap` y `audienceTech` (doce
  en total). Los mismos nombres en `snake_case` en Python, en variante
  asíncrona y síncrona.
- **Qué hizo la audiencia y segundos intentos**: `engagementBreakdown`
  (gratuito), `resendPreview`, `createResend` y `listResends`. Los siete grupos
  **no son porciones de una tarta** —`opened` ya contiene a `clicked`— y
  **«no abrieron» excluye siempre a quien pulsó**. Un segundo intento nunca
  escribe a quien se quejó, exige un asunto distinto del original, admite como
  mucho dos por campaña y **no toca los contadores de la campaña original**.
- **`listRecipients` deja de perder el filtro de EXCLUIDOS** (`isExcluded`) y
  gana `engagement`, ortogonal al resultado de envío.
- **Interés de los suscriptores y apartado de inactivos**: `listSubscribers`
  (que no existía en MCP, así que apartar era un viaje de ida),
  `engagementSummary`, `setAsideInactive` (vista previa por defecto) y
  `reactivateSubscriber`. **Apartar no es dar de baja**: no se toca el
  consentimiento.
- **Política de quejas**: `getComplaintPolicy` y `setComplaintPolicy`. El
  inquilino solo puede ser **más estricto** (10 a 30 puntos básicos); el techo
  del 0,3 % y el bloqueo del servidor compartido son de plataforma. Antes de un
  bloqueo hay siete días sostenidos y un aviso por correo con **48 horas** de
  antelación, y **se levanta solo** cuando la tasa baja.
- **La tasa de quejas es `null` cuando no hay forma de medirla y un SUELO
  cuando la hay.** Nunca es un cero.
- **HUECO ESCRITO**: `deviceType`, `emailClient` y `country` de
  `listRecipients` **se omiten** sin el permiso
  `EMAIL_MARKETING_VIEW_RECIPIENT_DETAIL` y el scope nuevo
  `marketing:recipients:detail:view`. La operación **responde igual** y llega
  `recipientDetailIncluded: false`: el recorte va por campo y no por operación
  para que ningún token ya emitido pierda la operación.
- **HUECO ESCRITO**: el mapa de calor por enlace **no existe para
  automatizaciones** ni para las campañas anteriores a la medición por enlace.
  Eso no es un cero. El desglose por dirección de destino (`clickThrough`)
  sigue disponible para todo y es gratuito.
- **Scope nuevo en el servidor Marketing**: `marketing:recipients:detail:view`.
  Documentado en [Descubrimiento OAuth](https://os.utilia.ai/docs/developers/oauth/discovery)
  y en [Servidor MCP Marketing](https://os.utilia.ai/docs/developers/mcp/marketing).

### SDK JavaScript y SDK Python: informes de Marketing

- **Nuevo `sdk.mcp.marketing.reports`**, la herramienta MCP `marketing_reports`
  **entera**: `campaignStats`, `engagementOverview`, `clickThrough`,
  `topCampaigns`, `engagementHeatmap` y `engagementTrends` (los mismos seis en
  `snake_case` en Python, en variante asíncrona y síncrona). Solo lectura y sin
  capacidad de plan. Detalle en
  [Servidor MCP Marketing](https://os.utilia.ai/docs/developers/mcp/marketing).
- **Las cifras de campaña cambian de base, no solo de nombre**: aparecen
  `accepted`, `delivered` (que vale `null` sin detección de devueltos),
  `acceptanceRate`, `clickToOpenRate`, `metricsAvailability` y
  `bounceDetectionEnabled`; `deliveryRate` queda sustituida por
  `acceptanceRate`, la tasa de rebote pasa a calcularse sobre los aceptados y el
  recuento de rebotados deja de incluir los fallos de salida. Quien tuviera
  cifras guardadas verá números distintos para las mismas campañas. Las
  definiciones, con su denominador, en
  [Definiciones de las métricas de Marketing](https://os.utilia.ai/docs/guides/marketing/metrics).
- **`sendStatus` deja de admitir `OPENED` y `CLICKED`** al filtrar
  destinatarios: nunca fueron estados de entrega y filtrar por ellos devolvía
  una lista vacía sin dar ningún error. Ahora responde con un error que lo
  explica. Usa las marcas de tiempo `openedAt` y `clickedAt`.
- **Los errores de plan se distinguen de los de permiso**: `isPlanError()` /
  `is_plan_error` y el objeto `planContext`. Ojo, por el carril MCP ese código
  no viaja: ver
  [Errores de plan](https://os.utilia.ai/docs/developers/sdk/plan-errors).

### SDK JavaScript y SDK Python: medición del origen y negocio por campaña

- **Nuevo `sdk.mcp.marketing.trackingSettings` / `tracking_settings`**, la
  herramienta MCP `marketing_tracking_settings` **entera desde su primera
  versión**: `get`, `update` y `previewLink` / `preview_link`. Configura qué
  etiquetas de origen se añaden a los enlaces de los correos y si viaja el
  identificador del destinatario. `previewLink` es una **función pura**: no
  firma nada y **no emite ningún identificador real**.
- **Scope OAuth nuevo `marketing:tracking:manage`**, que **no** entra en
  `marketing:write` y hay que pedir de forma explícita (o usar
  `marketing:full`). En la pantalla de autorización aparece con su propio
  bloque. Ver [Descubrimiento OAuth](https://os.utilia.ai/docs/developers/oauth/discovery)
  y [Pantalla de consentimiento](https://os.utilia.ai/docs/developers/oauth/consent-screen).
- **`sdk.mcp.marketing.reports` se completa con la atribución de ingresos**:
  `campaignAttribution`, `attributionSummary` y `entityOrigin` (los mismos en
  `snake_case` en Python). `topCampaigns` admite además `CONVERSIONS`
  (oportunidades originadas, **no** ventas cerradas), `REVENUE_ISSUED` y
  `REVENUE_COLLECTED`; hasta ahora `CONVERSIONS` devolvía un error diciendo que
  el producto no las registra.
- **Dos huecos que hay que leer antes de integrar.** Los **importes** de la
  atribución **no viajan por el servidor MCP de marketing** (su token no puede
  portar scopes de finanzas): llegan los recuentos y el bloque de dinero se
  declara en `excludedBlocks` con `reason: 'missing_scope'`, que **no es un
  cero**. Y las **facturas creadas con `sdk.invoices.create()`** (API externa
  v1) **nacen sin vínculo comercial y quedan fuera del negocio originado** por
  cualquier campaña, porque esa API no admite `opportunityId`.
- **Los formularios de captación ganan `captureUtm` y `utmPersistence`** en los
  tipos de `lead_forms_create` y `lead_forms_update`, y el envío público admite
  `_utm` y `_ur`. Aviso para quien incruste el widget: **se sirve con una caché
  de hasta 24 horas sobre una dirección sin versión**, así que los sitios ya
  publicados pueden tardar ese tiempo en tomar la versión nueva. Ver
  [Insertar un formulario en tu web](https://os.utilia.ai/docs/developers/lead-forms/embed).

### SDK JavaScript y SDK Python: enlaces con el dominio del cliente

- **Nuevo `sdk.mcp.marketing.trackingDomains` / `tracking_domains`**, la
  herramienta MCP `email_tracking_domains` **entera desde su primera versión**:
  `list`, `get`, `getSetupInstructions`, `create`, `verify`, `setEmission`,
  `retryCertificate` y `retire` (los mismos en `snake_case` en Python, en
  variante asíncrona y síncrona). Hace que los enlaces medidos de las campañas
  salgan por un subdominio del cliente en vez de por un dominio de UTILIA, que
  es lo último que quedaba con efecto real sobre la entregabilidad: los filtros
  antispam penalizan que los enlaces de un correo apunten a un dominio ajeno al
  remitente.
- **Ningún scope nuevo.** Se reutilizan `marketing:deliverability:view` y
  `marketing:deliverability:manage`, que ya existían. Las tres lecturas admiten
  además `marketing:campaigns:view`, porque el permiso que pide el REST es la
  vista del módulo y sin esa alternativa el token cerraría lo que el permiso
  abre.
- **Ninguna operación borra nada, y es una decisión.** `retire` **programa** el
  fin del servicio y devuelve en `serveUntil` / `serve_until` la fecha hasta la
  que ese subdominio sigue atendiendo los enlaces de los correos ya entregados.
  Un enlace de un correo ya enviado no se puede romper: lleva su dominio
  escrito dentro y no hay forma de reescribirlo.
- **`create` no empieza a medir.** El alta solo abre el proceso; la medición se
  enciende con `setEmission` cuando el dominio está sirviendo. Informar de que
  «ya se mide por su dominio» justo después de crearlo es falso.
- **Aviso que hay que trasladar al cliente**: si retira su registro CNAME, el
  certificado deja de poder renovarse y **a los 90 días como mucho los enlaces
  de sus correos YA ENVIADOS dejan de abrirse** con un aviso de seguridad del
  navegador. `certificateExpiresAt` / `certificate_expires_at` trae la fecha
  exacta. Ver
  [Enlaces con tu propio dominio](https://os.utilia.ai/docs/guides/marketing/custom-domain-links).
- **Puerta de plan**: `create`, `verify`, `set_emission` y `retry_certificate`
  exigen **las dos** capacidades del lote de medición, «Analítica avanzada de
  Marketing» y «Negocio por campaña». Quedan exentas las tres lecturas y
  `retire`, que es la salida: quien deja de tener las capacidades sigue viendo
  su subdominio y puede soltarlo cuando quiera.

---

## 2026-08-03

### SDK JavaScript 2.22.0 y SDK Python 2.22.0

- **Imágenes de presupuesto completas**. `sdk.budgets` gana `uploadImageToSection` / `upload_image_to_section` (colocar una imagen dentro de un apartado), `scanLegacyImages` / `scan_legacy_images` (detectar las imágenes heredadas incrustadas en el HTML, que se ven en el editor y no salen en el PDF) y `migrateLegacyImages` / `migrate_legacy_images` (convertirlas en colocaciones reales, de forma idempotente y tolerando fallos parciales).
- **`textWrap` queda obsoleto**: el generador del PDF nunca lo aplicó, la imagen siempre ocupa su propia línea. Se sigue aceptando y el servidor lo ignora.
- **La opción `deleteFile` al borrar una imagen queda obsoleta**: el borrado del binario pasó a ser automático por recuento de referencias.
- Cambios aditivos, sin rupturas de contrato.

---

## 2026-07-28

### SDK JavaScript 2.21.0 y SDK Python 2.21.0

- `sdk.oauth.getAuthorizedApps()` / `get_authorized_apps()` devuelve los permisos **ya traducidos y recortados por el plan** de la organización: `permissions`, `permissionCount`, `unavailablePermissionCount` y `unavailableModules`. Todos los campos son opcionales, para no romper contra un backend anterior.
- **`AuthorizedApp.scope` queda obsoleto (retirada prevista el 2027-02-01) y cambia de significado**: ya no es el universo de scopes concedidos a la aplicación, sino ese universo recortado por el plan. Quien lo use para decidir qué puede hacer la aplicación verá menos de lo que hay; sustitúyelo por `permissions` y `unavailablePermissionCount`.

---

## 2026-07-12

### SDK JavaScript 2.20.0 y SDK Python 2.20.0

- **Plataforma de chat extensible**: nuevo servicio `sdk.chat` sobre `/external/v1/chat`, con publicación de mensajes como aplicación (v1) y lectura de canales, miembros e historial visible (v2), más `sdk.chat.webhooks.verify()` para validar localmente la firma HMAC.
- Detalle completo en [Plataforma de chat](https://os.utilia.ai/docs/developers/chat-platform/overview).

---

## 2026-07-05

### API externa de clientes: grupos de empresas

- El CRM incorpora los **grupos de empresas** (matriz y filiales, dos niveles) y la API externa `/external/v1/clients` los refleja de forma aditiva: `parentClientId` en todas las respuestas, `parent { id, name, slug }` en el detalle y el endpoint nuevo `GET /external/v1/clients/:id/group`.
- `PATCH /external/v1/clients/:id` acepta `parentClientId` para vincular una filial a su matriz o desvincularla con `null`. Exige el **scope nuevo `crm:clients:write:hierarchy`** además de `crm:clients:write`; las API Keys existentes NO lo obtienen automáticamente. El catálogo de scopes pasa de 35 a **36**.
- **Webhooks nuevos** `CLIENT_LINKED_TO_GROUP` y `CLIENT_UNLINKED_FROM_GROUP`, con payload de solo identificadores. La mutación del vínculo NO dispara `CLIENT_UPDATED`.
- Errores nuevos: `409 CLIENT_HIERARCHY_CONFLICT` y `422 CLIENT_HIERARCHY_INVALID_TYPE`.
- Detalle en [Endpoints de clientes del CRM](https://os.utilia.ai/docs/developers/api/clients) y [Gestión de scopes](https://os.utilia.ai/docs/developers/api/scopes).

### SDK JavaScript 2.18.0 y SDK Python 2.18.0

- `sdk.externalClients.getGroup(id)` / `sdk.external_clients.get_group(client_id)` y soporte de `parentClientId` en `update` (en Python, `parent_client_id` con `None` explícito para desvincular).
- Tipos y modelos nuevos: `ExternalClientParent`, `ExternalClientHierarchyRole`, `ExternalClientGroupResponse`. Espejo del catálogo de scopes actualizado a 36 entradas.
- Cambios aditivos, sin cambios incompatibles. Matriz completa en [Versiones y compatibilidad del SDK](https://os.utilia.ai/docs/developers/sdk/versions).

---

## 2026-07-02

### SDK JavaScript 2.17.0 y SDK Python 2.17.0

- Nuevo método `sdk.mcp.opportunities.listStages()` / `list_stages()`: devuelve las etapas del embudo por defecto de la organización, cuyos `id` son los valores válidos para cambiar de etapa una oportunidad.

---

## 2026-05-27

### Avisos: la firma HMAC pasa a calcularse sobre `timestamp.rawBody` (cambio que rompe)

- **Todas las entregas de webhook cambian de mensaje firmado.** Hasta esta fecha
  la cabecera `X-Utilia-Signature` se calculaba sobre el cuerpo crudo a secas;
  desde este despliegue se calcula sobre la cadena `` `${timestamp}.${rawBody}` ``,
  donde `timestamp` es exactamente el valor de la cabecera `X-Utilia-Timestamp`
  (milisegundos Unix). Es el patrón que popularizó Stripe y añade defensa
  anti-replay.
- **Rompe a todo receptor que no se actualice.** Un receptor que siga firmando
  solo el cuerpo obtendrá siempre una firma distinta y rechazará las entregas
  legítimas (normalmente con 401). Como un 4xx se trata como fallo permanente,
  la entrega se cierra sin agotar los reintentos y el evento se pierde.
- **Qué hay que cambiar en tu servidor**: reconstruye el mensaje como
  `` `${timestamp}.${rawBody}` `` antes de calcular el HMAC-SHA256, comprueba que
  `X-Utilia-Timestamp` está dentro de una ventana de ±5 minutos y compara en
  tiempo constante. Ejemplos completos en Node.js y Python en
  [Firma HMAC de los avisos](https://os.utilia.ai/docs/developers/webhooks/hmac-signature).
- **El envío de prueba del panel no acompañó al cambio en ese momento** y seguía
  firmando solo el cuerpo. Ya está corregido: hoy la prueba firma exactamente
  igual que las entregas reales. Detalle en
  [Entregas y reintentos de los avisos](https://os.utilia.ai/docs/developers/webhooks/delivery-and-retries).

---

## 2026-05-12

### MCP: la herramienta `tickets` puede crear tickets

- Nueva operación de creación de tickets de soporte vía MCP. Ya estaba disponible la lectura y la actualización; ahora también la creación.
- Quien abre el ticket es siempre la persona del token; no se puede abrir en nombre de otra.
- Se quita el HTML de `title` y `description` antes de guardarlos, para que no pueda ejecutarse código si el contenido se muestra como HTML.
- Límite propio del canal MCP: como mucho 10 tickets por hora y persona. El límite general de MCP (60 peticiones por minuto) sigue aplicándose por encima.
- Trazabilidad en la actividad del ticket: `TICKET_CREATED` registra el canal (`source: 'mcp'`) y el `oauthClientId` del token de origen.

### OAuth: el conjunto `support:write`

- El conjunto `support:write` ahora se expande a `support:tickets:create` además de `support:tickets:update`. Los clientes OAuth con `support:write` autorizado antes del 12 de mayo de 2026 pueden crear tickets sin volver a dar su consentimiento. Consulta los detalles en la [referencia de scopes](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources#conjuntos).

---

## 2026-04-08

### SDK JavaScript 1.8.0

- Nuevo parámetro `theme` en `getAuthorizationUrl()`, `getLoginUrl()` y `loginWithPopup()`: permite solicitar tema claro (`'light'`) u oscuro (`'dark'`) en la pantalla de consentimiento OAuth.

### SDK Python 0.7.0

- Nuevo parámetro `theme` en `get_authorization_url()` y `get_login_url()` (versión asíncrona y síncrona): permite solicitar tema claro (`'light'`) u oscuro (`'dark'`) en la pantalla de consentimiento OAuth.

---

## 2026-04-05

### Documentación de OAuth

- Página nueva para obtener un token en cinco minutos: [OAuth: tu primer token en 5 minutos](https://os.utilia.ai/docs/developers/oauth/quickstart).
- Página nueva de botones y marca, con logotipos, colores, tipografía y código del botón «Iniciar sesión con UTILIA».
- Página nueva de problemas frecuentes, con los diez fallos más comunes al integrar OAuth.
- Reescritas las seis páginas de referencia (visión general, descubrimiento, flujo de autorización, token y revocación, scopes y recursos, pantalla de consentimiento), con ejemplos en curl, JavaScript y Python.

---

## 2026-04-02

### OAuth en el panel de aplicaciones

- OAuth integrado en cada aplicación externa (en su ficha, **Configuración › Inicio de sesión**).
- Configuración del nombre, las direcciones de retorno y los scopes.
- Generación y regeneración de Client ID y Client Secret.
- Visualización y revocación de aplicaciones autorizadas.

### Soporte OIDC (OpenID Connect)

- Endpoint `/.well-known/openid-configuration` para descubrimiento automático.
- Endpoint `/api/oauth/userinfo` para obtener claims del usuario autenticado.
- Endpoint `/api/oauth/jwks` para verificación de firma de ID tokens.
- ID tokens JWT con claims estándar: `sub`, `name`, `email`, `picture`, etc.
- Scopes OIDC: `openid` (obligatorio), `profile`, `email`.

### SDK JavaScript 1.7.0

- Flujo OAuth 2.1 simplificado: `getAuthorizationUrl()` gestiona PKCE automáticamente.
- `handleCallback(code)`: recupera el code_verifier sin parámetros manuales.
- Validación automática del estado CSRF en `handleCallback`.
- `isAuthenticated()`: verifica si hay sesión OAuth activa.
- `logout()`: revoca tokens y limpia el almacenamiento.
- `loginWithPopup()`: inicio de sesión OAuth mediante ventana emergente para SPAs.
- `ErrorCode.AUTH_EXPIRED`: código de error nuevo para sesiones OAuth caducadas.
- Estado PKCE persistente en `MemoryTokenStorage` y `FileTokenStorage`.

### SDK Python 0.6.0

- Flujo OAuth 2.1 simplificado: `get_authorization_url()` gestiona PKCE automáticamente.
- `handle_callback(code)`: recupera el code_verifier sin parámetros manuales.
- Validación automática del estado CSRF en `handle_callback`.
- `is_authenticated()`: verifica si hay sesión OAuth activa.
- `logout()`: revoca tokens y limpia el almacenamiento.
- `ErrorCode.AUTH_EXPIRED`: nuevo código de error para sesiones OAuth expiradas.
- Corrección crítica: el cliente síncrono ahora comparte storage con el asíncrono.
- Estado PKCE persistente en `MemoryTokenStorage` y `FileTokenStorage`.

---

## Versiones vigentes

| Pieza | Versión |
| - | - |
| API externa | v1 |
| SDK de JavaScript (`@utilia-os/sdk-js`) | 4.8.0 en el repositorio; de la 4.5.0 a la 4.8.0, sin publicar; 4.1.0 publicada en npm |
| SDK de Python (`utilia-sdk`) | 4.8.0 en el repositorio; de la 4.5.0 a la 4.8.0, sin publicar; 4.2.0 publicada en PyPI |
| Widget (`@utilia-os/widget`) | 1.1.0 |
