# Versiones y compatibilidad

Matriz de versiones de la API externa, los SDK de JavaScript y Python y el widget, con lo que aporta cada versión y sus cambios incompatibles.

Esta página es la referencia única de versiones del ecosistema de integración de UTILIA OS: la API externa, los dos SDK oficiales y el widget. Úsala para fijar las dependencias de tu proyecto y para saber qué aporta cada versión antes de actualizar.

## Matriz de versiones

| Componente | Paquete | Versión en el árbol | Última versión publicada |
| - | - | -: | -: |
| API externa | `/external/v1` | v1 | v1 |
| SDK JavaScript | `@utilia-os/sdk-js` | 4.8.0 | 4.1.0 |
| SDK Python | `utilia-sdk` | 4.8.0 | 4.2.0 |
| Widget | `@utilia-os/widget` | 1.2.0 | 1.1.0 |

La 4.8.0 es la versión del repositorio en los dos lenguajes y está **sin publicar**. La 4.7.0, del 22 de octubre de 2026, y la 4.6.0 y la 4.5.0, del 21, tampoco se han publicado todavía. La última publicada, la que instalan `npm install @utilia-os/sdk-js` y `pip install utilia-sdk`, es la 4.1.0 en npm y la 4.2.0 en PyPI. La 4.1.1 no llegó a publicarse por separado: sus cambios van incluidos en la 4.2.0, y cada versión incluye las anteriores. Los dos SDK comparten numeración, para que la paridad entre lenguajes se mantenga versión a versión.

Antes de dar por hecho que un método existe, comprueba qué versión tienes con `npm ls @utilia-os/sdk-js` o `pip show utilia-sdk`: si vienes de la 3.x, la 4.0.0 retiró tres métodos y tu código puede depender de ellos.

### Lo que todavía no puedes instalar

Varias guías enseñan métodos de versiones sin publicar. Las rutas de la API funcionan igual sin ellos: mientras tanto, llámalas directamente con los ejemplos de `curl` de cada guía.

