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.
Actualizado el . 73 min de lectura
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
5 oct
Enlaces a la documentación
create_share_token exige el ámbito dev:docs:share.9 oct
Introspección OAuth
Solo para clientes confidenciales, y solo sobre sus propios tokens.12 oct
Aviso BUDGET_VIEWED
view.ipAddress va siempre a null; la IP enmascarada llega en view.ipAddressMasked.13 oct
SDK 4.2.0 y responsable inactivo
Responsable inactivo: 403 APP_RESPONSIBLE_INACTIVE; SignerAuditEvent pierde ipAddress y userAgent.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.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.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.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.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.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.
Antes de octubre:
- 27 de mayo: se firma
timestamp.rawBody; los receptores que firmaban solo el cuerpo rechazan las entregas. - 2 de septiembre:
dev:writeycrm:writedejan de borrar archivos por MCP; hace falta un ámbito propio. - 3 de septiembre:
portal_accessexigecrm:portal:viewycrm:portal:manage. - 5 de septiembre: SDK 3.0.0;
identifyexigecrm:users:writey desaparece el periodo de gracia de los permisos. - 7 de septiembre: 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-installationsyGET /external/v1/app-installations/:idtraensubscriptionen 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.nullen las gratuitas. Filtro nuevosubscriptionStatus. Ver 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,_REFUNDEDy_ENDED. El catálogo de avisos pasa a 133. Ver 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_ENDEDyAPP_SUBSCRIPTION_PAUSEDllegan aunque la empresa haya perdido Integraciones.
MCP: la suscripción en las instaladas
installations_listeinstallations_gettraensubscriptionen las aplicaciones de pago; el importe, solo con el permiso de ver el plan de la empresa (SETTINGS_ORGANIZATION_VIEW).installations_cancel_subscription, conconfirm: 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_INSTALLoSETTINGS_ORGANIZATION_EDIT), con el ámbitointegrations:apps:manage. Comprar, cambiar de plan, reactivar o pagar no se hace por MCP.- Salen
listings_listylistings_get: las fichas que publica una cuenta de desarrollador se gestionan en el servidor de desarrolladores. La herramientaintegrationspasa 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.
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 camposurlyhrefde 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.
- 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-installationsyGET /external/v1/app-installations/:id. Ver Referencia de las instalaciones. - Dos permisos de la API nuevos, de la aplicación que publica la ficha:
apps:installations:readyapps:installations:exchange. Conanalytics: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_EXPIREDyAPP_INSTALLATION_NOT_FOUND, cada uno con su apartado en Errores y reintentos. - Avisos nuevos, que llegan solos a las aplicaciones del catálogo:
APP_INSTALLED,APP_UNINSTALLED,APP_ACCESS_CHANGED,APP_SUSPENDEDyAPP_REINSTATED. Los avisos de una instalación llevaninstallationen el sobre y la cabeceraX-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.
Widget: verificación de identidad
- El servidor del integrador firma el
userIdde cada persona con el secreto del widget (HMAC-SHA256 en hexadecimal) y el widget manda la firma en la cabeceraX-Utilia-User-Signature. Tres modos por aplicación: Desactivada, Solo avisar y Obligatoria. Ver Verificar la identidad en el widget. - Cambio incompatible para las aplicaciones nuevas: nacen en Obligatoria. Sin la firma, las peticiones del widget con un
userIdresponden403 WIDGET_IDENTITY_REQUIRED,WIDGET_IDENTITY_INVALIDoWIDGET_IDENTITY_SECRET_MISSING. Las aplicaciones anteriores quedan en Desactivada. - Hallazgos nuevos del diagnóstico:
widget-identity-not-verifiedywidget-identity-secret-missing. - Widget 1.2.0 (sin publicar): la opción
userSignaturey los atributosdata-user-signatureyuser-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.
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.
API externa: el registro de errores dice qué pasa con cada error
- Respuesta veraz en
POST /external/v1/errors/report:accepted,status(created,updatedoregressed) oreason(rate_limited,quota_exhausted,overloadedodiscarded_expected), ycount. Si UTILIA OS no puede guardar el error,503 ERROR_STORAGE_UNAVAILABLEconRetry-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
hashvacío. Ahora respondeaccepted: falseconreason: discarded_expected. Para registrar un 4xx que sí importa, ponlo encontext.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 traereason: storage_failedy se repite solo ese.- Campos nuevos al enviar:
release,environment,fingerprint(con"{{default}}"para combinar la agrupación propia con la automática) ytags.POST /external/v1/tickets/client-errorsadmitereleaseyenvironment, y respondereceived,acceptedydiscardedcuando ha procesado el lote. - Consultar:
GET /external/v1/errors/:hashcon la pila, el contexto saneado y lo vinculado. El listado ganastatus,silencedUntil,environment,firstRelease,lastRelease,regressionCountylastRegressedAt, y los filtrosstatusyfirstRelease;severityadmite un valor, varios separados por comas o el parámetro repetido. Las estadísticas ganansilenced. - Gestionar, con el permiso nuevo
analytics:errors:manage:POST /:hash/resolve,/reopen,/silence(de 1 a 672 horas) yDELETE /: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. - Regresiones: un error resuelto que vuelve a ocurrir se reabre solo.
ERROR_REOPENEDganareason(manualoregression) yrelease;ERROR_RESOLVED,reasonyrelease;ERROR_REPORTED,release,environmentycount. 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/batchy/tickets/client-errors) ya no gastan el cupo por hora de la clave (por eso no llevan las cabecerasX-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 y Enviar los errores de tu aplicación.
API externa: filtros de varios valores en leads, contactos y clientes
- Corregido: los filtros de varios valores de los listados (
status,stage,priority,leadTypeytemperatureen leads;statusen contactos;status,typeysizeen clientes) respondían400con 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[]=NEWde axios, que antes se ignoraba sin avisar. Un valor que no existe sigue respondiendo400.
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.
MCP: lo eliminado en el gestor de archivos se puede restaurar
- Cambio de comportamiento:
deleteybatch_deletedefm_files_manage,deletedefm_folders_managey quitar un archivo de su última carpeta (remove_from_folder) ya no destruyen nada: lo mandan 30 días a «Eliminados recientemente» y respondenoutcome: "TRASHED"conpurgeAfter.physicallyDeletedllega siempre afalse. - Operaciones nuevas:
fm_files: list_trash,fm_files_manage: restoreyfm_files_manage: purge(conconfirm: 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. 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) yflush: capturan lo que nadie trata y lo que atrapas, nunca lanzan y envían en lotes que respetanRetry-After(retryAfterMs,retry_after).errors.reportBatch(report_batch),get,resolve,reopen,silenceyunsilence.- Corregido en JavaScript:
errors.listcon varias gravedades enviabaseverity[]=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.
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).metatrae ademáshasMoreByType,moreTypes,intentTypesyappliedFilters, y cada resultado puede llevarstateLabel(«Borrador», «Vencida»).meta.totalMatchescuenta las filas leídas, no las que existen: para saber si hay más, usahasMoreByType. Detalle en Buscador global y avisos por MCP. - 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-sensitiveofinance: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/paymentsyfinance_payments:list,stats,exporty descarga por filtros):supplierNify la búsqueda por NIF solo cuentan conSUPPLIERS_VIEW_SENSITIVEy, por MCP, confinance:suppliers:view-sensitive. Sin ellos, el filtrosupplierNifse ignora, igual queuserIdsinPAYMENTS_VIEW_ALL. - Clientes (
GET /api/crm/clientsycrm_clients: list): la búsqueda por NIF exigeCLIENTS_VIEW_SENSITIVEy, por MCP,crm:clients:view-sensitive. - Proveedores (
suppliers: list): por MCP, la búsqueda por NIF exige ademásfinance:suppliers:view-sensitive; por REST ya exigíaSUPPLIERS_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 yerrorCode, 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-sampleywebhook-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 y en Integrar con un asistente de IA.
API externa: errores que dicen qué hacer
fixedByydocsUrlen el cuerpo de todo error de/api/external/v1: quién lo arregla (developer,organizationoutilia) y el apartado de la documentación que lo explica. Son aditivos.- El 403
INSUFFICIENT_SCOPEnombra el permiso que falta, en el mensaje y enrequiredScopes. Antes no lo decía. Retry-Afterreal. 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ásX-Utilia-Quota-Limit,X-Utilia-Quota-RemainingyX-Utilia-Quota-Reset(en segundos UNIX) con ese cupo; lasX-RateLimit-*siguen siendo las del freno por minuto.X-Utilia-Ignored-Fieldsen 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ónutk_contra la API externa, antesAPI_KEY_INVALIDoAPI_KEY_REQUIRED),API_KEY_WRONG_HEADER(la clave de la aplicación enAuthorizationen lugar deX-Api-Key, antesAPI_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_FOUNDyBUDGET_COMMENT_VISIBILITY_NOT_ALLOWED(antes 404 y 403 sin código), eINVOICE_NUMBERING_UNAVAILABLE,INVOICE_ISSUANCE_REVIEW_REQUIREDyRECTIFICATION_OPERATION_INCOMPLETE(antes 500 sin código, ahora conretryable).
Cada código está en Errores, identificador de petición y reintentos.
Avisos: tarjetas a punto de caducar y salud de tu aplicación
PAYMENT_METHOD_EXPIRINGse 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.- 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 traepreviousStatus: paused. Ver 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,
/mey el diagnóstico llegan recortados, concredential.publishable: true, y las llamadas rechazadas, la prueba activa y los avisos de prueba responden403 DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY: se lanzan desde la pestaña Pruebas del panel. Ver 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_CALLSdeja 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
/mey el diagnóstico. Una llamada que tu programa corta antes de la respuesta queda con el estado499y no cuenta como correcta. check-requestacota la consulta (100 parámetros y 16 KB), el anidamiento del cuerpo (32 niveles) y la petición entera (300 KB), con400 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-outresponde404 CONTACT_NOT_FOUND, como uno que no existe, en lugar de403 INSUFFICIENT_SCOPE. Ver 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_overviewgana la operaciónapp_diagnosis, el diagnóstico de una aplicación, conEXTERNAL_APPS_VIEWy el scopeintegrations:overview:view. Verintegrations_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.
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/usersya no traetaxIdnibillingAddress, eidentifysolo devuelve los que mandas en esa misma llamada. Además,identifysolo devuelve lo guardado del usuario concrm:users:ready 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. - CAMBIO QUE ROMPE: reintentar el cobro de una cuota.
POST /external/v1/subscriptions/:id/invoices/:invoiceId/retry-chargeexigebilling:scheduled-charges:writeen lugar debilling:subscriptions:write, admite 3 llamadas por minuto y aplicación y pide una persona responsable activa.generate-nowsobre una suscripción que se cobra sola pide ademásbilling:scheduled-charges:writeobilling:charge. Ybilling:scheduled-charges:writepasa 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. - CAMBIO QUE ROMPE: rectificativas de prueba.
POST /external/v1/invoices/:id/rectifications/validateexigecrm:invoices:rectify: sin él,403 INSUFFICIENT_SCOPEen lugar de un200con 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 avisoPUBLISHABLE_KEY_SENSITIVE_SCOPES. El catálogo de permisos ganapublishableAllowedyconditionalRequirements. Ver 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.
- 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.
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.
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.
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.
- 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/:idyexpiresAt, o se crea otra; una clave caducada no se renueva. Detalle en 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/:idya no desactiva: elimina. La aplicación deja de funcionar y sale de la lista, pero no se borra nada y se restaura conPOST /api/integrations/apps/:id/restore, que la devuelve desactivada. La clave de una aplicación eliminada responde401 API_KEY_INVALID. Con suscripciones o cobros programados en marcha, eliminarla responde409 EXTERNAL_APP_HAS_ACTIVE_BILLING.- Las eliminadas se listan con
GET /api/integrations/apps?deleted=onlyy conservan su identificador: reutilizarlo responde409 EXTERNAL_APP_SLUG_HELD_BY_DELETED. - Rutas nuevas
GET /api/integrations/apps/:id/permanent-deletion-previewyPOST /api/integrations/apps/:id/permanent-delete, que borra la aplicación para siempre conconfirmNamey conserva lo que obliga la ley: facturas, suscripciones, autorizaciones de cobro, tickets, fichas del CRM y archivos. - Cambio incompatible (MCP):
apps_deleteelimina en lugar de desactivar y solo lo permiteEXTERNAL_APPS_DELETE; ya no bastaINTEGRATIONS_APPS_MANAGE. Lleganapps_restoreyapps_delete_permanently, yintegrationspasa de 39 a 41 operaciones.
Detalle en 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.
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.
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,failedowithheld),reason(SEND_SCOPE_MISSING,PAYMENT_NOT_REGISTERED,SEND_QUOTA_EXCEEDED,NO_RECIPIENT,PDF_NOT_READY,STORAGE_UNAVAILABLE,MAIL_SERVER_UNAVAILABLE,SEND_FAILED,RETRIES_EXHAUSTEDoINTERRUPTED),messageen castellano listo para mostrar,attempts,lastAttemptAtysentAt. Solo viaja si se pidió el correo (sendEmail: trueo, sinsendEmail, el interruptor de la aplicación encendido), y se actualiza solo. Ver 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.emailSentsigue siendo el resumen en un booleano y ahora viaja también en las repeticiones con la misma clave.paymentRegisteredpasa atruecuando el cobro que falló al emitir se registra después, conmark-paido desde la ficha de la factura.- Avisos: todos los bloques de factura de los avisos
INVOICE_*(invoice,originalInvoiceyrectifyingInvoice) llevandisplayNumber.
API externa: el correo al emitir se reintenta solo
sendEmailmanda sobre el interruptor «Enviar las facturas por correo al emitirlas» en los dos sentidos:truela envía aunque esté apagado yfalsela retiene aunque esté encendido. SinsendEmail, 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.statusqueda enretryingy 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,failedconRETRIES_EXHAUSTEDy un aviso en UTILIA OS para la persona que figura como creadora de la factura. No la reenvíes mientras esté enretrying. - 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,emailDeliverypasa asenty 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.
API externa: rectificativas
RECTIFICATION_NOT_ALLOWEDdice cuándo basta con esperar. Sus motivos son ahora: borrador, anulada, ya rectificada por completo, es una rectificativa, emisión todavía en curso (409conretryable: trueyretryAfterSeconds: 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.- Ruta nueva
POST /external/v1/invoices/:id/rectifications/validate: con el mismo cuerpo, responde200convalid,livemode: false,blockers,warningsypreview(cause,rectificationCode,totalAmount,remainingAfter,issueDate,refundywillSendEmail) sin numerar ni registrar nada. Avisa conIDEMPOTENT_REPLAYsi 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_USEDy, si aquella acción ya rectificó la factura entera,409 RECTIFICATION_NOT_ALLOWED. Si una petición se cortó, repítela con la mismaidempotencyKey.
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_AVAILABLEconretryable: 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_AVAILABLEconretryable: false: el PDF original necesita que lo revise el equipo.502 INVOICE_EMAIL_NOT_SENT, conretryable: trueyretryAfterSeconds: 120si el servidor de correo no responde, o conretryable: falsesi 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.
Probar la integración sin facturas reales
POST /external/v1/invoices/validatey/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 permisocrm:invoices:*y no exigen la facturación activada: la informan como bloqueoBILLING_NOT_ENABLED.- Avisos de ejemplo:
POST /api/integrations/apps/:id/webhook/testadmiteeventen el cuerpo y envía una muestra de ese evento, firmada como los avisos reales, conlivemode: falseymeta.test: true. La misma opción tieneapps_webhooks_testdel 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.
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.
- Ruta nueva
POST /api/integrations/apps/:id/invoices/:invoiceId/issuance-review(permisosEXTERNAL_APPS_EDITeINVOICES_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.
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.
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.
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.
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(permisocrm: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.seriesCodeenPOST /external/v1/invoices: el código de una serie de tu aplicación oDEFAULTpara 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_MISMATCHy409 INVOICE_SERIES_INACTIVE.- Suscripciones:
type(ordinarias o simplificadas) yseriesCodeenPOST /external/v1/subscriptionsyPATCH /external/v1/subscriptions/:id; la respuesta los trae. - Respuestas y avisos: cada factura trae
seriesCode(DEFAULTen la serie de siempre y en las rectificativas, que van siempre aR-), también en el objetoinvoicede los avisosINVOICE_*.GET /external/v1/invoicesfiltra porseriesCode. - Panel de aplicaciones:
billing-configlee y fija las series por defecto de la aplicación (defaultStandardSeriesId,defaultSimplifiedSeriesIdyavailableSeries). Ver Series por defecto de la aplicación. - Sin permisos nuevos. La descripción de
crm:invoices:readdice 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/invoicesy las suscripciones admitentaxSystem: "EU_OSS", líneasEU_VATconvatRateClassyossSupplyType, yconsumerResidence.evidenceItems, la prueba estructurada de la ubicación del consumidor.GET /external/v1/invoices/eu-vat-rates(permisocrm:invoices:read) devuelve los tipos de IVA vigentes de los Estados miembros.- Las respuestas y los avisos
INVOICE_*traentaxSystem,consumerCountryCodeyossScheme; las líneas,vatRateClassyossSupplyType. - Mientras la función no esté disponible para tu organización, cualquier dato suyo responde
403 FEATURE_COMING_SOONconfeatureKey: "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()) yseriesCode(series_code) en el alta, en el listado y en las suscripciones.typeen las suscripciones yseriesCodeen las respuestas.- Los códigos de error de las series y de la fecha de expedición.
- Las series por defecto en
appsBillingGetConfigyappsBillingUpdateConfigdel MCP. - El contrato de la ventanilla única del IVA:
EU_OSS,getEuVatRates(), los códigosOSS_*yFEATURE_COMING_SOON.
El detalle está en Versiones y compatibilidad.
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.
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-suggestanaliza como máximo las 5 primeras imágenes deimages, en el orden del array. Con más, la petición se sigue aceptando (201), pero la respuesta trae ademásimagesTruncated: true,imagesUsedeimagesReceived. 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.
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-projectexige los permisosBUDGETS_EDITyPROJECTS_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')ycrm_budgets_workflow(operation: 'convert_to_project')exigen al tokencrm:budgets:updateycrm:projects:create, y al usuario los dos permisos.crm:writeycrm:fullya incluyen los dos scopes;crm:readno 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')ycrm_budgets_workflow(operation: 'convert_to_invoice')exigen el aplicativo Finanzas en el plan de la organización, como ya hacíaPOST /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_VIEWeINVOICES_CREATE; al token,crm:budgets:update,crm:invoices:viewycrm:invoices:create, máscrm:invoices:issuepara 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-invoicepide lo mismo quePOST /api/finance/recurring-invoices: el aplicativo Finanzas y la capacidad «Cobros recurrentes» en el plan de la organización, y el permisoRECURRING_INVOICES_CREATE, además deBUDGETS_EDIT. Antes bastabaBUDGETS_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:
fiscalyfiscal_capture. Siguen disponibles la configuración fiscal y las retenciones (fiscal: config_get,retention_rates_getyretention_rates_update) y toda la herramientafinance_settings. - Contabilidad:
accounting,accounting_journal,accounting_ledger,accounting_assets,accounting_closeysuppliers: accounting_accounts. - Comisiones:
commissionsycommissions_admin. - Informes:
finance_reports, salvosummary, el resumen de Finanzas, que sigue disponible. - Las copias del servidor
utilia-crmde 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",featureKeyyfeatureLabel. 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_captureen 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 y en Errores de plan.
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/invoicesadmitetype: "SIMPLIFIED", con su propia serie (T-), un tope de 400 € impuestos incluidos (422 SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED) y destinatario opcional.recipientpasa a ser opcional en el contrato; una ordinaria lo sigue exigiendo completo (400 INVOICE_RECIPIENT_REQUIREDoINVOICE_RECIPIENT_INCOMPLETE). Ver Facturas. pricesIncludeTax, el bloquepayment(factura cobrada en el acto con tu propia pasarela) yconsumerResidence(residencia del consumidor, cotejada con el sistema fiscal).- Régimen del pequeño empresario del IGIC (REPEP). Causa de exención
EXEMPT_IGIC_SMALL_BUSINESSy 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. ErroresIGIC_SMALL_BUSINESS_TAXED_LINE,IGIC_SMALL_BUSINESS_REQUIRES_IGICeIGIC_SMALL_BUSINESS_NOT_ACTIVE. - Rectificativa independiente.
POSTyGET /external/v1/invoices/:id/rectifications: tu aplicación da la causa (REFUNDoCHARGEBACK) y UTILIA OS decide el código legal (R5 o R1), siempre por diferencias. El bloquerefundregistra la devolución hecha en tu pasarela como «Pasarela de pago externa», sin mover dinero. Ver Facturas rectificativas. - 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,rectificationCodeyrectificationType; en el detalle,exemptionReasonyexemptpor línea,rectificationsyeffectiveTotal.GET /external/v1/invoicesfiltra portype. - El reembolso con
createRectifyingInvoice: trueemite 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 con422. - Avisos: el objeto
invoicede los avisos de facturas incluyetype;INVOICE_REFUNDEDadmiterefund.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.
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.
- 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,getCertificateUrlystreamGenerateItemsStatuslanzanMETHOD_NOT_AVAILABLE_WITH_API_KEYcon clave de API. - Avisos con varias claves.
sdk.webhooks.verifyyparseleenX-Utilia-Webhook-Signaturesy aceptan la clave nueva y la anterior, con fecha de caducidad. - Aviso de clave anterior con
onApiKeyExpires(on_api_key_expires), cabeceraX-Utilia-SDKen las peticiones, códigos de credenciales tipados ygetApprovalChallengepara las operaciones MCP que se confirman en el móvil. budgetSignaturescon clave de API:list,getyverifyfuncionan; el resto lanzaMETHOD_NOT_AVAILABLE_WITH_API_KEYantes de hacer la petición.- MCP: los métodos de las siete operaciones nuevas de
integrationsyappsWebhooksTestSigningKey(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-tokendevuelve 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) ySTREAM_TOKEN_UNAVAILABLE(503). Ver Endpoints de tickets.- La clave en
?apiKey=se sigue admitiendo de momento en las conexiones en tiempo real; cuando la API no la admite, responde 401API_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 del401 API_KEY_INVALIDo el422 EXTERNAL_APP_CREATOR_MISSINGde antes.EXTERNAL_APP_CREATOR_MISSINGqueda 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.ipAddressva siempre anull. Hasta ahora llevaba la dirección IP completa de quien abría el presupuesto.view.ipAddressMaskedes nuevo: la misma IP enmascarada con el formato de la API de firmas (***.***.113.45en IPv4; en IPv6, los dos primeros grupos y***), onullen 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, leeview.ipAddressMasked. Ejemplo completo en Avisos: contenido y catálogo de eventos.
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_budgetscongetdeja de devolver la IP completa en el historial de firma.crm_budget_signersycrm_budget_filesya la enmascaraban. - Rutas del panel (
/api/crm/budgets/:id,/signatures,/signatures/:sigIdy/signing-links/audit): se quitaipAddressy el agente de usuario completo, y lleganipAddressMaskedydeviceLabel(«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-keyy.../regenerate-webhook-secretadmitengraceDays(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. - 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-Signaturesigue llevando una sola firma. Cómo comprobarla, en Firma HMAC de los avisos. - 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-candidatesycredential-advisories/:advisoryId/dismiss.webhook/testadmite{"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:
integrationspasa 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_responsibleyapps_list_responsible_candidates), y las dos renovaciones aceptangraceDaysytwoFactorApprovalId.observabilityganawebdav_app_passwords_list_userywebdav_app_passwords_revoke_user. Detalle en la referencia MCP.
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.
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.
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 scopedev:docs:sharey el permisoDEV_DOCS_SHARE(«Compartir documentación con enlace»). Antes bastaba condev:docs:updatey con poder ver la documentación.dev:docs:sharese pide por su nombre: no viaja endev:read,dev:write,dev:fullniadmin: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/shareexigeDEV_DOCS_VIEWyDEV_DOCS_SHARE. - Los enlaces
PRIVATEpiden sesión:GET /api/share/:tokenresponde 401 conerrorCode: SHARE_LOGIN_REQUIREDsi no hay sesión, y 404 si la sesión es de otra organización o no puede leer el documento. LosPUBLICyTEMPORARYno cambian. - Adjuntos de un enlace
PRIVATE:GET /api/share/:token/file/:fileId?format=jsondevuelve{ "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
scopede 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
scopeen la solicitud, la pantalla enseña el conjunto por defecto que se concede (losallowedScopesdel registro o, sin ellos, los de identidad), y también se puede estrechar. - Detalle en Pantalla de consentimiento.
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(sobrePOST /api/mcp/rrhh) ofrecegetScope(),setScope(),import(year)ylist()en JavaScript, yget_scope(),set_scope(),import_year(year)ylist()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:viewpara consultar y listar;hr:attendance:managepara 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.
AnnouncementsServicedocumenta sus reglas: qué ámbitos exige (dev:announcements:view,:managey:publish) y que antes de preparar o publicar un anuncio hay que consultarinstructions().- El número de versión del paquete de Python queda alineado: arrastraba una
cifra antigua en su
package.jsonauxiliar.
Quien venga de la 3.x debe leer antes los cambios incompatibles de la 4.0.0: 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:requestlo expanden los conjuntos:writey:fullde 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.
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. DevuelveplatformAvailable,organizationCanCharge,reasonylivemode. Los seis motivos son estables:PLATFORM_DISABLED,NO_ACCOUNT,ONBOARDING_INCOMPLETE,CHARGES_DISABLED,ACCOUNT_DEAUTHORIZEDyREADY. Exige los aplicativos Finanzas y Stripe y el permisobilling:payment-methods:read; no estrena ningún permiso. - En los dos SDK:
sdk.paymentMethods.getReadiness()en JavaScript ypayment_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.
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 tiposChargingReadinessyChargingReadinessReason. - Retira sin sustituto los métodos del correo al proveedor
getEmailRecipients,sendEmailyreplyEmail, y con ellos los tiposSupplierEmailRecipients,SupplierSendEmailInputySupplierReplyEmailInput.
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.
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-emailyPOST /finance/suppliers/:supplierId/interactions/:interactionId/reply. - Se retiran tres operaciones MCP de la herramienta
suppliers:get_email_recipients,send_emailyreply_email, junto con su parámetroemailMessage. 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,sendEmailyreplyEmail(get_email_recipients,send_emailyreply_emailen Python) y sus tipos de entrada y salida. - Los ámbitos
finance:suppliers:interactions:ready:managesiguen 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.
sdk.subscriptionsfunciona, sobre/external/v1/subscriptions. Hasta ahora atacaba una ruta de la API del panel. Firmas nuevas:clientIddesaparece yuserIdes obligatorio. Se retiranremove(la baja escancel) ystats.sdk.paymentAuthorizationses nuevo (sdk.payment_authorizationsen Python), y ahora sí crea autorizaciones desde la aplicación, con su prueba legal.sdk.mandatesqueda como alias en desuso;listForClientse retira.sdk.invoicesgana cinco acciones:send,charge,markPaid,getPublicLinkyregeneratePublicLink.sdk.invoices.scheduledChargesfunciona sobre la API externa. En Python se reubica bajosdk.invoicescon alias en la raíz, y se retiraretrieve, cuya ruta no existía.sdk.invoices.refunds.createemite el reembolso.cancelPreviewycancelse retiran: deshacer un reembolso es del equipo.sdk.invoices.rectificationsse retira con un error que dice qué usar.sdk.invoices.getPdfUrltambién (sustituto:regeneratePublicLink). Si tu integración lo usó, regenera la clave de API de la aplicación.sdk.webhooks.verifyy.parseverifican cualquier familia de avisos, con tipos por evento.sdk.chat.webhooks.verifyqueda como alias.- Enumerados derivados del esquema real:
InvoiceStatuspasa de 7 a 12 valores yTaxSystemsumaIPSIeINTERNATIONAL. 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/subscriptionsy 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-chargesy/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) ycharge(cobro sin nadie delante). POST /external/v1/invoicesaceptasendEmaily devuelveemailSent. UnemailSenten 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/identifyaceptaclientIdpara vincularlo a una ficha creada antes. POST /users/identifyacepta los datos fiscales:taxIdybillingAddress(constreet,city,postalCodeycountryobligatorios yprovinceopcional). 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 declareidentifymanda siempre.dayOfMonthes opcional en una suscripción mensual: sin él se toma el día destartDate, 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, sinnextStep): la organización tiene apagado el cobro automático con tarjeta. Antes salía comoPAYMENT_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:writeycrm: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_GENERATEDy_PAYMENT_AUTHORIZATION_PENDING. - Cobros programados (ocho):
CHARGE_SCHEDULED,_STARTED,_SUCCEEDED,_FAILED,_REQUIRES_ACTION,_RETRY_SCHEDULED,_RETRIES_EXHAUSTEDy_CANCELLED. - Dos más en dominios existentes:
PAYMENT_METHOD_EXPIRINGyPAYMENT_AUTHORIZATION_CONSUMED. INVOICE_SENTya se emite. Estaba declarado desde el principio y nunca llegaba a nadie.PAYMENT_AUTHORIZATION_GRANTEDy_REVOKEDpasan 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 llegapublicLinkAvailable.
2026-09-03
Ámbitos: el portal del cliente sale de «Modificar contactos»
- CAMBIO QUE ROMPE:
portal_accessexige ámbitos propios. Sus lecturas exigencrm:portal:viewy sus diez escriturascrm:portal:manage; ya no basta concrm:contacts:update. - Sin red de compatibilidad, a propósito. Los tokens con
crm:fullsiguen 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:readpese a terminar en:view, y la gestión fuera decrm: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:mergeycrm:leads:merge. Gobiernan las operacionesdeduplicateymergede 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:writey también decrm: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 concrm:full. Se piden por su nombre. - Las previas por reglas siguen siendo baratas.
deduplicate_previewde 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 demerge, en cambio, la ejecuta la misma operación destructiva y exige el ámbito de fusión. dryRunes 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: mergeañ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) yallowPortalAccessMerge(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.crmes un servicio nuevo, contraPOST /api/mcp/crm, con los sub-espaciosclients,contactsyleads: 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,mergePreviewymergeen clientes y contactos, ymergePreview/mergeen 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, 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) ysdk.mcp.notifications(avisos de la persona que autorizó el token y sus preferencias, en los siete servidores). Tienen página propia. - Los datos bancarios y fiscales dejan de viajar por defecto. Ámbitos nuevos
finance:suppliers:view-sensitiveycrm: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:writeycrm:writedejan de borrar archivos, versiones y carpetas del gestor. Para borrar hace faltadev:files:fullocrm:files:delete. En contrapartida,dev:writeganadev:files:manage. - Vincular archivos a una ficha exige
FILE_MANAGER_LINK_CRM, noCLIENTS_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.
linkContactyunlinkContactdesaparecen y no se sustituyen por nada: ya no hay nada que atar ni que soltar. El equivalente escreateContactpara el alta ydeleteContactpara la baja.getContactydeleteContactson nuevos, yupdateContactcambia 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.marketingpasa a cubrir SEIS herramientas enteras. Se añadencampaigns(marketing_campaigns, 35 operaciones),lists(marketing_lists, 17) ydeliverability(marketing_deliverability, 10), yreportsse completa concomplaints,linkHeatmapyaudienceTech(doce en total). Los mismos nombres ensnake_caseen Python, en variante asíncrona y síncrona.- Qué hizo la audiencia y segundos intentos:
engagementBreakdown(gratuito),resendPreview,createResendylistResends. Los siete grupos no son porciones de una tarta —openedya contiene aclicked— 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. listRecipientsdeja de perder el filtro de EXCLUIDOS (isExcluded) y ganaengagement, 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) yreactivateSubscriber. Apartar no es dar de baja: no se toca el consentimiento. - Política de quejas:
getComplaintPolicyysetComplaintPolicy. 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
nullcuando no hay forma de medirla y un SUELO cuando la hay. Nunca es un cero. - HUECO ESCRITO:
deviceType,emailClientycountrydelistRecipientsse omiten sin el permisoEMAIL_MARKETING_VIEW_RECIPIENT_DETAILy el scope nuevomarketing:recipients:detail:view. La operación responde igual y llegarecipientDetailIncluded: 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 y en Servidor MCP Marketing.
SDK JavaScript y SDK Python: informes de Marketing
- Nuevo
sdk.mcp.marketing.reports, la herramienta MCPmarketing_reportsentera:campaignStats,engagementOverview,clickThrough,topCampaigns,engagementHeatmapyengagementTrends(los mismos seis ensnake_caseen Python, en variante asíncrona y síncrona). Solo lectura y sin capacidad de plan. Detalle en Servidor MCP Marketing. - Las cifras de campaña cambian de base, no solo de nombre: aparecen
accepted,delivered(que valenullsin detección de devueltos),acceptanceRate,clickToOpenRate,metricsAvailabilityybounceDetectionEnabled;deliveryRatequeda sustituida poracceptanceRate, 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. sendStatusdeja de admitirOPENEDyCLICKEDal 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 tiempoopenedAtyclickedAt.- Los errores de plan se distinguen de los de permiso:
isPlanError()/is_plan_errory el objetoplanContext. Ojo, por el carril MCP ese código no viaja: ver Errores de plan.
SDK JavaScript y SDK Python: medición del origen y negocio por campaña
- Nuevo
sdk.mcp.marketing.trackingSettings/tracking_settings, la herramienta MCPmarketing_tracking_settingsentera desde su primera versión:get,updateypreviewLink/preview_link. Configura qué etiquetas de origen se añaden a los enlaces de los correos y si viaja el identificador del destinatario.previewLinkes una función pura: no firma nada y no emite ningún identificador real. - Scope OAuth nuevo
marketing:tracking:manage, que no entra enmarketing:writey hay que pedir de forma explícita (o usarmarketing:full). En la pantalla de autorización aparece con su propio bloque. Ver Descubrimiento OAuth y Pantalla de consentimiento. sdk.mcp.marketing.reportsse completa con la atribución de ingresos:campaignAttribution,attributionSummaryyentityOrigin(los mismos ensnake_caseen Python).topCampaignsadmite ademásCONVERSIONS(oportunidades originadas, no ventas cerradas),REVENUE_ISSUEDyREVENUE_COLLECTED; hasta ahoraCONVERSIONSdevolví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
excludedBlocksconreason: 'missing_scope', que no es un cero. Y las facturas creadas consdk.invoices.create()(API externa v1) nacen sin vínculo comercial y quedan fuera del negocio originado por cualquier campaña, porque esa API no admiteopportunityId. - Los formularios de captación ganan
captureUtmyutmPersistenceen los tipos delead_forms_createylead_forms_update, y el envío público admite_utmy_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.
SDK JavaScript y SDK Python: enlaces con el dominio del cliente
- Nuevo
sdk.mcp.marketing.trackingDomains/tracking_domains, la herramienta MCPemail_tracking_domainsentera desde su primera versión:list,get,getSetupInstructions,create,verify,setEmission,retryCertificateyretire(los mismos ensnake_caseen 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:viewymarketing:deliverability:manage, que ya existían. Las tres lecturas admiten ademásmarketing: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.
retireprograma el fin del servicio y devuelve enserveUntil/serve_untilla 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. createno empieza a medir. El alta solo abre el proceso; la medición se enciende consetEmissioncuando 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_attrae la fecha exacta. Ver Enlaces con tu propio dominio. - Puerta de plan:
create,verify,set_emissionyretry_certificateexigen las dos capacidades del lote de medición, «Analítica avanzada de Marketing» y «Negocio por campaña». Quedan exentas las tres lecturas yretire, 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.budgetsganauploadImageToSection/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) ymigrateLegacyImages/migrate_legacy_images(convertirlas en colocaciones reales, de forma idempotente y tolerando fallos parciales). textWrapqueda 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
deleteFileal 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,unavailablePermissionCountyunavailableModules. Todos los campos son opcionales, para no romper contra un backend anterior.AuthorizedApp.scopequeda 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 porpermissionsyunavailablePermissionCount.
2026-07-12
SDK JavaScript 2.20.0 y SDK Python 2.20.0
- Plataforma de chat extensible: nuevo servicio
sdk.chatsobre/external/v1/chat, con publicación de mensajes como aplicación (v1) y lectura de canales, miembros e historial visible (v2), mássdk.chat.webhooks.verify()para validar localmente la firma HMAC. - Detalle completo en Plataforma de chat.
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/clientslos refleja de forma aditiva:parentClientIden todas las respuestas,parent { id, name, slug }en el detalle y el endpoint nuevoGET /external/v1/clients/:id/group. PATCH /external/v1/clients/:idaceptaparentClientIdpara vincular una filial a su matriz o desvincularla connull. Exige el scope nuevocrm:clients:write:hierarchyademás decrm: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_GROUPyCLIENT_UNLINKED_FROM_GROUP, con payload de solo identificadores. La mutación del vínculo NO disparaCLIENT_UPDATED. - Errores nuevos:
409 CLIENT_HIERARCHY_CONFLICTy422 CLIENT_HIERARCHY_INVALID_TYPE. - Detalle en Endpoints de clientes del CRM y Gestión de 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 deparentClientIdenupdate(en Python,parent_client_idconNoneexplí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.
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, cuyosidson 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-Signaturese calculaba sobre el cuerpo crudo a secas; desde este despliegue se calcula sobre la cadena`${timestamp}.${rawBody}`, dondetimestampes exactamente el valor de la cabeceraX-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 queX-Utilia-Timestampestá 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. - 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.
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
titleydescriptionantes 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_CREATEDregistra el canal (source: 'mcp') y eloauthClientIddel token de origen.
OAuth: el conjunto support:write
- El conjunto
support:writeahora se expande asupport:tickets:createademás desupport:tickets:update. Los clientes OAuth consupport:writeautorizado antes del 12 de mayo de 2026 pueden crear tickets sin volver a dar su consentimiento. Consulta los detalles en la referencia de scopes.
2026-04-08
SDK JavaScript 1.8.0
- Nuevo parámetro
themeengetAuthorizationUrl(),getLoginUrl()yloginWithPopup(): permite solicitar tema claro ('light') u oscuro ('dark') en la pantalla de consentimiento OAuth.
SDK Python 0.7.0
- Nuevo parámetro
themeenget_authorization_url()yget_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.
- 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-configurationpara descubrimiento automático. - Endpoint
/api/oauth/userinfopara obtener claims del usuario autenticado. - Endpoint
/api/oauth/jwkspara 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
MemoryTokenStorageyFileTokenStorage.
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
MemoryTokenStorageyFileTokenStorage.
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 |