Avisos: contenido y catálogo de eventos
La forma común de los avisos que UTILIA OS envía a tu aplicación y los 115 eventos que puede recibir, con ejemplos del contenido por área.
Actualizado el . 24 min de lectura
Incluido en todos los planes. Ver precios
Todos los avisos que UTILIA OS envía a tu servidor comparten una forma común. Aquí están esa forma, los 115 eventos que puede recibir tu aplicación, agrupados por área, y ejemplos del contenido de los más usados. Cómo configurarlos y comprobar su firma se explica en Avisos a tu servidor: visión general.
Forma común de los avisos
- eventstringobligatorio
- Nombre del evento.
- timestampstringobligatorio
- Fecha y hora en que se generó el aviso, en ISO 8601. Es la misma en todos los reintentos. Es informativa: la firma usa la cabecera X-Utilia-Timestamp, que cambia en cada intento.
- app.idstringobligatorio
- Identificador de tu aplicación externa.
- app.slugstringobligatorio
- Identificador legible de tu aplicación.
- installationobject
- Solo en los avisos de una aplicación instalada desde el catálogo: id de la instalación, organizationId y organizationName de la empresa que la instaló. Su id va también en la cabecera X-Utilia-Installation-Id. En las demás aplicaciones la clave no viaja.
- dataobjectobligatorio
- Datos propios de cada evento.
- meta.schemaVersionstringobligatorio
- Versión de la forma del contenido (2026-05; 2026-07 en los eventos de chat). Viaja también en la cabecera X-Utilia-Schema-Version.
- livemodeboolean
- Solo en las muestras de un evento, y siempre en false: las que se envían desde la pestaña Pruebas de la aplicación o con POST /external/v1/diagnostics/webhook-sample. Los avisos reales no lo llevan.
- meta.testboolean
- Solo en las muestras de un evento, y siempre en true. Trata como prueba todo aviso con livemode: false o meta.test: true, después de comprobar su firma. Ver Avisos de prueba en «Probar tu integración».
Enrutado: dirigido o por difusión
Cada evento del catálogo se entrega de una de dos maneras, y la diferencia importa porque decide qué recibe tu aplicación:
- Dirigido: solo llega a la aplicación dueña del recurso. Todos los avisos de suscripciones y de cobros son dirigidos, y también los de facturas, métodos de pago y autorizaciones cuando el recurso pertenece a un usuario de una aplicación externa. Sin aplicación dueña no hay destinatario.
- Por difusión: llega a todas las aplicaciones suscritas a ese evento. Es como viajan los avisos de los recursos propios de la organización, que no pertenecen a ninguna aplicación en concreto.
Un mismo nombre de evento puede viajar de las dos formas según el recurso: una factura emitida por tu aplicación te llega dirigida, y una emitida por el equipo desde el panel viaja por difusión.
Catálogo de eventos
Los 115 eventos, por área. La lista que ves al configurar los avisos en la ficha de tu aplicación ya está recortada por el plan de tu organización: un evento de un aplicativo o una capacidad que no tienes no aparece. Los cinco de las instalaciones del catálogo no salen en esa lista: llegan solos. Qué exige cada área está en Eventos disponibles.
Tickets de soporte
- TICKET_CREATED
- TICKET_REPLIED
- TICKET_RESOLVED
- TICKET_CLOSED
- TICKET_CONVERTED
- TICKET_ASSIGNED
- TICKET_REOPENED
Usuarios externos
- USER_CREATED
Archivos
- FILE_UPLOADED
Errores del sistema
- ERROR_REPORTED
- ERROR_RESOLVED
- ERROR_REOPENED
- ERROR_SILENCED
- ERROR_LINKED_TO_TICKET
- ERROR_LINKED_TO_ISSUE
ERROR_REPORTED llega una sola vez por grupo, con su primera ocurrencia. ERROR_RESOLVED llega cuando alguien resuelve el grupo, desde la pestaña Errores de la aplicación, por la API o al cerrar el ticket o la tarea vinculados. ERROR_REOPENED dice en reason si alguien lo reabrió (manual) o si volvió a ocurrir después de resuelto (regression). Los tres últimos llegan cuando alguien silencia el grupo, desde la pestaña o por la API, o lo vincula con un ticket de soporte o con una tarea de Gestión de proyectos. Qué es cada uno está en Pestaña de errores de la aplicación.
Salud de tu aplicación
- APP_HEALTH_CHANGED
UTILIA OS vigila sola la salud de cada aplicación con su tráfico real y sus cambios de configuración, sin hacer llamadas de prueba, y este aviso llega a la propia aplicación cada vez que cambia su estado: ok (funciona), attention (hay algo que revisar, o funciona solo en parte), failing (algo bloquea y hay llamadas o avisos que fallan) o idle (sin uso en 30 días). Exige, como el resto, los avisos activados, el evento marcado y el aplicativo Integraciones en el plan.
statusnunca valepaused. Una aplicación desactivada o eliminada no recibe avisos: rechaza todas sus llamadas conAPP_DISABLED, que ya lo dice.- Al volver a activarla, el primer aviso que llega trae
previousStatus: paused. - La primera comprobación de una aplicación no se avisa: es su estado de partida, no un cambio.
Son los mismos estados que devuelve el diagnóstico en verdict.status; qué los provoca está en Salud de la aplicación y la forma del aviso, en APP_HEALTH_CHANGED.
Instalaciones del catálogo
- APP_INSTALLED
- APP_UNINSTALLED
- APP_ACCESS_CHANGED
- APP_SUSPENDED
- APP_REINSTATED
Solo los recibe una aplicación del catálogo de aplicaciones, de cada empresa que la instala. No se marcan: llegan solos a la dirección de avisos de la ficha, firmados con la clave de esa instalación, con installation en el sobre y la cabecera X-Utilia-Installation-Id. APP_INSTALLED llega cuando tu servidor canjea el primer código de la instalación; APP_UNINSTALLED, cuando la empresa la desinstala, antes de cortar el acceso; APP_ACCESS_CHANGED, cuando cambian sus permisos; y APP_SUSPENDED y APP_REINSTATED, cuando UTILIA la suspende o la reanuda. Qué trae cada uno está en Avisos de cada instalación.
Suscripción de una aplicación de pago
- APP_SUBSCRIPTION_STARTED
- APP_SUBSCRIPTION_TRIAL_ENDING
- APP_SUBSCRIPTION_RENEWED
- APP_SUBSCRIPTION_CHANGED
- APP_SUBSCRIPTION_PRICE_CHANGE_SCHEDULED
- APP_SUBSCRIPTION_PAST_DUE
- APP_SUBSCRIPTION_PAUSED
- APP_SUBSCRIPTION_RESUMED
- APP_SUBSCRIPTION_CANCELED
- APP_SUBSCRIPTION_REACTIVATED
- APP_SUBSCRIPTION_REFUNDED
- APP_SUBSCRIPTION_ENDED
Solo los recibe una aplicación «De pago en UTILIA» del catálogo, por cada empresa que la contrata, igual que los de la instalación: sin marcarlos, firmados con la clave de esa instalación y con X-Utilia-Installation-Id. APP_SUBSCRIPTION_STARTED llega justo después de APP_INSTALLED y APP_SUBSCRIPTION_ENDED, antes de APP_UNINSTALLED. Todos traen en data el bloque subscription de la instalación; cuándo llega cada uno y qué más trae está en Avisos de cada instalación.
Presupuestos
- BUDGET_CREATED
- BUDGET_UPDATED
- BUDGET_SENT
- BUDGET_APPROVED
- BUDGET_REJECTED
- BUDGET_CANCELLED
- BUDGET_EXPIRED
- BUDGET_DUPLICATED
- BUDGET_CONVERTED_TO_PROJECT
- BUDGET_CONVERTED_TO_INVOICE
- BUDGET_CONVERTED_TO_RECURRING_INVOICE
- BUDGET_COMMENT_ADDED
- BUDGET_COMMENT_UPDATED
- BUDGET_COMMENT_DELETED
- BUDGET_SIGNED
- BUDGET_SIGNING_LINK_GENERATED
- BUDGET_SIGNING_LINK_REVOKED
- BUDGET_VIEWED
- BUDGET_PARTIALLY_SIGNED
- BUDGET_SIGNING_LINK_REGENERATED
- BUDGET_SIGNING_LINK_EXPIRED
- BUDGET_VALIDITY_EXTENDED
- BUDGET_IMAGE_PLACEMENT_CREATED
- BUDGET_IMAGE_PLACEMENT_UPDATED
- BUDGET_IMAGE_PLACEMENT_DELETED
- BUDGET_IMAGE_PLACEMENT_REPOSITIONED
- BUDGET_IMAGE_PLACEMENT_CROPPED
- BUDGET_IMAGE_PLACEMENT_REORDERED
Como el resto, los de firma electrónica y los de imágenes solo llegan a las aplicaciones que los marquen en su configuración de avisos.
BUDGET_VIEWED no lleva la dirección IP completa de quien abre el presupuesto: solo view.ipAddressMasked. Su forma está en el ejemplo de más abajo.
Facturación
- INVOICE_CREATED
- INVOICE_SENT
- INVOICE_PAID
- INVOICE_PARTIALLY_PAID
- INVOICE_PAYMENT_FAILED
- INVOICE_OVERDUE
- INVOICE_CANCELLED
- INVOICE_REFUNDED
- INVOICE_REFUND_CANCELLED
- INVOICE_RECTIFIED
- INVOICE_RECTIFICATION_CREATED
- INVOICE_RECTIFICATION_METADATA_UPDATED
Métodos de pago
- PAYMENT_METHOD_ADDED
- PAYMENT_METHOD_REMOVED
- PAYMENT_METHOD_EXPIRING
Autorizaciones de cobro
- PAYMENT_AUTHORIZATION_GRANTED
- PAYMENT_AUTHORIZATION_REVOKED
- PAYMENT_AUTHORIZATION_CONSUMED
Suscripciones
- SUBSCRIPTION_CREATED
- SUBSCRIPTION_UPDATED
- SUBSCRIPTION_PAUSED
- SUBSCRIPTION_RESUMED
- SUBSCRIPTION_CANCELLED
- SUBSCRIPTION_COMPLETED
- SUBSCRIPTION_INVOICE_GENERATED
- SUBSCRIPTION_PAYMENT_AUTHORIZATION_PENDING
Cobros programados
- CHARGE_SCHEDULED
- CHARGE_STARTED
- CHARGE_SUCCEEDED
- CHARGE_FAILED
- CHARGE_REQUIRES_ACTION
- CHARGE_RETRY_SCHEDULED
- CHARGE_RETRIES_EXHAUSTED
- CHARGE_CANCELLED
Contactos del CRM
- CONTACT_CREATED
- CONTACT_UPDATED
- CONTACT_DELETED
- CONTACT_OPTED_OUT
- CONTACT_OPTED_IN
Leads del CRM
- LEAD_CREATED
- LEAD_UPDATED
- LEAD_QUALIFIED
- LEAD_DISQUALIFIED
- LEAD_CONVERTED
- LEAD_SCORE_UPDATED
Clientes del CRM
- CLIENT_CREATED
- CLIENT_UPDATED
- CLIENT_DEACTIVATED
- CLIENT_REACTIVATED
- CLIENT_FISCAL_DATA_UPDATED
- CLIENT_LINKED_TO_GROUP
- CLIENT_UNLINKED_FROM_GROUP
Grupos de empresas. CLIENT_LINKED_TO_GROUP y CLIENT_UNLINKED_FROM_GROUP avisan de que una filial se vincula a su empresa matriz o se desvincula. Viajan solo con identificadores (parentClientId, childClientId, organizationId y, en la desvinculación, reason): si necesitas los datos de los clientes, pídelos a la API. Cambiar el vínculo no dispara CLIENT_UPDATED, y al disolver un grupo llega un CLIENT_UNLINKED_FROM_GROUP por filial con reason=DISSOLVE.
Sin bucles. Los eventos CONTACT_*, LEAD_* y CLIENT_* llegan por difusión a todas las aplicaciones suscritas, salvo a la que provocó el cambio: si tu aplicación registra la baja de un contacto, no recibe el aviso de su propia baja. LEAD_CONVERTED incluye contactId y, si se creó, opportunityId. No existen LEAD_DELETED ni CLIENT_DELETED: la API externa no borra leads ni clientes, los descarta (/disqualify) o los desactiva (/deactivate). El IBAN no viaja nunca en un aviso de cliente. Detalle en Contactos, Leads y Clientes.
Tarjeta guardada no es permiso para cobrar. Los eventos PAYMENT_AUTHORIZATION_* cubren la firma y la retirada del consentimiento con el que la organización cobra suscripciones y cobros programados sin el usuario delante. Son distintos de PAYMENT_METHOD_*: guardar una tarjeta (PAYMENT_METHOD_ADDED) no autoriza cobros automáticos; para eso el usuario firma un texto de consentimiento con su versión.
Facturas dirigidas o por difusión. Los eventos INVOICE_* de una factura emitida por una aplicación externa (source = EXTERNAL_APP) solo llegan a esa aplicación. Los de una factura emitida desde el panel (source = UTILIA_NATIVE) llegan a todas las aplicaciones suscritas. Las rectificativas heredan el origen de la factura que corrigen: INVOICE_RECTIFICATION_CREATED e INVOICE_RECTIFIED de una factura de tu aplicación llegan solo a tu aplicación, tanto si la rectificativa la emite ella con POST /external/v1/invoices/:id/rectifications como si la emite tu equipo desde UTILIA OS. Solo se difunden cuando la original y la rectificativa son las dos del panel.
Tipo de documento. Desde el 13 de octubre de 2026, el objeto invoice de todos los avisos de factura lleva type: STANDARD (ordinaria), SIMPLIFIED (simplificada), RECTIFYING (rectificativa) o CREDIT_NOTE (abono). Las facturas anteriores a ese campo llegan como STANDARD.
Serie. Desde el 15 de octubre de 2026, el objeto invoice de todos los avisos de factura lleva también seriesCode: el código de la serie de tu aplicación en la que se numeró o DEFAULT para la serie de siempre de su tipo. Las rectificativas y los abonos llegan siempre con DEFAULT, porque van a su propia serie. Ver Series disponibles.
Número impreso. Desde el 19 de octubre de 2026, todos los bloques de factura de los avisos INVOICE_* (invoice y, en los de rectificación, originalInvoice o rectifyingInvoice) llevan también displayNumber, el número tal como se imprime: invoiceNumber con un guion entre el año y el número de orden (T-202600001 se imprime T-2026-00001). Úsalo para enseñarlo; guarda y concilia por invoiceNumber.
Sistema impositivo y Estado de consumo. Desde el 15 de octubre de 2026, el objeto invoice (y originalInvoice o rectifyingInvoice en los avisos de rectificación) lleva taxSystem, consumerCountryCode y ossScheme. consumerCountryCode es el Estado miembro del consumidor (nulo si no hay); en la difusión de las facturas que emite tu equipo llega siempre nulo, porque es un dato del consumidor. ossScheme solo tiene valor en las facturas de la ventanilla única del IVA (EU_OSS), que está en «Próximamente». Los avisos no llevan líneas ni la prueba de la ubicación del consumidor.
La divisa llega en minúsculas
Los campos currency de los avisos de factura y de pago llegan en minúsculas (eur, usd), y eur es el valor por defecto cuando no se indicó divisa. Compara siempre sin distinguir mayúsculas y minúsculas (currency.toLowerCase() === 'eur') o tu condición fallará sin avisar.
Plataforma de chat
- CHAT_MESSAGE_CREATED
- CHAT_MESSAGE_UPDATED
- CHAT_MESSAGE_DELETED
- CHAT_CHANNEL_CREATED
- CHAT_CHANNEL_UPDATED
- CHAT_CHANNEL_ARCHIVED
- CHAT_MEMBER_ADDED
- CHAT_MEMBER_REMOVED
- CHAT_APP_MENTIONED
- CHAT_AGENT_TASK_EXECUTED
- CHAT_AGENT_PENDING_APPROVAL
- CHAT_AGENT_APPROVED
- CHAT_AGENT_REJECTED
- CHAT_AGENT_BUDGET_EXCEEDED
Los eventos CHAT_* tienen su propia forma y viajan con meta.schemaVersion = 2026-07 (el resto usa 2026-05). Son dirigidos: solo llegan a las aplicaciones que participan en el canal y tienen el permiso correspondiente, y nunca transportan mensajes internos ni privados. Su contenido y ejemplos de receptor están en Webhooks de chat.
Ejemplos del contenido
TICKET_CREATED
{
"event": "TICKET_CREATED",
"timestamp": "2026-02-08T00:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"ticket": {
"id": "t_1",
"ticketKey": "mi-aplicacion-0001",
"title": "No puedo iniciar sesión",
"category": "BUG",
"priority": "MEDIA",
"status": "OPEN",
"createdAt": "2026-02-08T00:00:00.000Z"
},
"user": {
"externalId": "user_01HXYZ",
"email": "cliente@ejemplo.com",
"name": "Ana Cliente"
}
}
}ERROR_REPORTED
{
"event": "ERROR_REPORTED",
"timestamp": "2026-02-27T12:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"hash": "a1b2c3d4e5f6...",
"message": "Connection timeout to payment gateway",
"module": "payment-processor",
"severity": "critical",
"deduplicated": false,
"release": "2.4.1",
"environment": "production",
"count": 1
}
}El aviso se emite una única vez por hash, en la primera ocurrencia del error; las repeticiones posteriores suben el contador sin generar entrega. count son las ocurrencias ya contadas al emitirlo, y release y environment son los que envió tu aplicación, o null. deduplicated llega siempre con valor false y se conserva solo por compatibilidad del contrato: no construyas lógica sobre este campo ni esperes un aviso por cada repetición.
ERROR_RESOLVED
{
"event": "ERROR_RESOLVED",
"timestamp": "2026-02-27T14:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"hash": "a1b2c3d4e5f6...",
"resolvedBy": "user-admin-id",
"note": "Reiniciado el servicio de pagos",
"reason": "manual"
}
}resolvedBy es quien lo resolvió: la persona del equipo o, si lo resolvió tu aplicación por la API, su persona responsable. Si lo resuelve el equipo de soporte de UTILIA, resolvedBy y note llegan vacíos (null): su nota es interna. reason es hoy siempre manual.
ERROR_REOPENED
reason dice por qué se reabrió: manual, si alguien lo reabrió (reopenedBy lo identifica), o regression, si estaba resuelto y volvió a ocurrir (reopenedBy llega null). En una regresión, release trae la versión en la que reapareció, si tu aplicación la envía.
{
"event": "ERROR_REOPENED",
"timestamp": "2026-02-27T16:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"hash": "a1b2c3d4e5f6...",
"reopenedBy": null,
"reason": "regression",
"release": "2.5.0"
}
}ERROR_SILENCED
until es la fecha hasta la que el grupo de errores queda silenciado, o null si se ha quitado el silencio. silencedBy es nulo cuando la acción no la origina una persona.
{
"event": "ERROR_SILENCED",
"timestamp": "2026-08-04T09:15:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"hash": "a1b2c3d4e5f6...",
"until": "2026-08-11T09:15:00.000Z",
"silencedBy": "user-admin-id"
}
}ERROR_LINKED_TO_TICKET
{
"event": "ERROR_LINKED_TO_TICKET",
"timestamp": "2026-08-04T09:20:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"hash": "a1b2c3d4e5f6...",
"ticket": { "id": "tkt_123", "ticketKey": "SUP-482" },
"linkedBy": "user-admin-id"
}
}ERROR_LINKED_TO_ISSUE
{
"event": "ERROR_LINKED_TO_ISSUE",
"timestamp": "2026-08-04T09:25:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"hash": "a1b2c3d4e5f6...",
"issue": { "id": "iss_456", "key": "PROJ-123" },
"linkedBy": "user-admin-id"
}
}APP_HEALTH_CHANGED
{
"event": "APP_HEALTH_CHANGED",
"timestamp": "2026-10-21T10:20:00.000Z",
"app": { "id": "ckapp1234567890", "slug": "mi-integracion" },
"data": {
"status": "failing",
"previousStatus": "ok",
"title": "Falla",
"since": "2026-10-21T10:20:00.000Z",
"findings": [
{
"code": "REJECTED_CALLS",
"severity": "blocking",
"fixedBy": "developer",
"since": "2026-10-21T10:00:00.000Z",
"count": 42,
"route": "/external/v1/tickets",
"errorCode": "VALIDATION_ERROR"
}
],
"diagnosticsPath": "/external/v1/diagnostics"
}
}title es el nombre corto del estado nuevo, tal como lo manda el servidor; para decidir, usa status (ok, attention, failing o idle), que no cambia de nombre. En el panel, failing se lee No funciona. previousStatus puede valer además paused, en el primer aviso después de reactivar la aplicación. findings trae los hallazgos de la evaluación con su código, su gravedad, quién lo arregla y, si los hay, desde cuándo, cuántas veces, la ruta y el errorCode; nunca texto libre ni datos personales. El texto y el arreglo de cada hallazgo te los da GET /external/v1/diagnostics con tu clave, y cada código tiene su apartado en Hallazgos del diagnóstico. La muestra que se envía desde las pruebas tiene esta misma forma.
CLIENT_LINKED_TO_GROUP
{
"event": "CLIENT_LINKED_TO_GROUP",
"timestamp": "2026-07-05T10:15:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"parentClientId": "cli-matriz-uuid",
"childClientId": "cli-filial-uuid",
"organizationId": "org-uuid",
"occurredAt": "2026-07-05T10:15:00.000Z"
}
}CLIENT_UNLINKED_FROM_GROUP
{
"event": "CLIENT_UNLINKED_FROM_GROUP",
"timestamp": "2026-07-05T11:40:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"parentClientId": "cli-matriz-uuid",
"childClientId": "cli-filial-uuid",
"organizationId": "org-uuid",
"reason": "DISSOLVE",
"occurredAt": "2026-07-05T11:40:00.000Z"
}
}BUDGET_VIEWED
Alguien ha abierto el presupuesto: desde el enlace del correo o dentro del Portal del Cliente. Se envía con la primera apertura de cada ventana de 30 minutos, no con cada recarga.
{
"event": "BUDGET_VIEWED",
"timestamp": "2026-10-12T09:30:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"budget": { "id": "bud-uuid" },
"view": {
"viewTokenId": "tok-uuid",
"viewedAt": "2026-10-12T09:30:00.000Z",
"ipAddress": null,
"ipAddressMasked": "***.***.113.45",
"channel": "public-view-link"
}
}
}- view.channelstringobligatorio
- public-view-link si se abrió desde el enlace del correo; portal si se abrió dentro del Portal del Cliente.
- view.viewTokenIdstring | nullobligatorio
- Enlace con el que se abrió. null en el portal, que no usa enlace.
- view.ipAddressMaskedstring | nullobligatorio
- IP de quien lo abrió, enmascarada: en IPv4 se ocultan los dos primeros bloques (***.***.113.45); en IPv6 se conservan los dos primeros grupos y el resto pasa a ***. null en el portal y cuando no hay dirección.
- view.ipAddressnullobligatorio
- Siempre null. Se conserva para no romper a quien ya leía el campo.
Cambio incompatible: la IP completa ya no viaja
Antes, view.ipAddress llevaba la dirección IP completa de quien abría el presupuesto. Ahora va siempre a null y la dirección enmascarada llega en view.ipAddressMasked. Las entregas guardadas anteriores también se han enmascarado, así que reintentar una antigua no envía la IP completa. Si tu integración la usaba, lee view.ipAddressMasked; la IP completa de quien firma solo está en el certificado de la firma, dentro de UTILIA OS.
INVOICE_CREATED
{
"event": "INVOICE_CREATED",
"timestamp": "2026-04-19T10:32:15.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"type": "STANDARD",
"seriesCode": "DEFAULT",
"taxSystem": "IGIC",
"consumerCountryCode": null,
"ossScheme": null,
"status": "ISSUED",
"totalAmount": 53.45,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": null
},
"externalUser": { "externalId": "user_01HXYZ" },
"metadata": { "orderId": "ORD-4471" }
}
}metadata solo viaja en INVOICE_CREATED: es el objeto metadata que tu aplicación envió al crear la factura por la API externa, devuelto tal cual salvo las claves reservadas de UTILIA OS (como externalIdempotencyKey o la residencia del consumidor), que se quitan antes de enviarlo. Llega null si la factura se creó sin metadata.
INVOICE_PAID
{
"event": "INVOICE_PAID",
"timestamp": "2026-04-20T11:22:33.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"status": "PAID",
"totalAmount": 53.45,
"paidAmount": 53.45,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": null
},
"externalUser": { "externalId": "user_01HXYZ" },
"payment": {
"paymentIntentId": "pi-utilia-uuid",
"amount": 53.45,
"currency": "eur",
"isFullPayment": true
}
}
}INVOICE_PARTIALLY_PAID
{
"event": "INVOICE_PARTIALLY_PAID",
"timestamp": "2026-04-20T11:22:33.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"status": "PARTIALLY_PAID",
"totalAmount": 100.00,
"paidAmount": 53.45,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": null
},
"externalUser": { "externalId": "user_01HXYZ" },
"payment": {
"paymentIntentId": "pi-utilia-uuid",
"amount": 53.45,
"currency": "eur",
"isFullPayment": false
}
}
}INVOICE_PAYMENT_FAILED
{
"event": "INVOICE_PAYMENT_FAILED",
"timestamp": "2026-04-20T11:10:05.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"status": "ISSUED",
"totalAmount": 53.45,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": null
},
"externalUser": { "externalId": "user_01HXYZ" },
"payment": {
"paymentIntentId": "pi-utilia-uuid",
"amount": 53.45,
"currency": "eur",
"errorCode": "card_declined",
"errorMessage": "Tu banco ha rechazado la tarjeta.",
"initiator": "SYSTEM_RECURRING"
}
}
}INVOICE_OVERDUE
{
"event": "INVOICE_OVERDUE",
"timestamp": "2026-05-20T06:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"status": "OVERDUE",
"totalAmount": 53.45,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": "2026-05-19T00:00:00.000Z"
},
"externalUser": { "externalId": "user_01HXYZ" }
}
}INVOICE_CANCELLED
{
"event": "INVOICE_CANCELLED",
"timestamp": "2026-04-20T15:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"status": "CANCELLED",
"totalAmount": 53.45,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": null
},
"externalUser": { "externalId": "user_01HXYZ" },
"reason": "Factura duplicada"
}
}INVOICE_REFUNDED
{
"event": "INVOICE_REFUNDED",
"timestamp": "2026-04-22T09:15:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "inv-uuid",
"invoiceNumber": "202600001",
"displayNumber": "2026-00001",
"type": "STANDARD",
"seriesCode": "DEFAULT",
"status": "PARTIALLY_PAID",
"totalAmount": 53.45,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-04-19T00:00:00.000Z",
"dueDate": null
},
"externalUser": { "externalId": "user_01HXYZ" },
"refund": {
"refundId": "ref-uuid",
"paymentIntentId": "pi-utilia-uuid",
"amount": 5345,
"currency": "eur",
"reason": "requested_by_customer",
"isFullRefund": true,
"source": "STRIPE"
}
}
}refund.amount viaja en céntimos, payment.amount no
refund.amount es el importe devuelto en céntimos (en el ejemplo, 5345 son 53,45 €). Los demás importes de los avisos de factura van en unidades de la divisa: invoice.totalAmount, invoice.paidAmount y también payment.amount de INVOICE_PAID, INVOICE_PARTIALLY_PAID e INVOICE_PAYMENT_FAILED. Si lo sumas o lo comparas con ellos sin convertir, obtendrás una cifra cien veces mayor. refund.source dice por dónde se devolvió: STRIPE, MANUAL_BANK, MANUAL_CASH, MANUAL_OTHER o EXTERNAL_GATEWAY.
EXTERNAL_GATEWAY («Pasarela de pago externa») es una devolución que tu
aplicación hizo en su propia pasarela y registró en UTILIA OS al emitir
la rectificativa
con el bloque refund, o que tu equipo registró a mano. UTILIA OS no
movió ese dinero.
INVOICE_RECTIFIED
Llega cuando una rectificativa ya emitida surte efectos sobre la factura
original. invoice es la original, rectifyingInvoice la rectificativa
y rectification su código, su modalidad y su causa impresa. Antes llega
INVOICE_RECTIFICATION_CREATED, con la rectificativa en invoice y la
original en originalInvoice.
{
"event": "INVOICE_RECTIFIED",
"timestamp": "2026-10-13T10:05:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "cksimp1234567890",
"invoiceNumber": "T-202600001",
"displayNumber": "T-2026-00001",
"type": "SIMPLIFIED",
"seriesCode": "DEFAULT",
"status": "RECTIFIED",
"totalAmount": 39.99,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-10-13T00:00:00.000Z",
"dueDate": null
},
"rectifyingInvoice": {
"id": "ckrect1234567890",
"invoiceNumber": "R-202600003",
"displayNumber": "R-2026-00003",
"type": "RECTIFYING",
"seriesCode": "DEFAULT",
"status": "ISSUED",
"totalAmount": -39.99,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-10-13T00:00:00.000Z",
"dueDate": null
},
"rectification": {
"code": "R5",
"type": "DIFFERENCE",
"reason": "Devolución del precio."
},
"externalUser": { "externalId": "user_01HXYZ" }
}
}PAYMENT_METHOD_ADDED
{
"event": "PAYMENT_METHOD_ADDED",
"timestamp": "2026-04-10T12:00:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"externalUser": { "externalId": "user_01HXYZ" },
"paymentMethod": {
"id": "pm-uuid",
"brand": "visa",
"last4": "4242"
}
}
}PAYMENT_METHOD_REMOVED
{
"event": "PAYMENT_METHOD_REMOVED",
"timestamp": "2026-04-15T09:30:00.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"externalUser": { "externalId": "user_01HXYZ" },
"paymentMethod": {
"id": "pm-uuid",
"brand": "visa",
"last4": "4242"
}
}
}PAYMENT_AUTHORIZATION_GRANTED
Se dispara cada vez que un cliente firma una autorización de cobro automático sobre una tarjeta concreta. Una reactivación tras revocar no reusa el mandato anterior: emite un evento nuevo con mandateId distinto.
Forma del campo data
- mandateIdstringobligatorio
- Identificador interno UTILIA del mandato firmado. Único e idempotente.
- clientIdstringobligatorio
- Identificador del cliente al que pertenece la autorización.
- scopeenumobligatorio
- Alcance del mandato: SINGLE_INVOICE, PER_INVOICE_OPT_IN, SCHEDULED_INVOICE, RECURRING o RECURRING_SUBSCRIPTION.
- paymentMethod.idstringobligatorio
- Identificador de UTILIA OS del método de pago, no el de Stripe. Llega como cadena vacía en el caso límite de un mandato sin tarjeta vinculada, así que comprueba que no está vacío antes de usarlo como clave.
- paymentMethod.brandstring
- Marca de la tarjeta (visa, mastercard, amex, etc.). Puede ser null.
- paymentMethod.last4string
- Últimos cuatro dígitos. Puede ser null.
- paymentMethod.expMonthnumber
- Mes de caducidad (1-12). Puede ser null.
- paymentMethod.expYearnumber
- Año de caducidad (cuatro dígitos). Puede ser null.
- consent.versionstringobligatorio
- Versión del texto legal aceptado (no cambia aunque la plantilla evolucione).
- consent.ipAddressstring
- IP enmascarada con los dos primeros octetos (formato a.b.x.x para IPv4 o a:b:x:x:x para IPv6). Puede ser null si el proxy no la pudo resolver.
- consent.userAgentstring
- User-Agent del navegador o cliente que firmó (truncado a 500 caracteres). Puede no llegar.
- consent.signedAtdatetimeobligatorio
- Fecha y hora ISO 8601 de aceptación.
- linkedSubscriptionIdsstring[]obligatorio
- Suscripciones (RecurringInvoice) vinculadas al mandato en el instante de la activación. Puede crecer después: consulta la API antes de cada cobro para tener la lista vigente.
- sourceenum
- Origen del flujo: PORTAL_ADD_CARD, PORTAL_PAY_INVOICE, PORTAL_SUBSCRIPTION, PUBLIC_SETUP_INTENT, STAFF_DASHBOARD u OTHER. Puede ser null.
Ejemplo
{
"event": "PAYMENT_AUTHORIZATION_GRANTED",
"timestamp": "2026-05-09T18:42:13.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"mandateId": "pma_01HQ2X7C9MZK3FQAGBYQ8K2W4P",
"clientId": "cli_01HXYZK6Z3AQAGBYQ8K2W4P",
"scope": "RECURRING_SUBSCRIPTION",
"paymentMethod": {
"id": "pm_01HQ2X7C9MZK3FQAGBYQ8K2W4P",
"brand": "visa",
"last4": "4242",
"expMonth": 4,
"expYear": 2029
},
"consent": {
"version": "v1.2",
"ipAddress": "203.0.x.x",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) AppleWebKit/537.36",
"signedAt": "2026-05-09T18:42:13.000Z"
},
"linkedSubscriptionIds": ["rec_01HXYZK6Z3AQAGBYQ8K2W4P"],
"source": "PORTAL_ADD_CARD"
}
}La IP de consent.ipAddress llega recortada por protección de datos: en IPv4 se conservan los dos primeros bloques y los dos últimos se sustituyen por x.x (por ejemplo, 203.0.x.x); en IPv6 se conservan los dos primeros segmentos y el resto se sustituye por x:x:x. La IP completa queda en el registro de auditoría de UTILIA OS y no se envía por aviso.
paymentMethod.id es el identificador de UTILIA OS del método de pago, estable entre avisos del mismo método. No es el identificador de Stripe, que no se envía nunca.
PAYMENT_AUTHORIZATION_REVOKED
Se dispara cuando una autorización deja de valer. Las suscripciones afectadas pasan a cobro manual en UTILIA OS; si guardas una copia en tu sistema, refleja el cambio.
Forma del campo data
- mandateIdstringobligatorio
- Identificador del mandato revocado. Coincide con el mandateId del GRANTED previo.
- clientIdstringobligatorio
- Identificador del cliente al que pertenecía la autorización.
- scopeenumobligatorio
- Mismo enum que en GRANTED, pero NO es fiable en la revocación: si el mandato ya se ha marcado como revocado en base de datos, el emisor no puede recuperar su alcance original y envía siempre RECURRING_SUBSCRIPTION. No filtres por este campo en los eventos REVOKED.
- revokedByenumobligatorio
- Quién inició la revocación: PORTAL_USER, STAFF, SYSTEM_PAYMENT_METHOD_EXPIRED o SYSTEM_OTHER.
- revokedAtdatetimeobligatorio
- Fecha y hora ISO 8601 en la que se efectuó la revocación.
- revocationReasonenumobligatorio
- Motivo categorizado: USER_REVOKED, STAFF_REVOKED, PAYMENT_METHOD_EXPIRED, PAYMENT_METHOD_DETACHED u OTHER.
- affectedSubscriptionIdsstring[]obligatorio
- Suscripciones del cliente afectadas por la revocación. UTILIA OS las pasa a cobro manual; tu aplicación debería reflejar el cambio. La lista se calcula de forma aproximada (ver aviso más abajo): puede incluir alguna suscripción del mismo cliente que no dependía de este mandato.
Ejemplo
{
"event": "PAYMENT_AUTHORIZATION_REVOKED",
"timestamp": "2026-05-09T19:14:02.000Z",
"app": { "id": "app_1", "slug": "mi-aplicacion" },
"data": {
"mandateId": "pma_01HQ2X7C9MZK3FQAGBYQ8K2W4P",
"clientId": "cli_01HXYZK6Z3AQAGBYQ8K2W4P",
"scope": "RECURRING_SUBSCRIPTION",
"revokedBy": "PORTAL_USER",
"revokedAt": "2026-05-09T19:14:02.000Z",
"revocationReason": "USER_REVOKED",
"affectedSubscriptionIds": ["rec_01HXYZK6Z3AQAGBYQ8K2W4P"]
}
}Dos campos de la revocación que no son exactos
scopeno conserva el alcance original. Al revocar, el mandato pasa a estado revocado en base de datos y el emisor pierde el alcance con el que se firmó, así que rellena siempreRECURRING_SUBSCRIPTION. Un mandato que eraSINGLE_INVOICEllegará igualmente comoRECURRING_SUBSCRIPTIONen el evento REVOKED. Si necesitas el alcance real, guárdalo delPAYMENT_AUTHORIZATION_GRANTEDprevio, que sí lo trae bien, y búscalo pormandateId.affectedSubscriptionIdses una aproximación. Además de las suscripciones que todavía apuntan al mandato, la consulta recoge las del mismo cliente que se hayan quedado sin mandato en el último minuto, porque la cascada interna las desvincula antes de emitirse el webhook. En un cliente con varias suscripciones puede colarse alguna que no dependía de este mandato. Trátala como una lista de candidatas a revisar, no como una verdad cerrada.
Quién y por qué retiró la autorización:
revokedBy = PORTAL_USERconrevocationReason = USER_REVOKED: el cliente la retiró desde el portal del cliente.revokedBy = STAFFconrevocationReason = STAFF_REVOKED: una persona del equipo la retiró desde el panel.revokedBy = SYSTEM_PAYMENT_METHOD_EXPIREDconrevocationReason = PAYMENT_METHOD_EXPIRED: UTILIA OS detectó que la tarjeta del mandato había caducado y retiró la autorización.revokedBy = SYSTEM_OTHERconrevocationReason = PAYMENT_METHOD_DETACHED: la tarjeta se desvinculó en Stripe (por el cliente o por el equipo) y arrastró al mandato.revokedBy = SYSTEM_OTHERconrevocationReason = OTHER: cualquier otra causa interna.
Por protección de datos, los avisos de autorización no incluyen el texto íntegro del consentimiento (solo su versión), el identificador de Stripe del método de pago, la IP completa, el motivo libre que escribe el equipo al retirarla ni el identificador de la persona que firmó o retiró. Todo eso queda en el registro de auditoría de UTILIA OS.
INVOICE_SENT
Se emite cuando la factura sale por correo hacia el usuario final, tanto desde la API externa como cuando la envía el equipo desde el panel.
{
"event": "INVOICE_SENT",
"timestamp": "2026-10-01T09:13:10.482Z",
"app": { "id": "app_...", "slug": "mi-aplicacion" },
"data": {
"invoice": {
"id": "ckinv1234567890",
"invoiceNumber": "202600042",
"displayNumber": "2026-00042",
"status": "SENT",
"totalAmount": 53.45,
"paidAmount": 0,
"currency": "eur",
"issueDate": "2026-10-01T00:00:00.000Z",
"dueDate": "2026-10-08T00:00:00.000Z"
},
"externalUser": { "externalId": "user_01HXYZ" },
"delivery": {
"recipients": ["administracion@estudiomarea.example"],
"sentAt": "2026-10-01T09:13:10.000Z"
}
},
"meta": { "schemaVersion": "2026-05" }
}El bloque payment de INVOICE_PAID e INVOICE_PARTIALLY_PAID incluye
source. Vale manual cuando el cobro se registró fuera de la pasarela (una
transferencia, un pago en efectivo, la llamada a mark-paid), y en ese caso
method dice con qué forma de pago.
Suscripciones
Los ocho avisos del dominio comparten estos campos en data, además de los que
añade cada uno:
- subscriptionIdstringobligatorio
- Identificador de la suscripción en UTILIA OS.
- externalUserIdstring | nullobligatorio
- Identificador del usuario en tu aplicación. Nulo si el usuario ya no existe.
- namestringobligatorio
- Nombre visible de la suscripción.
- statusenumobligatorio
- ACTIVE, PAUSED, CANCELLED o COMPLETED.
- periodAmountCentsintegerobligatorio
- Importe por periodo, en céntimos.
- currencystringobligatorio
- Divisa en minúsculas.
Campos propios de cada aviso:
| Aviso | Campos adicionales |
|---|---|
SUBSCRIPTION_CREATED | frequency, chargeMode, startDate, nextInvoiceDate |
SUBSCRIPTION_UPDATED | changes[] con field, from y to |
SUBSCRIPTION_PAUSED | pausedAt, reason |
SUBSCRIPTION_RESUMED | nextInvoiceDate |
SUBSCRIPTION_CANCELLED | cancelledAt, reason |
SUBSCRIPTION_COMPLETED | completedAt, invoiceCount |
SUBSCRIPTION_INVOICE_GENERATED | invoiceId, invoiceNumber, amountCents, dueDate, emailSent |
SUBSCRIPTION_PAYMENT_AUTHORIZATION_PENDING | nextInvoiceDate, reason |
SUBSCRIPTION_INVOICE_GENERATED
{
"event": "SUBSCRIPTION_INVOICE_GENERATED",
"timestamp": "2026-11-01T06:00:12.104Z",
"app": { "id": "app_...", "slug": "mi-aplicacion" },
"data": {
"subscriptionId": "cksub1234567890",
"externalUserId": "user_01HXYZ",
"name": "Plan profesional mensual",
"status": "ACTIVE",
"periodAmountCents": 5345,
"currency": "eur",
"invoiceId": "ckinv2234567890",
"invoiceNumber": "202600097",
"amountCents": 5345,
"dueDate": "2026-11-08T00:00:00.000Z",
"emailSent": true
},
"meta": { "schemaVersion": "2026-05" }
}SUBSCRIPTION_PAYMENT_AUTHORIZATION_PENDING es el aviso que hay que atender
Significa que la suscripción está configurada para cobrarse sola pero no tiene
autorización vigente. Si nadie hace nada, el periodo se facturará y no se
cobrará. Pide al usuario que autorice el cobro antes de nextInvoiceDate.
Cobros programados
Cada aviso de este dominio cuenta un paso en la vida de un cobro sin el usuario delante. Este es el orden en que pueden llegar:
Los avisos de un cobro programado
Cobro programado
CHARGE_SCHEDULED
Queda fecha para cobrar la factura.Se llega desde Cobro programado cuando: Se cancela antes del cobro
Cobro programado cancelado
CHARGE_CANCELLED
Estado final
Se llega desde Cobro programado cuando: Llega la fecha
Cobro iniciado
CHARGE_STARTED
Empieza un intento de cobro.Se llega desde Cobro iniciado cuando: El banco pide verificar al titular
Necesita que el usuario se identifique
CHARGE_REQUIRES_ACTION
Lleva a tu usuario al enlace público de la factura.Se llega desde Cobro iniciado cuando: La pasarela rechaza el cobro
Cobro fallido
CHARGE_FAILED
Se llega desde Cobro fallido cuando: Quedan intentos
Reintento de cobro programado
CHARGE_RETRY_SCHEDULED
Se llega desde Cobro fallido cuando: No quedan intentos
Reintentos de cobro agotados
CHARGE_RETRIES_EXHAUSTED
Estado final
Se llega desde Cobro iniciado cuando: La pasarela confirma el cobro
Cobro realizado
CHARGE_SUCCEEDED
Estado final
Renueva el servicio de tu usuario.
Otras transiciones
- Reintento de cobro programado pasa a Cobro iniciado: Llega la fecha del reintento.
Los ocho avisos del dominio comparten estos campos en data:
- scheduledChargeIdstring | nullobligatorio
- Cobro programado que originó el aviso. Viaja NULO cuando el cobro lo lanzó la aplicación con POST /invoices/:id/charge, porque en ese caso no hay programación detrás.
- invoiceIdstringobligatorio
- Factura que se cobra.
- invoiceNumberstringobligatorio
- Número fiscal de la factura.
- externalUserIdstring | nullobligatorio
- Identificador del usuario en tu aplicación.
- subscriptionIdstring | nullobligatorio
- Suscripción de la que viene la factura, si viene de una. Nulo en los cobros que lanza la aplicación.
- amountCentsintegerobligatorio
- Importe del cobro, en céntimos.
- currencystringobligatorio
- Divisa en minúsculas.
Campos propios de cada aviso:
| Aviso | Campos adicionales |
|---|---|
CHARGE_SCHEDULED | scheduledAt, cancellableUntil |
CHARGE_STARTED | startedAt, attemptNumber |
CHARGE_SUCCEEDED | paymentIntentId, succeededAt, invoiceFullyPaid |
CHARGE_FAILED | paymentIntentId, failedAt, attemptNumber, failureCode, failureMessage, willRetry |
CHARGE_REQUIRES_ACTION | paymentIntentId, expiresAt, publicUrl, publicLinkAvailable |
CHARGE_RETRY_SCHEDULED | nextRetryAt, attemptNumber, attemptsRemaining |
CHARGE_RETRIES_EXHAUSTED | exhaustedAt, totalAttempts, lastFailureCode, subscriptionPaused |
CHARGE_CANCELLED | cancelledAt, cancelledBy, reason |
CHARGE_REQUIRES_ACTION
Es el más importante de los dieciséis: el cobro no se ha producido y no prosperará sin que el usuario se identifique ante su banco.
{
"event": "CHARGE_REQUIRES_ACTION",
"timestamp": "2026-11-08T08:00:31.902Z",
"app": { "id": "app_...", "slug": "mi-aplicacion" },
"data": {
"scheduledChargeId": "cksch1234567890",
"invoiceId": "ckinv2234567890",
"invoiceNumber": "202600097",
"externalUserId": "user_01HXYZ",
"subscriptionId": "cksub1234567890",
"amountCents": 5345,
"currency": "eur",
"paymentIntentId": "pi_3QabcDEFghiJKLmn",
"expiresAt": "2026-11-09T08:00:00.000Z",
"publicUrl": null,
"publicLinkAvailable": true
},
"meta": { "schemaVersion": "2026-05" }
}La dirección del enlace no llega siempre:
- Cobro que lanza tu aplicación con
POST /invoices/:id/charge:publicUrlsí trae la dirección, porque el emisor acaba de crear el enlace y conoce su valor.scheduledChargeIdysubscriptionIdllegan nulos. - Cobro programado:
publicUrlllega nulo. De un enlace emitido solo se guarda su huella, así que no se puede reconstruir, y generar otro sería abrir un acceso público que nadie ha pedido. LlegapublicLinkAvailable: truey tu aplicación pide la dirección conPOST /external/v1/invoices/:id/public-link.
En los cobros programados, failureCode de CHARGE_FAILED llega nulo y el
motivo viene como texto en failureMessage. En los cobros que lanza tu
aplicación, failureCode sí trae el código de la pasarela.
PAYMENT_METHOD_EXPIRING
Avisa de que la tarjeta con la que se cobran las suscripciones o los cobros
programados de un usuario de tu aplicación está a punto de caducar. Llega antes
de que falle el cobro, para que pidas a tiempo una tarjeta nueva con
POST /external/v1/payment-methods/setup-intent.
Avisos de una tarjeta que caduca
30 días antes
Primer aviso
Al entrar en la ventana de 30 días.Espera de 23 días hasta el siguiente.
7 días antes
Segundo aviso
Al entrar en la de 7 días.Fin del mes
La tarjeta caduca
Una tarjeta 11/2026 vale hasta el 30 de noviembre.
Cómo funciona:
- Cuándo. Una tarea diaria, a las 09:40 UTC, revisa las tarjetas. Cada una recibe un aviso al entrar en la ventana de 30 días y otro al entrar en la de 7, nunca más. Una tarjeta que ya entra con 7 días o menos (se guardó con poca vida o la tarea no pudo pasar) recibe solo el de 7. Si la red renueva la tarjeta y cambian su mes o su año de caducidad, los avisos vuelven a empezar para la fecha nueva.
- Qué tarjetas. Solo las activas que respaldan, para un usuario de tu aplicación, una suscripción activa con cobro automático o un cobro programado que aún se va a intentar. Una tarjeta guardada que no paga nada no avisa.
- A quién. Es un aviso dirigido: solo a la aplicación dueña de esas suscripciones y cobros, si está activa y suscrita al evento. Si te suscribes a mitad de una ventana, recibes su aviso en la siguiente pasada diaria. Un aviso por tarjeta y aplicación, con todos los cobros que dependen de ella.
- Plan. La organización tiene que estar activa y tener en su plan el cobro automático de facturas, que incluye Finanzas, y Pagos con tarjeta (Stripe).
El cuerpo no dice si es el aviso de 30 días o el de 7: calcúlalo con expMonth
y expYear si lo necesitas. brand y last4 llegan vacíos si la pasarela no
los dio, y externalUserId es el identificador del usuario en tu aplicación.
{
"event": "PAYMENT_METHOD_EXPIRING",
"timestamp": "2026-10-31T09:40:02.000Z",
"app": { "id": "app_...", "slug": "mi-aplicacion" },
"data": {
"paymentMethodId": "ckpm1234567890",
"externalUserId": "user_01HXYZ",
"brand": "visa",
"last4": "4242",
"expMonth": 11,
"expYear": 2026,
"affectedSubscriptionIds": ["cksub1234567890"],
"affectedScheduledChargeIds": []
},
"meta": { "schemaVersion": "2026-05" }
}PAYMENT_AUTHORIZATION_CONSUMED
Una autorización de un solo uso se ha agotado al ejecutarse el cobro que la
respaldaba. Deja de servir: si hace falta cobrar otra vez, el usuario tiene que
autorizarlo de nuevo. Las autorizaciones de alcance RECURRING no se consumen.
Comprueba la firma de cada aviso
Todos los avisos llevan la firma HMAC en dos cabeceras: X-Utilia-Signature, con una sola firma, y X-Utilia-Webhook-Signatures, con la lista de firmas separadas por comas, la de la clave vigente primero. Fuera de una renovación de la clave las dos llevan la misma. Mientras dura el periodo de transición de una renovación, la lista lleva también la firma de la clave anterior. Acepta el aviso si alguna firma de la lista coincide con alguna de las claves que tenga tu servidor. El cálculo y el código de ejemplo están en Firma HMAC de los avisos.
Buenas prácticas al procesarlos
- Procesa cada aviso una sola vez, usando
X-Utilia-Delivery-Idpara descartar repeticiones. - Todos los avisos siguen la misma política de reintentos: hasta 5 intentos con esperas de 5, 10, 20 y 40 segundos. Detalle en Entregas y reintentos de los avisos.
- En
PAYMENT_AUTHORIZATION_*, la parejamandateIdmástimestampidentifica un cambio: un mismo mandato no recibe dosGRANTEDni dosREVOKEDcon el mismotimestamp. Si guardas autorizaciones, usa esa pareja para detectar duplicados antes de aplicar el cambio.
Siguiente paso
Comprueba la firma de cada aviso antes de procesarlo, como se explica en Firma HMAC de los avisos.