| Versión | Lo que usan las guías | npm | PyPI |
| - | - | - | - |
| 4.2.0 | `tickets.createStreamToken`, `onApiKeyExpires`, verificar avisos con varias claves y `budgetSignatures` con clave de API | No | Sí |
| 4.3.0 | Facturas simplificadas, `pricesIncludeTax`, el bloque `payment` y `invoices.rectifications.create` y `list` | No | No |
| 4.4.0 | `invoices.listSeries`, `seriesCode` e `invoices.getEuVatRates` | No | No |
| 4.5.0 | `invoices.validate`, `validatePdf`, `rectifications.validate`, `webhooks.signTestPayload`, los campos `displayNumber` y `emailDelivery` de las facturas y las claves de API de la organización por MCP (`orgKeys*`) | No | No |
| 4.6.0 | `sdk.diagnostics` (con `getMarkdown()` y `organizationApiKey()`), la orden `doctor`, `sdk.aiAnalytics`, `onIgnoredFields`, la política de reintentos nueva, `retryable` en los errores, `appDiagnosis()`, los tipos corregidos de `sdk.mcp.integrationsOverview`, la captura de errores (`errors.install`, `capture`) y su gestión (`reportBatch`, `get`, `resolve`, `reopen`, `silence`) | No | No |
| 4.7.0 | `appInstallations.exchange`, `list` y `get` (`app_installations` en Python) y `webhooks.verifyInstallation` (`verify_installation`), para las [instalaciones del catálogo](https://os.utilia.ai/docs/developers/apps-platform/installations-api) | No | No |
| 4.8.0 | La suscripción de las instalaciones de pago (`subscription`), el filtro `subscriptionStatus`, `isAccessAllowed` (`is_access_allowed`) y los tipos de los avisos `APP_SUBSCRIPTION_*` | No | No |

El widget 1.2.0, con la opción `userSignature` para la [verificación de identidad](https://os.utilia.ai/docs/developers/widget/identity-verification), tampoco está publicado: el que se instala es el 1.1.0, que no manda la firma.

## Novedades de la versión 4.8.0

La 4.8.0 está sin publicar en npm y en PyPI. Trae las suscripciones a las aplicaciones de pago del catálogo y necesita el servidor con los cobros de aplicaciones; contra uno anterior, `subscription` no llega. No hay métodos nuevos de escritura: comprar, cambiar de plan, cancelar o pagar se hace en UTILIA OS.

- **La suscripción de cada instalación.** En una ficha «De pago en UTILIA», `appInstallations.list()` y `get()` traen `subscription` (`ExternalAppSubscription`): plan, precio sin impuestos, estado (`TRIALING`, `ACTIVE`, `PAST_DUE`, `PAUSED` o `ENDED`), prueba, periodo, cancelación y cambio programado. `list({ subscriptionStatus })` (`subscription_status` en Python) filtra por su estado. 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).
- **`isAccessAllowed(instalacion)`** (`is_access_allowed`): si la empresa puede usar tu aplicación, mirando la instalación y su suscripción.
- **Avisos de la suscripción.** Doce avisos `APP_SUBSCRIPTION_*` en el dominio `integrations` (`APP_SUBSCRIPTION_WEBHOOK_EVENTS`) y su carga tipada, `AppSubscriptionWebhookData`. Ver [Los avisos de la suscripción](https://os.utilia.ai/docs/developers/apps-platform/webhooks#los-avisos-de-la-suscripción).
- **MCP.** `installationsGet()` e `installationsList()` traen `subscription`, sin importe si no puedes ver el plan de la empresa. Salen `listingsList()` y `listingsGet()`, que llegaron con la 4.7.0 sin publicarse: las fichas que publica una cuenta de desarrollador se gestionan en el servidor de desarrolladores.

## Novedades de la versión 4.6.0

La 4.6.0 está sin publicar en npm y en PyPI. `sdk.diagnostics`, la orden `doctor`, el diagnóstico de las claves de la organización y la orientación de los errores necesitan el servidor con el sistema de pruebas de la API, y la captura y la gestión de los errores, el servidor con `POST /external/v1/errors/batch` y las rutas de gestión; el resto funciona igual contra un servidor anterior. La guía de la captura está en [Capturar los errores de tu aplicación](https://os.utilia.ai/docs/developers/sdk/error-reporting). Todo es aditivo salvo los tipos de `sdk.mcp.integrationsOverview` y los cambios de la tabla de debajo.

- **Comprobar tu integración.** `sdk.diagnostics`, con la clave de tu aplicación y sin ningún permiso de la API: `me()`, `get()`, `calls()`, `checkRequest()`, `run()`, `sendWebhookSample()`, `checkWebhookReceiver()` y `openApi()`, sobre `GET /external/v1/me` y las rutas de `/external/v1/diagnostics`. En Python, los mismos en `snake_case` (`check_request`, `send_webhook_sample`, `check_webhook_receiver`, `open_api`), asíncronos y síncronos. Ver [Referencia de métodos](https://os.utilia.ai/docs/developers/sdk/reference#diagnostics).
- **La orden `doctor`.** `npx @utilia-os/sdk-js doctor` en JavaScript y `utilia-doctor` en Python, con `--run`, `--check "POST /external/v1/invoices" --body cuerpo.json`, `--json`, `--strict`, `--base-url` y `--allow-insecure-http`. Lee la clave de `UTILIA_API_KEY` (nunca la muestra) y sale con `0` si no hay nada que bloquee, `1` si lo hay (con `--strict`, también si hay avisos) y `2` si no se pudo comprobar. No manda la clave por `http://` salvo a este mismo equipo (`localhost`, `127.0.0.1` o `::1`). Pensada para tu asistente de IA y tu integración continua: ver [La orden doctor](https://os.utilia.ai/docs/developers/getting-started/ai-assistants#la-orden-doctor).
- **Instrucciones para tu asistente de IA.** `diagnostics.getMarkdown()` (`get_markdown()`) pide `GET /external/v1/diagnostics?format=markdown` y devuelve el texto, sin la clave.
- **Diagnóstico de las claves de API de la organización.** `diagnostics.organizationApiKey()` (`organization_api_key()`) pide [el diagnóstico de una clave `utk_`](https://os.utilia.ai/docs/developers/organization-api-keys/diagnostics). Una clave `utk_` pasada en `apiKey` viaja ahora en `Authorization: Bearer`, que es donde la acepta el servidor (antes iba en `X-Api-Key` y se rechazaba siempre), y `doctor` la reconoce por su prefijo.
- **Claves publicadas en una web.** Con la clave de una aplicación con dominios autorizados, `calls()`, `run()`, `sendWebhookSample()` y `checkWebhookReceiver()` responden `403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY` (constante nueva en los dos SDK) y el diagnóstico trae `credential.publishable`. `doctor --run` no lanza la prueba activa con esa clave: lo explica y no lo cuenta como fallo.
- **Campos que ya mandaba el servidor**: `traffic.hourly` del diagnóstico, `credential.publishable` de `me()`, y `payload` y `responseSnippet` de la muestra de un aviso. `DiagnosisAccessReason` gana `TENANT_SUSPENDED`, `TENANT_CANCELLED` y `PUBLISHABLE_KEY_CEILING`.
- **Uso de IA de tu aplicación.** `sdk.aiAnalytics.getOverview()` y `getTrends()` (`sdk.ai_analytics.get_overview()` y `get_trends()`), con `startDate` y `endDate` opcionales y el scope `analytics:ai:read`.
- **Comentarios de presupuesto con clave de API.** `budgetComments.list` y `create` van a `/external/v1/budgets/{budgetId}/comments`, siempre con visibilidad `CLIENT`. `update` y `remove` lanzan `METHOD_NOT_AVAILABLE_WITH_API_KEY` sin hacer la petición.
- **Campos descartados.** La opción `onIgnoredFields` (`on_ignored_fields`) recibe los campos que el servidor descartó sin error en una escritura (cabecera `X-Utilia-Ignored-Fields`); sin la opción, el SDK deja un aviso en la consola o en el registro `utilia_sdk`.
- **Errores que dicen quién lo arregla.** `UtiliaSDKError` gana `fixedBy`, `docsUrl`, `requiredScopes` y `retryAfterMs` (`fixed_by`, `docs_url`, `required_scopes` y `retry_after`, en segundos), y `retryable` llega del cuerpo de todo error de `/external/v1`, con el ayudante `readErrorGuidance` (`read_error_guidance`). Códigos tipados nuevos: `INSUFFICIENT_SCOPE` (con `isInsufficientScopeError`), `DIAGNOSTICS_RATE_LIMITED`, `DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY`, `DIAGNOSTICS_UNAVAILABLE`, `BUDGET_ERROR_CODES` y, entre los de las credenciales, `API_KEY_WRONG_KIND`, `API_KEY_WRONG_HEADER` y `AUTH_TOKEN_INVALID`. `INSUFFICIENT_SCOPE_MESSAGE` queda en desuso: el servidor ya nombra el permiso que falta.
- **Catálogo público de errores.** `PUBLIC_API_ERROR_CATALOG` trae cada `errorCode` de la API con quién lo arregla y si se puede repetir, comprobado contra el del servidor. `REQUEST_ERROR_CODES` tipa los errores de la forma de la petición (`INVALID_JSON`, `JSON_CONTENT_TYPE_REQUIRED`, `METHOD_NOT_ALLOWED` y `REQUEST_BODY_TOO_LARGE`), y los de facturas ganan `INVOICE_PDF_PENDING`, `INVOICE_PDF_REVIEW_REQUIRED`, `INVOICE_PDF_STORAGE_UNAVAILABLE`, `INVOICE_NUMBERING_UNAVAILABLE`, `INVOICE_ISSUANCE_REVIEW_REQUIRED` y `RECTIFICATION_OPERATION_INCOMPLETE`.
- **Avisos tipados con sus nombres reales.** `ChatWebhookEvent` lleva los nombres que manda el servidor (`CHAT_MESSAGE_CREATED`…) y llegan `ALL_WEBHOOK_EVENTS`, `WEBHOOK_EVENTS_BY_DOMAIN`, `WebhookEventName` e `isWebhookEventName` (`is_webhook_event_name`), comprobados contra el catálogo del servidor.
- **Datos fiscales de los usuarios.** Permiso nuevo `crm:users:read:fiscal-data`: sin él, `users.list` no trae el NIF ni la dirección de facturación, e `identify` solo los que mandas en esa llamada. `ExternalUser` los declara opcionales. Ver [Usuarios externos](https://os.utilia.ai/docs/developers/api/users#qué-devuelve-identify).
- **Claves dentro de una página web.** Constantes nuevas `SCOPE_NOT_ALLOWED_FROM_BROWSER` (403, con los permisos en `error.details.notAllowedScopes`) y `SCOPE_NOT_ALLOWED_WITH_ALLOWED_ORIGINS` (400), y el hallazgo `PUBLISHABLE_KEY_SENSITIVE_SCOPES`, que `doctor` enseña como aviso. 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).
- **Catálogo de permisos.** Los nombres y las descripciones de `EXTERNAL_API_SCOPE_DESCRIPTIONS` son copia de los que enseña el panel. Los valores de los permisos no cambian: compara el valor, no el texto.
- **Tipos de `sdk.mcp.integrationsOverview` con la forma real del servidor.** Los de `status()`, `health()` y `quickstart()` prometían campos que el servidor no manda (`worstApp.name`, `credentials.v1Count`, `webhooks.last24hSuccessRate`, `events.catalogCount`…), y en Python `status()` fallaba al validar la respuesta. Ahora son copia de la respuesta: `health.worstApp.appName` (`worst_app.app_name`), `status.webhooks.last24h.successRate`, `events.catalogSize`, y los nuevos `apiCalls`, `appsByStatus` y `appsNeedingAttention`. Llega también `appDiagnosis(appId)` (`app_diagnosis`), el diagnóstico de una aplicación por MCP.
- **Captura de errores.** `errors.install` engancha lo que nadie trata (en el navegador, `error` y `unhandledrejection`; en Node, `uncaughtExceptionMonitor`; en Python, `sys.excepthook`, `threading.excepthook` y `asyncio`) y `errors.capture` (`capture_exception`) y `captureMessage` (`capture_message`) envían lo que atrapas. Nunca lanzan: van a una cola en memoria que envía en lotes de 25 por `POST /external/v1/errors/batch`, junta los repetidos, respeta `Retry-After` y no se reporta a sí misma. `flush` envía lo pendiente.
- **Más métodos de `errors`.** `reportBatch` (`report_batch`), `get`, `resolve`, `reopen`, `silence` y `unsilence`. La gestión exige el scope nuevo `analytics:errors:manage`: el catálogo pasa a **59**.
- **Respuestas que dicen la verdad.** `report` devuelve `accepted`, `status` (`created`, `updated` o `regressed`) o `reason`, y `count`. `SystemError` (`SystemErrorEntry`) gana `status`, `silencedUntil`, `environment`, `firstRelease`, `lastRelease`, `regressionCount` y `lastRegressedAt`, y `ErrorStats`, `silenced`.
- **Campos nuevos al enviar**: `release`, `environment`, `fingerprint` y `tags` en `ReportErrorInput`. `ErrorFilters` gana `status` y `firstRelease` (`first_release`), y en JavaScript `severity` admite una sola gravedad o una lista.
- **`ai.reportError`** (`report_error`) admite `release` y `environment` y hace un solo intento.
- **Corregido (JavaScript):** `errors.list` enviaba varias gravedades como `severity[]=` y el servidor ignoraba el filtro. Ahora van como parámetro repetido.

### Qué cambia si vienes de la 4.5

| Antes | Después | Qué hay que cambiar |
| - | - | - |
| Un `POST` fallido por la red, un 429 o un 5xx se repetía hasta tres veces | Un `POST` sin clave de idempotencia (campo `idempotencyKey` o, en JavaScript, la cabecera `Idempotency-Key`) se intenta una sola vez | Si una alta (ticket, mensaje, cliente, lead, archivo, IA) falla por la red, comprueba si se creó antes de repetirla |
| Ante un 429 esperaba 0,5 s y 1 s | Espera lo que pide `Retry-After`; `X-Utilia-Quota-Reset` solo en el 429 del cupo por hora agotado sin `Retry-After`, y los 5xx se repiten como siempre. Si la espera pasa de `maxRetryAfterMs` (`max_retry_after`, un minuto por defecto), no reintenta y el error trae `retryAfterMs`. Con `retryable: false` en el cuerpo, no repite nunca | Nada, salvo que quieras otro tope |
| Un 409 llegaba como `ErrorCode.UNKNOWN` | Llega como `ErrorCode.CONFLICT`. Es un cambio visible en una versión menor: el código que entraba por la rama de `UNKNOWN` deja de hacerlo | Compara con `ErrorCode.CONFLICT` o, mejor, con `statusCode` 409 (`status_code` en Python) y su `errorCode` (`BILLING_IDEMPOTENCY_CONFLICT`, `ROTATION_IN_PROGRESS`…), que no cambian entre versiones |
| La validación local del SDK hablaba en inglés (`title must be at least 5 characters long`) | Habla en castellano y con el criterio del servidor (`title: escribe al menos 5 caracteres (ahora tiene 3)`) | Si comparabas el texto, compara el código del error o el campo que nombra el mensaje |
| `budgetComments` con clave de API respondía 401 | `list` y `create` funcionan; `update` y `remove` lanzan antes de la petición | Contempla `METHOD_NOT_AVAILABLE_WITH_API_KEY` |
| `ChatWebhookEvent` declaraba nombres con puntos (`chat.message.created`) | Declara los reales (`CHAT_MESSAGE_CREATED`) | Si comparabas con los nombres con puntos, tu receptor no reaccionaba a ningún aviso: cámbialos |
| `subscriptions.retryCharge` pedía `billing:subscriptions:write` | Pide `billing:scheduled-charges:write`, porque reintentar un cobro mueve dinero; sin él, `403 INSUFFICIENT_SCOPE` | Pide a la organización que conceda «Programar cobros». La firma del método no cambia |
| `users.list` e `identify` traían el NIF y la dirección de facturación guardados | Solo con `crm:users:read:fiscal-data` | Pide ese permiso o guarda esos datos en tu sistema cuando los mandes |
| Los tipos de `sdk.mcp.integrationsOverview` declaraban `worstApp.name`, `webhooks.last24hSuccessRate` y otros campos que nunca llegaban | Declaran los que manda el servidor | El código que leía los nombres viejos deja de compilar: cámbialo por los nuevos (`worstApp?.appName`, `webhooks.last24h.successRate`…) |
| `errors.list` con varias gravedades devolvía todas (JavaScript) | Filtra de verdad | Nada, salvo que contaras con ver todas |
| `process.on('unhandledRejection', ...)` que llamaba a `errors.report` | `errors.install` | Quita ese manejador: provoca un bucle si el envío falla |
| Un error resuelto que volvía a ocurrir seguía resuelto | Se reabre solo y llega `ERROR_REOPENED` con `reason: regression` | Si escuchas `ERROR_REOPENED`, distingue `reason` |
| Un 4xx en el contexto respondía como registrado, con `hash` vacío | `accepted: false` con `reason: discarded_expected` | Mira `accepted` antes de usar el `hash` |
| El envío de errores gastaba el cupo por hora de la clave | Límite propio de 120 peticiones por minuto en cada ruta | Nada |

Los tres últimos cambios de los errores son del servidor, no del SDK: llegan igual con cualquier versión.

Las lecturas, `PUT`, `PATCH`, `DELETE` y los `POST` que solo consultan (`invoices.validate`, `users.identify`, el pase de conexión, `checkRequest`, `run` y el carril MCP) se reintentan como antes.

## Novedades de la versión 4.5.0

La 4.5.0 (2026-10-21) trae los métodos de las claves de API de la organización y el ensayo de facturas sin numerar, y retira los métodos que ninguna organización cliente podía usar. Está sin publicar en npm y en PyPI.

- **Claves de API de la organización por MCP.** `sdk.mcp.apiCredentials` gana `orgKeysList`, `orgKeysGet`, `orgKeysUsage`, `orgKeysCatalog`, `orgKeysCandidates`, `orgKeysCreate`, `orgKeysUpdate`, `orgKeysRotate`, `orgKeysEndTransition` y `orgKeysRevoke` (en Python, en `snake_case`). Crear, renovar y ampliar una clave piden la confirmación en el móvil: la primera llamada lanza `APPROVAL_REQUIRED` y se repite con `twoFactorApprovalId`. Ver las operaciones `org_keys_*` en la [referencia MCP](https://os.utilia.ai/docs/developers/mcp/reference#api_credentials).
- **Credenciales personales y de aplicación.** `create` y `rotate` admiten `twoFactorApprovalId`, que el servidor ya pedía. `create` ya no admite `kind: 'SERVICE_ACCOUNT'` y `CreateApiCredentialInput.userId` queda en desuso.
- **Probar sin facturas reales.** `invoices.validate`, `invoices.validatePdf` (`validate_pdf`), `invoices.rectifications.validate` y `webhooks.signTestPayload` (`sign_test_payload`), con los campos `displayNumber` y `emailDelivery` en los tipos de las facturas y `livemode` y `meta.test` en los avisos.
- **Listados del CRM por MCP.** El servidor ya no repite la lista en `clients`, `contacts` y `leads`; el SDK rellena ese alias desde `items`, así que el código que aún lo lee sigue funcionando.

### Qué cambia si vienes de la 4.4

| Antes | Después | Qué hay que cambiar |
| - | - | - |
| `apiCredentials.create` admitía `kind: 'SERVICE_ACCOUNT'` | Ya no: el servidor lo rechazaba | Para conectar un programa, usa `orgKeysCreate` |
| Métodos que ninguna organización cliente podía usar: `AnnouncementsService` y `sdk.mcp.announcements`, varios métodos de ajustes de `sdk.mcp.orgIntegrationsSettings` y el campo `isFirstParty` de las altas de OAuth | Ya no existen | Bórralos de tu código: respondían siempre «prohibido» |

## Novedades de la versión 4.4.0

La 4.4.0 (2026-10-15) trae las series de facturación de las aplicaciones. Es aditiva: sin `seriesCode` ni serie por defecto en tu aplicación, todo se numera exactamente igual que en la 4.3.

- **Series de tu aplicación.** Tu organización puede reservar a tu aplicación una o varias series (por ejemplo, `APP-202600001`), separadas de las facturas del equipo, y fijar cuál se usa por defecto. `invoices.listSeries()` (`list_series()`, asíncrono y síncrono) las consulta con `crm:invoices:read`, y `invoices.create` las elige con `seriesCode` (`series_code`) o `DEFAULT` para la de siempre. Tu aplicación no crea ni cambia series. Ver [Series disponibles](https://os.utilia.ai/docs/developers/api/invoices#series-disponibles).
- **Suscripciones**: `type` (ordinarias o simplificadas, estas solo con cuotas de hasta 400 € impuestos incluidos) y `seriesCode` en el alta y en el cambio. La serie queda fijada en la suscripción.
- **Respuestas ampliadas**: `seriesCode` en cada factura (también en las rectificativas, con `DEFAULT`) y `type` y `seriesCode` en cada suscripción. `invoices.list` filtra por `seriesCode`.
- **Fecha de expedición acotada**: `issueDate` no puede ser futura ni anterior a la de la última factura de la misma serie, y su año es el de la numeración (`INVOICE_ISSUE_DATE_OUT_OF_ORDER`).
- **Errores**: `INVOICE_ERROR_CODES` (`InvoiceErrorCode`) gana `INVOICE_SERIES_NOT_AVAILABLE`, `INVOICE_SERIES_TYPE_MISMATCH`, `INVOICE_SERIES_INACTIVE` e `INVOICE_ISSUE_DATE_OUT_OF_ORDER`, y los dos de la gestión de series de la organización (`INVOICE_SERIES_PREFIX_RESERVED`, `INVOICE_SERIES_IN_USE`).
- **MCP**: `appsBillingGetConfig` y `appsBillingUpdateConfig` (`apps_billing_get_config` y `apps_billing_update_config`) leen y fijan las series por defecto de la aplicación.
- **Ventanilla única del IVA (Próximamente).** El SDK trae ya el contrato de una función que todavía no está disponible para las organizaciones: `taxSystem: 'EU_OSS'`, líneas `EU_VAT` con `vatRateClass` y `ossSupplyType`, `consumerResidence.evidenceItems`, `invoices.getEuVatRates()` (`get_eu_vat_rates()`, asíncrono y síncrono), `taxSystem`, `consumerCountryCode` y `ossScheme` en las respuestas y los 14 códigos `OSS_*`. Mientras tanto, cualquier dato suyo responde 403 `FEATURE_COMING_SOON`, que se reconoce con `isFeatureComingSoonError` y `getComingSoonFeature` (`is_feature_coming_soon_error` y `get_coming_soon_feature`).
- **Permisos**: sin scopes nuevos; el catálogo sigue en **57**. La descripción de `crm:invoices:read` dice ahora que también permite consultar las series y, si la ventanilla única está disponible, los tipos de IVA de la Unión.

### Qué cambia si vienes de la 4.3

| Antes | Después | Qué hay que cambiar |
| - | - | - |
| `issueDate` libre, y el año del número salía del reloj | No puede ser futura ni anterior a la última factura de la misma serie; su año es el del número | Si mandas fechas pasadas o futuras, respeta el orden de la serie o captura `INVOICE_ISSUE_DATE_OUT_OF_ORDER` |
| Las facturas de la aplicación siempre en la serie general | En la serie por defecto de la aplicación, si tu organización le fija una | Nada, salvo que quieras la de siempre: manda `seriesCode: 'DEFAULT'` |
| `TaxSystem` sin `EU_OSS` | `TaxSystem` gana `EU_OSS`, y las respuestas traen `taxSystem`, `consumerCountryCode` y `ossScheme` | Si haces un `switch` exhaustivo sobre `TaxSystem`, añade el caso |

## Novedades de la versión 4.3.0

La 4.3.0 (2026-10-13) amplía la facturación de las aplicaciones que cobran con su propia pasarela. Todo es aditivo en la API externa.

- **Facturas simplificadas.** `invoices.create` admite `type: 'SIMPLIFIED'` (`type=InvoiceType.SIMPLIFIED`): serie propia (`T-`), hasta 400 € impuestos incluidos y destinatario opcional. Sin `type`, la factura sigue siendo ordinaria. `list` filtra por `type`. Ver [Facturas](https://os.utilia.ai/docs/developers/api/invoices#ordinaria-o-simplificada).
- **Precios con impuestos incluidos** (`pricesIncludeTax`, `prices_include_tax`): el total de cada línea es exactamente lo cobrado; 39,99 € con IGIC al 7 % son 37,37 de base y 2,62 de cuota.
- **Factura ya cobrada** con el bloque `payment` y **residencia del consumidor** con `consumerResidence` (`consumer_residence`).
- **Régimen del pequeño empresario del IGIC (REPEP)**: causa `EXEMPT_IGIC_SMALL_BUSINESS` y mención «Exención franquicia fiscal», que UTILIA OS pone sola dentro de un periodo del régimen.
- **Rectificativas desde tu aplicación**: `invoices.rectifications.create` y `list`, sobre `POST` y `GET /external/v1/invoices/:id/rectifications`. UTILIA OS decide el código legal (R5 o R1); el bloque `refund` registra la devolución hecha en tu pasarela. Scope nuevo `crm:invoices:rectify`, crítico financiero: el catálogo pasa a **57**. Ver [Facturas rectificativas](https://os.utilia.ai/docs/developers/api/invoice-rectifications).
- **Respuestas ampliadas**: `type`, `legalMentions`, `originalInvoiceId`, `paymentRegistered` y, por línea, `exemptionReason` y `exempt`; en el detalle, `rectifications` y `effectiveTotal`.
- **Errores**: `INVOICE_ERROR_CODES` e `isInvoiceErrorCode` (`InvoiceErrorCode` e `is_invoice_error_code` en Python) con los códigos nuevos, y los datos de un error de negocio (`missingFields`, `lineNumber`, `remainingRectifiableAmount`) en `error.details`.
- **Corregido**: un 403 cuyo mensaje contenía «origen» se reescribía como «Origen no permitido por CORS». El rechazo por origen se reconoce ahora solo por su `errorCode` (`ORIGIN_ERROR_CODES`, `isOriginErrorCode`).

### Qué cambia si vienes de la 4.2

| Antes | Después | Qué hay que cambiar |
| - | - | - |
| `CreateInvoiceInput.recipient` y sus campos de dirección obligatorios en el tipo | Opcionales en el tipo (`None` por defecto en Python) | Si leías esos campos de un objeto propio tipado así, contempla que falten |
| Ordinaria sin `recipient` en Python: error de validación del servidor | `ValueError` al construir el modelo, antes de salir a la red | Captura `ValueError` |
| `UtiliaSDKError.details` vacío en los errores de negocio | Trae el cuerpo de la respuesta | Nada; puedes leer los datos nuevos |
| Un 403 con «origen» en el mensaje se convertía en «Origen no permitido por CORS» | Llegan el mensaje y el `errorCode` del servidor | Si decidías por ese texto, decide por `errorCode` |
| `InvoiceRectificationsService` construido sin argumentos | Recibe el cliente en el constructor | Usa `sdk.invoices.rectifications`, que ya viene construido |

## Novedades de la versión 4.2.0

La 4.2.0 (2026-10-13) acompaña los cambios de la API externa sobre las claves de las aplicaciones: la clave ya no viaja en ninguna dirección, las claves se pueden renovar con un periodo de transición y el SDK avisa cuando usas la anterior.

- **Pase de conexión.** `sdk.tickets.createStreamToken({ route, userId })` o `({ route, jobId })` (`tickets.create_stream_token(route, user_id=..., job_id=...)` en Python) pide un pase de un solo uso y cinco minutos para abrir por tu cuenta una conexión en tiempo real, sin la clave en la dirección. En JavaScript, `streamUpdates`, `ai.streamSuggestions` y `ai.streamTranscription` ya lo piden solos, y `streamUpdates` reconecta con un pase nuevo cada vez (esperas de 1 a 30 segundos) y deja de intentarlo si el servidor rechaza el pase con un 4xx. En Python, las conexiones del SDK llevan la clave en la cabecera. Ver [Abrir la conexión por tu cuenta](https://os.utilia.ai/docs/developers/sdk/tickets#abrir-la-conexión-por-tu-cuenta).
- **Verificar avisos con varias claves.** `sdk.webhooks.verify` y `parse` leen también `X-Utilia-Webhook-Signatures` y aceptan una clave o varias, con fecha de caducidad (`validUntil`, `WebhookSecret(..., valid_until=...)` en Python), para poner la clave de firma nueva en tu servidor en cuanto la renuevas. Errores locales nuevos: `WEBHOOK_SECRET_EXPIRED` y `WEBHOOK_SECRET_INVALID`. Ver [Cuando renuevas la clave de firma](https://os.utilia.ai/docs/developers/webhooks/hmac-signature#cuando-renuevas-la-clave-de-firma).
- **Aviso de clave anterior.** La opción `onApiKeyExpires` (`on_api_key_expires`) recibe, una vez por fecha, el aviso de que la clave en uso es la anterior de una renovación y hasta cuándo vale; `sdk.apiKeyExpiresAt` (`api_key_expires_at`) guarda esa fecha. Sin la opción, el SDK deja un único aviso por fecha en la consola o en el registro `utilia_sdk`. Ver [Configurar el cliente](https://os.utilia.ai/docs/developers/sdk/configuration#aviso-de-que-la-clave-en-uso-es-la-anterior).
- **El SDK se identifica** con la cabecera `X-Utilia-SDK` (`utilia-sdk-js/4.2.0`, `utilia-sdk-python/4.2.0`).
- **Errores de las credenciales tipados**: `CREDENTIAL_ERROR_CODES`, `isCredentialErrorCode` y `KnownErrorCode` en JavaScript; `CredentialErrorCode` e `is_credential_error_code` en Python. Con OAuth, un 401 `OAUTH_BEARER_NOT_ALLOWED` ya no renueva el token ni repite la petición.
- **Confirmación en el móvil de las operaciones MCP**: `getApprovalChallenge`, `isApprovalRequired` y `APPROVAL_REQUIRED` (`get_approval_challenge`, `is_approval_required`). Ver [Confirmar una operación en el móvil](https://os.utilia.ai/docs/developers/sdk/error-handling#confirmar-una-operación-en-el-móvil).
- **`budgetSignatures` con clave de API**: `list`, `get` y `verify` van a `/external/v1/budgets/{budgetId}/signatures` y funcionan. El resto de métodos lanzan `METHOD_NOT_AVAILABLE_WITH_API_KEY` antes de hacer la petición. `SignerAuditEvent` gana `ipAddressMasked` y `deviceLabel`.
- **MCP, renovación de claves de las aplicaciones externas**: `appsRegenerateApiKey` y `appsWebhooksRegenerateSecret` admiten `graceDays` y `twoFactorApprovalId`, y llegan `appsWebhooksSecretCutover`, `appsWebhooksSecretRetirePrevious`, `appsWebhooksSecretAdvisoryDismiss`, `appsApiKeyRetirePrevious`, `appsApiKeyAdvisoryDismiss`, `appsSetResponsible`, `appsListResponsibleCandidates` y `appsWebhooksTestSigningKey` (en Python, en `snake_case`).

### Qué cambia si vienes de la 4.1

| Antes | Después | Qué hay que cambiar |
| - | - | - |
| `EventSource` propio con `buildSseUrl('/external/v1/tickets/...')` (JavaScript) | La dirección ya no lleva la clave | Usa el `url` de `tickets.createStreamToken` |
| `SignerAuditEvent.ipAddress` y `userAgent` | Ya no llegan | Usa `ipAddressMasked` y `deviceLabel` |
| Métodos de `budgetSignatures` sin acceso con clave de API: 401 (`UNAUTHORIZED`) | `METHOD_NOT_AVAILABLE_WITH_API_KEY` (`FORBIDDEN`), sin hacer la petición | Si decidías por el código, contempla el nuevo |
| `downloadCertificate` con OAuth en Python: `httpx.HTTPStatusError` | `UtiliaSDKError` | Captura `UtiliaSDKError` |
| `RegenerateApiKeyResponse.app` obligatorio | Opcional y en desuso: el servidor nunca lo ha enviado | No dependas de ese campo |
| `streamUpdates` sin errores del pase (JavaScript) | Puede pasar a `onError` un `UtiliaSDKError` y, tras un 4xx del pase, no reconecta | Trata ese error en `onError` |
| Responsable inactivo: 401 `API_KEY_INVALID` o 422 `EXTERNAL_APP_CREATOR_MISSING` | 403 `APP_RESPONSIBLE_INACTIVE` | Contempla el código nuevo |

El último cambio es del servidor, no del SDK: llega igual con cualquier versión.

## Novedades de la versión 4.1.1

La 4.1.1 (2026-10-11) dejó de poner credenciales en las direcciones que genera el SDK. `budgets.getPdfUrl`, `budgetSignatures.getCertificateUrl` y `budgets.streamGenerateItemsStatus` ya no incluyen la clave, y, con clave de API, lanzan antes de construir nada `METHOD_NOT_AVAILABLE_WITH_API_KEY`, porque sus rutas exigen la sesión de una persona y nunca han aceptado la clave. Con OAuth devuelven la dirección como hasta ahora, sin credenciales. En Python, la clave de las conexiones en tiempo real pasó a viajar solo en la cabecera. Sin cambios incompatibles para ninguna integración que funcionara. No se publicó por separado: va incluida en la 4.2.0.

## Novedades de la versión 4.1.0

La 4.1.0 (2026-09-12) añade **el calendario laboral de la empresa por MCP**, sin
cambios incompatibles. `sdk.mcp.attendance.holidays` envuelve la parte de
festivos de la herramienta `attendance` del servidor de recursos humanos
(`POST /api/mcp/rrhh`):

- `getScope()` (`get_scope()` en Python) devuelve el ámbito de la empresa
  (comunidad autónoma, isla y municipio) y el vocabulario cerrado para elegirlo.
- `setScope()` (`set_scope()`) lo fija. No borra ningún festivo: los del ámbito
  anterior dejan de contar y quedan visibles como «de otros ámbitos».
- `import(year)` (`import_year(year)`, porque `import` es palabra reservada en
  Python) carga el calendario oficial de ese año. Es idempotente.
- `list()` lista los festivos; `applicable` filtra los que cuentan para la
  empresa o los de otros ámbitos.

Los festivos municipales no se cargan: los publica cada ayuntamiento y la
respuesta lo avisa en `notice`. Consultar y listar exige `hr:attendance:view`;
elegir el ámbito y cargar el año, `hr:attendance:manage`.

Si ya estás en la 4.0.1, subir a la 4.1.0 no te obliga a tocar nada.

## Novedades de la versión 4.0.1

La 4.0.1 (2026-09-09) es una versión **solo de documentación interna** del paquete: no cambia ningún comportamiento ni ninguna firma. Además alinea el número de versión del paquete de Python, que arrastraba un `2.23.0` en su `package.json` que no correspondía con lo que de verdad se publica.

Si ya estás en la 4.0.0, subir a la 4.0.1 no te obliga a tocar nada.

## Novedades de la versión 4.0.0

La 4.0.0 (2026-09-07) añade `sdk.paymentMethods.getReadiness()`
(`payment_methods.get_readiness()` en Python, en las variantes asíncrona y
síncrona), que responde si la organización puede cobrar con tarjeta ahora mismo
y por qué no. Es el mismo criterio que usa la interfaz de UTILIA OS para
decidir si enseña el botón de cobrar. Tipos nuevos: `ChargingReadiness` y
`ChargingReadinessReason`. El detalle está en
[Comprobar si se puede cobrar](https://os.utilia.ai/docs/developers/sdk/payment-methods#comprobar-si-se-puede-cobrar).

Es una versión mayor porque retira los tres métodos del correo al proveedor: `getEmailRecipients`, `sendEmail` y `replyEmail`.

## Novedades de la versión 3.0.0

La 3.0.0 (2026-09-05) hace que la facturación y el cobro de una aplicación
externa funcionen de verdad. Es una versión **mayor**, y la razón principal no
es el volumen de cambios: es que cinco servicios que existían y devolvían 401
siempre pasan a funcionar contra otra ruta, y uno desaparece.

Lo que gana:

- **`sdk.subscriptions` funciona**, sobre `/external/v1/subscriptions`, con trece métodos: alta, listado, detalle, cambios, pausa, reanudación, baja, reactivación, generación adelantada, próximo cobro, facturas generadas, historial de ejecuciones y reintento del cobro.
- **`sdk.paymentAuthorizations` es nuevo** (`sdk.payment_authorizations` en Python), con el texto de consentimiento, el alta de la autorización firmada, el listado, el detalle y la retirada.
- **`sdk.invoices` gana cinco acciones**: `send`, `charge`, `markPaid`, `getPublicLink` y `regeneratePublicLink`.
- **`sdk.invoices.scheduledCharges` funciona**, con programar, listar, cancelar y adelantar el reintento.
- **`sdk.invoices.refunds.create`** emite el reembolso por la API externa.
- **`sdk.webhooks.verify` y `sdk.webhooks.parse`** verifican la firma de cualquier familia de avisos, con tipos por evento para facturación, suscripciones y cobros.
- **`sdk.users.identify` acepta `clientId`** para vincular el usuario a una ficha de cliente que la aplicación creó antes.
- **Los enumerados se derivan 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.
- **El catálogo de permisos de los dos SDK pasa a 56**, con la categoría nueva de críticos financieros.

### Tabla de rupturas

| Antes | Después | Qué hay que cambiar |
| - | - | - |
| `sdk.subscriptions.*` sobre `/finance/recurring-invoices` (401 siempre) | sobre `/external/v1/subscriptions` | `clientId` desaparece y `userId` pasa a obligatorio en todos los métodos |
| `sdk.subscriptions.create({ clientId, ... })` | `create({ userId, ... })` | Cambiar el identificador |
| `sdk.subscriptions.remove(id)` | retirado | La baja es `cancel(id, { userId })`, que conserva el historial |
| `sdk.subscriptions.stats()` | retirado | Era una métrica del tenant, no de la aplicación |
| `sdk.subscriptions.pause(id)` | `pause(id, { userId })` | Añadir el usuario |
| `sdk.mandates.*` | `sdk.paymentAuthorizations.*` | Renombre visible. El alias antiguo sigue funcionando y avisa |
| `sdk.mandates.listForClient(clientId)` | retirado | Una aplicación externa no conoce clientes del CRM |
| `sdk.mandates.revoke(id)` | `revoke(id, { userId })` | Añadir el usuario |
| `sdk.invoices.scheduledCharges.schedule(invoiceId, { stripePaymentMethodId, mandateConsent })` | `.schedule(invoiceId, { userId, scheduledAt, paymentAuthorizationId })` | Cuerpo distinto. El consentimiento se firma antes, en su propio endpoint |
| `sdk.invoices.scheduledCharges.update(...)` | retirado | Cambiar la fecha es cancelar y volver a programar |
| `sdk.invoices.refunds.cancelPreview` y `.cancel` | `sdk.invoices.refunds.create(...)` | Deshacer un reembolso es del equipo; lo que la aplicación puede hacer es emitirlo |
| `sdk.invoices.rectifications.updateLegalMetadata` | retirado con error claro | Reservado al equipo; usar la herramienta MCP `crm_invoices_rectifications` |
| `sdk.invoices.getPdfUrl(id, userId)` | retirado | Sustituto: `regeneratePublicLink`. Si lo usaste, regenera la clave de API |
| `sdk.chat.webhooks.verify(...)` | `sdk.webhooks.verify(rawBody, headers, secret)` | Sale de `chat`. El alias antiguo conserva su firma |
| `sdk.scheduled_charges` (Python, en la raíz) | `sdk.invoices.scheduled_charges` | Reubicación. El alias antiguo sigue funcionando y avisa |
| `sdk.scheduled_charges.retrieve(id)` (Python) | retirado | La ruta no existía |
| `InvoiceStatus` con 7 valores (Python) | 12 valores | El enumerado reventaba en la primera factura reembolsada |

> **Atención: Antes de actualizar, revisa tus permisos**
>
> La 3.0.0 estrena once permisos y endurece uno existente: `POST /external/v1/users/identify` pasa a exigir `crm:users:write`. Si tu aplicación ya usaba `identify`, concédele ese permiso **antes** de desplegar, o la llamada empezará a responder `403`.

## Novedades de la versión 2.27.0

La 2.27.0 (2026-09-04) completa la paridad del CRM en los dos SDK oficiales.

- **`sdk.mcp.crm` es nuevo**, con clientes, contactos y leads: altas y ediciones unitarias o por lotes, duplicados, fusiones y transiciones.
- **Las operaciones destructivas o con efecto externo mandan confirmación explícita** desde sus métodos de alto nivel.
- **Los errores MCP conservan `errorCode` y `details`**, de modo que una integración puede distinguir permisos, campos inválidos y reglas de negocio.

## Novedades de la versión 2.26.0

La 2.26.0 (2026-09-02) recoge el lote MCP del 2 de septiembre: dos herramientas transversales nuevas y varios cambios de contrato del servidor. Los detalles y las fechas de caducidad de las redes de compatibilidad están en la [sección de cambios de contrato](https://os.utilia.ai/docs/developers/mcp/reference).

- **`sdk.mcp.search` y `sdk.mcp.notifications`, nuevos.** El buscador global del ecosistema y los avisos de la persona que autorizó el token. Tienen [página propia](https://os.utilia.ai/docs/developers/mcp/search-and-notifications).
- **El cliente MCP deja de tragarse el mensaje de los errores de una herramienta.** Ahora lee las tres formas que conviven, añade la pista al mensaje y conserva el código original en `errorCode`.

## Novedades de la versión 2.23.0

La 2.23.0 (2026-08-05) cierra el lote de **medición y atribución de Marketing**: `sdk.mcp.marketing` pasa de 20 a 87 métodos por lenguaje y cubre enteras `marketing_campaigns` (35 operaciones), `marketing_lists` (17) y `marketing_deliverability` (10).

- **Comportamiento de la audiencia y segundos intentos**: `engagementBreakdown` (siete grupos, dos niveles, y el recuento es gratuito), `resendPreview`, `createResend` y `listResends`.
- **Interés de los suscriptores**: `listSubscribers` —que no existía en MCP—, `engagementSummary`, `setAsideInactive` y `reactivateSubscriber`. Apartar **no** es dar de baja y el consentimiento no se toca.
- **Quejas por correo no deseado**: `reports.complaints`, `deliverability.getComplaintPolicy` y `setComplaintPolicy`. La tasa es `null` cuando no hay forma de medirla, nunca `0 %`.
- **Medición del correo**: `reports.linkHeatmap` y `reports.audienceTech`. El país es **aproximado** y sale de una base local.

## Novedades de la versión 2.22.0

La 2.22.0 (2026-08-03) completa las **imágenes de presupuesto** en `sdk.budgets`:

- `uploadImageToSection(budgetId, sectionId, file, placement?)` / `upload_image_to_section(...)`: atajo para colocar una imagen dentro de un apartado. El apartado lo fija la dirección, así que el cuerpo no puede apuntar a otro.
- `scanLegacyImages(budgetId)` / `scan_legacy_images(...)`: detecta las imágenes heredadas incrustadas en el HTML de los apartados, que se ven en el editor y **no** salen en el PDF.
- `migrateLegacyImages(budgetId)` / `migrate_legacy_images(...)`: las convierte en colocaciones reales. Es idempotente y tolera fallos parciales: cada imagen vuelve como `MIGRATED` o `FAILED` con su motivo.
- Quedan **obsoletos** `textWrap` (el generador del PDF nunca lo aplicó: la imagen siempre ocupa su propia línea) y la opción `deleteFile` al borrar una imagen (el borrado del binario pasó a ser automático por recuento de referencias). Los dos se siguen aceptando y el servidor los ignora.

## Novedades de la versión 2.21.0

La 2.21.0 (2026-07-28) hace que `sdk.oauth.getAuthorizedApps()` / `get_authorized_apps()` devuelva los permisos **ya traducidos y recortados por el plan** de la organización: `permissions`, `permissionCount`, `unavailablePermissionCount` y `unavailableModules`, todos opcionales para no romper contra un backend anterior.

**`AuthorizedApp.scope` cambió de significado en la 2.21.0.** El campo queda **obsoleto** (retirada prevista el 2027-02-01) y, sobre todo, ya no es el universo de scopes concedidos a la aplicación, sino ese universo **recortado por el plan**. Un scope que sigue vivo en el token puede no aparecer en el campo si su aplicativo no está contratado, así que quien lo use para decidir qué puede hacer la aplicación verá menos de lo que hay. Sustitúyelo por `permissions` (lo ejercible) y `unavailablePermissionCount` (lo que quedó fuera).

## Novedades de la versión 2.18.0

La 2.18.0 (2026-07-05) incorpora los **grupos de empresas del CRM (matriz y filiales)** a la API externa de clientes:

- El tipo `ExternalClient` incluye `parentClientId` (escalar, presente en todas las respuestas) y `parent { id, name, slug }` (solo en el detalle).
- Nuevo método `sdk.externalClients.getGroup(id)` / `sdk.external_clients.get_group(client_id)`: rol del cliente en su grupo (`PARENT` / `SUBSIDIARY` / `STANDALONE`), referencia a la matriz y lista de filiales. Requiere el scope `crm:clients:read`.
- `update` acepta `parentClientId` para vincular una filial a su matriz o desvincularla (`null` en JavaScript; `None` explícito en Python). Requiere el sub-scope nuevo `crm:clients:write:hierarchy` además de `crm:clients:write`.
- Webhooks nuevos `CLIENT_LINKED_TO_GROUP` y `CLIENT_UNLINKED_FROM_GROUP` (payload de solo identificadores).
- El espejo del catálogo de scopes pasa a **36 entradas** con `crm:clients:write:hierarchy`.

La 2.17.0 (2026-07-02) añadió `sdk.mcp.opportunities.listStages()` / `list_stages()` para las etapas de oportunidad configurables por organización.

Cambios aditivos, sin breaking changes. Detalle completo en [Endpoints de clientes del CRM](https://os.utilia.ai/docs/developers/api/clients).

## Novedades de la versión 2.16.0, y por qué quedó rota

La 2.16.0 (5 de junio de 2026) añadió `sdk.subscriptions` y `sdk.mandates`
envolviendo rutas **internas** de `/finance/*`, no la API externa.

Esas rutas exigen la sesión de un usuario del panel, que el SDK no sabe obtener:
con clave de API devolvían 401 y con OAuth **también**, porque el token OAuth es
opaco y solo se resuelve en el carril MCP. Hasta el 4 de agosto de 2026 esta
página y los comentarios del SDK decían que bastaba con OAuth, y era falso.

Se arregló en la **3.0.0**, que reescribe esos servicios sobre `/external/v1/*`
con la clave de API. Si tu código quedó escrito contra la 2.16.0, revisa la
tabla de rupturas de arriba: las firmas cambian.

## Hitos por versión

Resumen de las versiones más relevantes de ambos SDK, de la rama 1.x a la actual. La numeración es compartida entre JavaScript y Python.

| Versión | Fecha | Aportación principal |
| - | - | - |
| 4.6.0 | Sin publicar | **Comprobar tu integración**: `sdk.diagnostics`, la orden `doctor`, `getMarkdown()`, `sdk.aiAnalytics`, `onIgnoredFields`, errores con `fixedBy`, `docsUrl` y `retryable` y su catálogo público, reintentos que respetan `Retry-After` y no repiten altas sin clave de idempotencia, la validación local en castellano y la **captura de errores en dos líneas** (`errors.install`, `capture`), con su gestión (`resolve`, `reopen`, `silence`). Catálogo de permisos: **59**. Ver la tabla de cambios de la 4.5. |
| 4.5.0 | 2026-10-21 | **Claves de API de la organización por MCP** (`orgKeys*`), la validación en seco de facturas y rectificativas, `signTestPayload` y la retirada de los métodos que solo servían a la organización dueña de la plataforma. Catálogo de permisos: **57**. Ver la tabla de cambios de la 4.4. |
| 4.4.0 | 2026-10-15 | **Series de facturación de la aplicación**: `listSeries`, `seriesCode` y fecha de expedición acotada; contrato de la ventanilla única del IVA (Próximamente). Ver la tabla de cambios de la 4.3. |
| 4.3.0 | 2026-10-13 | **Facturas simplificadas y rectificativas desde tu aplicación**: `type: 'SIMPLIFIED'`, `pricesIncludeTax`, `payment`, `consumerResidence`, régimen del pequeño empresario del IGIC y `invoices.rectifications.create` y `list`. Catálogo de permisos: **57**. Ver la tabla de cambios de la 4.2. |
| 4.2.0 | 2026-10-13 | **Claves de las aplicaciones sin exponer**: pase de conexión (`createStreamToken`), verificación de avisos con varias claves, aviso de clave anterior (`onApiKeyExpires`), errores de credenciales tipados, confirmación en el móvil de las operaciones MCP y `budgetSignatures` con clave de API. Ver la tabla de cambios de la 4.1. |
| 4.1.1 | 2026-10-11 | **Ninguna dirección lleva la clave**: `getPdfUrl`, `getCertificateUrl` y `streamGenerateItemsStatus` avisan de que no funcionan con clave de API. Incluida en la 4.2.0. |
| 4.1.0 | 2026-09-12 | **Calendario laboral por MCP**: `sdk.mcp.attendance.holidays` consulta y fija el ámbito de festivos de la empresa, carga el calendario oficial de un año y lista los festivos que cuentan. Sin cambios incompatibles. |
| 4.0.1 | 2026-09-09 | **Solo documentación interna** del paquete y número de versión del paquete de Python alineado. Sin cambios de comportamiento ni de firma. |
| 4.0.0 | 2026-09-07 | **Disposición de cobro con tarjeta**: `sdk.paymentMethods.getReadiness()` (`payment_methods.get_readiness()` en Python) responde si la organización puede cobrar ahora mismo y por qué no. Versión mayor por una retirada en un servicio no disponible para las organizaciones cliente. |
| 3.1.0 | 2026-09-05 | **Honestidad de la medición y fiabilidad del envío en Marketing por correo**: 39 métodos nuevos y dos sub-espacios nuevos, sin cambios incompatibles. Aviso: `marketing:write` deja de enviar por sí solo. |
| 3.0.0 | 2026-09-05 | **Facturación y cobro de aplicaciones externas**: `sdk.subscriptions` y `sdk.invoices.scheduledCharges` funcionan de verdad sobre `/external/v1/*`, `sdk.paymentAuthorizations` es nuevo, `sdk.invoices` gana `send`, `charge`, `markPaid` y el enlace público, y `sdk.webhooks` verifica cualquier familia de avisos. Versión MAYOR: ver la tabla de rupturas. Catálogo de permisos: **56**. |
| 2.27.0 | 2026-09-04 | **CRM completo por MCP**: servicio `sdk.mcp.crm`, con lotes y fusión de duplicados. |
| 2.26.0 | 2026-09-02 | **Buscador global y avisos por MCP** (`sdk.mcp.search`, `sdk.mcp.notifications`). |
| 2.23.0 | 2026-08-05 | **Medición y atribución de Marketing**: `marketing_campaigns`, `marketing_lists` y `marketing_deliverability` completas; comportamiento de la audiencia, segundos intentos, interés de suscriptores, quejas por correo no deseado, mapa de calor por enlace y desglose por aparato, programa y país. |
| 2.22.0 | 2026-08-03 | **Imágenes de presupuesto completas**: `uploadImageToSection`, `scanLegacyImages` y `migrateLegacyImages`. Quedan obsoletos `textWrap` y la opción `deleteFile`. |
| 2.21.0 | 2026-07-28 | **Permisos recortados por el plan** en `getAuthorizedApps()`: `permissions`, `permissionCount`, `unavailablePermissionCount` y `unavailableModules`. `AuthorizedApp.scope` queda obsoleto y cambia de significado. |
| 2.20.0 | 2026-07-12 | **Plataforma de chat extensible** (`sdk.chat`): publicar (`sendMessage`, v1) y leer canales, miembros e historial visible (`listChannels`/`getChannel`/`listMembers`/`listMessages`/`getMessage`, v2), más `sdk.chat.webhooks.verify()` para validar localmente la firma HMAC de los webhooks de chat. Catálogo de scopes: **40** (Chat pasa de 1 a 4 con `chat:channels:view`, `chat:messages:view` y `chat:events:subscribe`). |
| 2.18.0 | 2026-07-05 | **Grupos de empresas**: `parentClientId`/`parent` en `ExternalClient`, `getGroup`, vínculo vía `update` con el sub-scope `crm:clients:write:hierarchy` y webhooks `CLIENT_LINKED_TO_GROUP` / `CLIENT_UNLINKED_FROM_GROUP`. Catálogo de scopes: **36**. |
| 2.17.0 | 2026-07-02 | Etapas de oportunidad configurables: `sdk.mcp.opportunities.listStages()`. |
| 2.16.0 | 2026-06-05 | Paridad de **suscripciones** (`sdk.subscriptions`) y **mandatos** (`sdk.mandates`). |
| 2.15.1 | 2026-05-25 | Ajuste del catálogo de scopes externos: se retira `fm:files:delete` y se añaden 7 scopes (presupuestos, analítica de IA, telemetría de errores). Catálogo resultante en esa versión: **35 scopes**. |
| 2.15.0 | 2026-05-25 | Catálogo público de scopes de la API externa como tipos TypeScript / modelos, espejo 1:1 del backend. |
| 2.13.0 | 2026-05-24 | Servicios `sdk.externalLeads` y `sdk.externalClients` (`/external/v1/leads` y `/external/v1/clients`). |
| 2.12.1 | 2026-05-21 | Servicio `sdk.externalContacts` (`/external/v1/contacts`), pensado para aplicaciones de envío de correo. |
| 2.10.0 | 2026-05-14 | Servicio MCP tipado `sdk.mcp.opportunities` (refleja la tool `crm_opportunities_stats`). |
| 2.9.0 | 2026-05-12 | Tool MCP de tickets y ampliaciones del árbol `sdk.mcp.*`. |
| 2.8.0 | 2026-05-12 | Módulo `sdk.mcp.*`: integraciones, credenciales de API, ajustes de integraciones de la organización y vista de resumen. |
| 1.8.0 | 2026-04-08 | Parámetro `theme` (`'light'` / `'dark'`) en la pantalla de consentimiento OAuth (en Python, 0.7.0). |

Los dos SDK admiten **OAuth 2.1 con PKCE** como alternativa a la clave de la aplicación. El parámetro `theme` para elegir tema claro u oscuro en la pantalla de consentimiento llegó en las versiones 1.8.0 (JavaScript) y 0.7.0 (Python). Ver [OAuth con el SDK](https://os.utilia.ai/docs/developers/sdk/oauth).

## Catálogo de scopes de la API externa

El catálogo de scopes que una `ExternalApp` puede tener concedidos se expone en el endpoint:

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

La fuente de verdad es la lista `EXTERNAL_API_SCOPES_LIST` del backend, que tiene **61 permisos**. Las cifras que aparecen en la tabla de hitos (35, 36, 40, 45, 56, 57) son el tamaño del catálogo **en aquella versión**, no el de hoy. El conteo canónico y su desglose viven en [Catálogo de permisos de la API](https://os.utilia.ai/docs/developers/api/scopes-catalog):

| Dominio | Permisos |
| - | -: |
| Contactos | 6 |
| Leads | 5 |
| Clientes | 5 |
| Usuarios externos | 3 |
| Facturación | 6 |
| Cobros con tarjeta | 4 |
| Suscripciones | 3 |
| Autorizaciones de cobro | 3 |
| Cobros programados | 2 |
| Soporte | 3 |
| Ficheros | 2 |
| Presupuestos | 4 |
| Analítica y telemetría | 4 |
| Chat | 9 |
| Instalaciones del catálogo | 2 |
| **Total** | **61** |

El cuarto scope del dominio Clientes es `crm:clients:write:hierarchy` (2.18.0): gestiona el vínculo matriz-filial de los grupos de empresas y se concede por separado del write genérico. El dominio Chat aporta nueve scopes con la plataforma de chat extensible: `chat:messages:send` (v1, publicar); `chat:channels:view`, `chat:messages:view` y `chat:events:subscribe` (v2: leer canales y mensajes, y recibir webhooks); `chat:channels:create`, `chat:channels:manage` y `chat:members:manage` (v3: crear, editar y archivar canales y gestionar sus miembros); y `chat:agents:view` y `chat:agents:manage:external` (v4: gestión de agentes de IA por apps). De todos ellos, solo `chat:agents:manage:external` exige `SUPER_ADMIN` y queda fuera de todo paquete.

Los scopes de gobierno del canal (v3) tienen ya sus métodos en `sdk.chat`: `createChannel`, `updateChannel`, `archiveChannel`, `addMember` y `removeMember` (en Python, en `snake_case`). La gestión de agentes de IA de la v4 (`sdk.chat.agents`) se documenta en [Gestionar agentes de IA](https://os.utilia.ai/docs/developers/chat-platform/managed-agents).

El cuarto scope de Analítica y telemetría es `analytics:errors:manage` (4.6.0): resolver, reabrir y silenciar los errores que ha enviado la propia aplicación. Los dos de Instalaciones del catálogo, `apps:installations:read` y `apps:installations:exchange`, llegan con la 4.7.0 y son de la aplicación que publica una ficha en el catálogo.

De esos 61 permisos, **17 solo puede concederlos un Super Administrador**, con un motivo razonado de al menos veinte caracteres. Nueve lo son por **datos personales críticos** (contactos en derecho de oposición, notas internas de contactos, leads y clientes, conversión de leads, datos fiscales de clientes y de usuarios, envío de datos de tickets a proveedores de IA y verificación de firmas), uno por permitir que una aplicación **gestione agentes de IA** (`chat:agents:manage:external`) y siete por ser **críticos financieros**: `billing:charge`, `billing:refund`, `billing:subscriptions:cancel`, `billing:payment-authorizations:write`, `billing:scheduled-charges:write` (desde el 21 de octubre de 2026), `crm:invoices:cancel` y `crm:invoices:rectify` (4.3.0). `crm:users:read:fiscal-data` llegó con la 4.6.0.

La categoría de críticos financieros es nueva en la 3.0.0. Existe porque quien revisa la solicitud de una aplicación necesita saber **por qué** un permiso está blindado: no es lo mismo «leerá notas sobre personas» que «podrá cobrar y devolver dinero de tus clientes».

**Cambio de catálogo en la 2.15.1.** El scope `fm:files:delete` se retiró del catálogo porque ningún endpoint de la API externa lo aplicaba (no existe `DELETE /external/v1/files/:id`), de modo que concederlo generaba una falsa sensación de seguridad. Las aplicaciones que ya lo tuvieran concedido seguirán mostrándolo hasta que el operador lo retire desde el panel, pero nunca habilitó ninguna operación real. Volverá al catálogo cuando se exponga el endpoint correspondiente.

## Política de compatibilidad

1. **La API externa es estable en v1**

   Todos los endpoints de la API externa viven bajo `/external/v1`. Mientras tu integración consuma esa versión, los contratos publicados se mantienen. Las novedades se añaden de forma aditiva.
2. **Los SDK siguen versionado semántico**

   La rama actual de ambos SDK es la **4.x**. Los cambios menores (segundo número) añaden funcionalidad de forma compatible; los de parche (tercer número) corrigen o ajustan sin romper. Fija una versión mínima conocida y revisa el changelog antes de subir de versión menor.
3. **Las retiradas se anuncian con antelación**

   Los métodos que se van a eliminar se marcan primero como `@deprecated` y se mantienen durante una ventana de coexistencia antes de desaparecer en una versión mayor. Por ejemplo, `generateSigningLink` quedó obsoleto en favor de `forceSendSignature`: hoy lanza siempre un error que remite al método nuevo y se conserva solo para que el código antiguo compile.
4. **Mantén SDK y backend alineados**

   Algunas funcionalidades nuevas del SDK dependen de endpoints que deben existir en tu instancia de UTILIA OS. Si llamas a un método nuevo contra un backend más antiguo, recibirás un error de endpoint no encontrado. Actualiza ambos lados de forma coordinada.

## Siguiente paso

- [Instalar el SDK](https://os.utilia.ai/docs/developers/sdk/install): Requisitos e instalación en JavaScript y Python.
- [Cambios de la API](https://os.utilia.ai/docs/developers/changelog/integrations): Registro completo de cambios del ecosistema de integración.
