Avisos de cada instalación
Cómo te llegan los avisos de cada empresa que instala tu aplicación: el sobre, la cabecera, la firma por instalación, los cinco avisos del ciclo de vida y los de la suscripción de una aplicación de pago.
Actualizado el . 9 min de lectura
Cada empresa que instala tu aplicación te manda sus avisos a la dirección de avisos de tu ficha, firmados con la clave de firma de su instalación. Así, con una sola dirección, sabes de qué empresa viene cada aviso y con qué clave comprobarlo antes de leer el cuerpo. Es la misma infraestructura que los avisos a tu servidor de siempre: reintentos, historial de entregas e identificador de entrega.
Qué avisos llegan
Llegan tres tipos de aviso, y solo si la ficha tiene dirección de avisos:
- Los cinco del ciclo de vida de la instalación, siempre, sin marcarlos:
APP_INSTALLED,APP_UNINSTALLED,APP_ACCESS_CHANGED,APP_SUSPENDEDyAPP_REINSTATED. - Los doce de la suscripción, si tu aplicación es de pago, también sin marcarlos:
APP_SUBSCRIPTION_*(más abajo). - Los que declara tu ficha en Avisos que recibirá, pero solo los que cubren los permisos que concedió esa empresa. Un aviso cuyo dato no podrías leer por la API con esos permisos no se envía.
| Avisos | Permiso de la instalación que los abre |
|---|---|
Tickets de soporte (TICKET_*) | support:tickets:read |
Usuarios externos (USER_CREATED) | crm:users:read |
Archivos (FILE_UPLOADED) | fm:files:read |
Contactos del CRM (CONTACT_*) | crm:contacts:read |
Leads del CRM (LEAD_*) | crm:leads:read |
Clientes del CRM (CLIENT_*) | crm:clients:read |
Facturación (INVOICE_*) | crm:invoices:read |
Comentarios de presupuesto (BUDGET_COMMENT_ADDED, _UPDATED, _DELETED) | crm:budgets:comments:read |
Firma de presupuesto (BUDGET_SIGNED, BUDGET_PARTIALLY_SIGNED, BUDGET_VIEWED y los de su enlace de firma) | crm:budgets:signatures:read |
Plataforma de chat (CHAT_*) | chat:events:subscribe; los de agentes, además chat:agents:view |
El resto de avisos de presupuestos, los de cobros, suscripciones, métodos de pago, autorizaciones de cobro, errores y salud de la aplicación no llegan nunca a una aplicación del catálogo. Además, como en cualquier aplicación, un aviso de un aplicativo que la empresa no tiene en su plan no sale. La lista que te toca en cada instalación es la de installation.webhookEvents del canje.
Los avisos de datos solo salen mientras la instalación funciona: no llegan si está sin conectar, suspendida o desinstalada.
El sobre
Es el de siempre, con un campo más, installation, que solo llevan los avisos de una aplicación instalada desde el catálogo:
{
"event": "CLIENT_CREATED",
"timestamp": "2026-11-03T10:02:11.000Z",
"app": { "id": "cm3k9x2a50002qz8hm4n5p6q7", "slug": "acme-recordatorios-k7f2" },
"installation": {
"id": "cm3k9x2a40001qz8h7d1v2b3c",
"organizationId": "cm1a2b3c40000ab12cd34ef56",
"organizationName": "Ferretería López"
},
"data": { "...": "el contenido del evento, igual que en cualquier aplicación" },
"meta": { "schemaVersion": "2026-05" }
}app es la aplicación que UTILIA OS crea dentro de la empresa para esa instalación, no la tuya: para saber de quién es el aviso, usa installation. El contenido de data de cada evento es el del catálogo de eventos.
Además de las cabeceras de siempre (X-Utilia-Signature, X-Utilia-Timestamp, X-Utilia-Event, X-Utilia-Delivery-Id...), cada entrega lleva:
- X-Utilia-Installation-Idstringobligatorio
- El id de la instalación, el mismo que installation.id. Úsalo para elegir la clave de firma antes de leer el cuerpo. Va también en los reintentos.
Comprobar la firma
La firma se calcula igual que en cualquier aviso, HMAC-SHA256(claveDeFirma, timestamp + "." + cuerpo), pero con la clave de firma de esa instalación: la que recibiste en el canje. Con la clave de otra instalación o la de tu propia aplicación no coincide nunca.
Comprobar un aviso de una instalación
X-Utilia-Installation-Id
Lee la instalación
Antes de interpretar el cuerpo.Tu base de datos
Busca su clave de firma
La que guardaste al canjear su código.Si no la tienes: responde 401 y revisa tus canjes.
X-Utilia-Signature
Comprueba la firma
Sobre el cuerpo crudo y la marca de tiempo, en tiempo constante.Si no coincide o es antigua: responde 401.
installation.organizationId
Procesa el evento
Con los datos de esa empresa y, si llamas a la API, con su clave.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/utilia/avisos', express.raw({ type: 'application/json' }), async (req, res) => {
const instalacionId = req.header('X-Utilia-Installation-Id');
const firma = req.header('X-Utilia-Signature') ?? '';
const marca = req.header('X-Utilia-Timestamp') ?? '';
const instalacion = instalacionId ? await buscarInstalacion(instalacionId) : null; // tu base de datos
if (!instalacion) return res.status(401).send('Instalación desconocida');
if (!marca || Math.abs(Date.now() - Number(marca)) > 5 * 60 * 1000) {
return res.status(401).send('Aviso fuera de la ventana de cinco minutos');
}
const esperada =
'sha256=' +
crypto
.createHmac('sha256', instalacion.webhookSigningSecret)
.update(`${marca}.${req.body.toString('utf8')}`)
.digest('hex');
const a = Buffer.from(firma);
const b = Buffer.from(esperada);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).send('Firma no válida');
const aviso = JSON.parse(req.body.toString('utf8'));
if (aviso.installation?.id !== instalacionId) return res.status(401).send('Instalación cambiada');
await procesar(instalacion, aviso); // descarta duplicados por X-Utilia-Delivery-Id
return res.status(200).send('ok');
});La cabecera X-Utilia-Installation-Id no va firmada: sirve para elegir la clave, pero, después de comprobar la firma, confirma que installation.id del cuerpo es la misma instalación. Si no coincide, descarta el aviso.
Con los SDK 4.7.0 (sin publicar todavía) lo hace sdk.webhooks.verifyInstallation: tú le das una función que devuelve la clave de firma de una instalación y él elige la clave, comprueba la firma y la ventana de tiempo y confirma que el sobre es de esa instalación.
const aviso = await sdk.webhooks.verifyInstallation(cuerpoCrudo, req.headers, async (id) =>
(await buscarInstalacion(id))?.webhookSigningSecret ?? null,
);
if (aviso.event === 'APP_UNINSTALLED') await borrarInstalacion(aviso.installation.id);Si falta la cabecera, si no tienes la clave de esa instalación o si el sobre es de otra, lanza UtiliaSDKError con WEBHOOK_INSTALLATION_MISSING, WEBHOOK_INSTALLATION_UNKNOWN o WEBHOOK_INSTALLATION_MISMATCH: en los tres casos, descarta el aviso. Todo lo demás de la verificación (la ventana de cinco minutos, los reintentos y el identificador de entrega) está en Firma HMAC de los avisos.
Los cinco avisos del ciclo de vida
Llegan solos a la dirección de avisos de tu ficha, firmados con la clave de la instalación y con installation en el sobre. No se marcan ni se pueden quitar.
| Aviso | Cuándo llega | Qué trae data |
|---|---|---|
APP_INSTALLED | Tu servidor acaba de canjear el primer código de la instalación. Al volver a conectar no se repite. | organizationName, scopes y webhookEvents |
APP_ACCESS_CHANGED | La empresa acepta los permisos nuevos de una versión, o se aprueba una versión que pide menos y se quitan solos. | scopes (los vigentes), addedScopes, removedScopes, webhookEvents y revisionId. Cuando se quitan solos, addedScopes va vacío |
APP_SUSPENDED | UTILIA suspende tu aplicación. Cuando llega, la suspensión ya se ha aplicado: la clave de la instalación responde 403 APP_DISABLED. | reason, el motivo público |
APP_REINSTATED | UTILIA levanta la suspensión y la instalación vuelve a funcionar con los permisos que tenía. | Nada |
APP_UNINSTALLED | La empresa la desinstala. Sale antes de cortar el acceso, así que te llega aunque la clave ya no responda. | reason, el motivo que dio la empresa o null |
APP_UNINSTALLED y APP_SUSPENDED llegan aunque la empresa haya perdido el aplicativo Integraciones: anuncian un corte y no llevan datos de la empresa. Los demás, solo si lo tiene.
{
"event": "APP_UNINSTALLED",
"timestamp": "2026-12-01T16:40:05.000Z",
"app": { "id": "cm3k9x2a50002qz8hm4n5p6q7", "slug": "acme-recordatorios-k7f2" },
"installation": {
"id": "cm3k9x2a40001qz8h7d1v2b3c",
"organizationId": "cm1a2b3c40000ab12cd34ef56",
"organizationName": "Ferretería López"
},
"data": { "reason": "Ya no enviamos recordatorios por correo." },
"meta": { "schemaVersion": "2026-05" }
}Para saber los permisos vigentes después de un APP_ACCESS_CHANGED, lo más seguro es pedir la instalación con GET /external/v1/app-installations/:id y leer grantedScopes. Qué hacer con los datos de la empresa al recibir APP_UNINSTALLED está en Desinstalación, suspensión y datos.
Los avisos de la suscripción
Si tu aplicación es «De pago en UTILIA», cada cambio de la suscripción de una empresa te llega como un aviso más de su instalación: firmado con su clave, con X-Utilia-Installation-Id y con installation en el sobre. No se marcan ni se pueden quitar. Te sirven para dar o quitar acceso dentro de tu aplicación sin preguntar cada vez.
| Aviso | Cuándo llega |
|---|---|
APP_SUBSCRIPTION_STARTED | La empresa contrata tu aplicación: empieza la prueba gratis o se cobra el primer periodo. Llega justo después de APP_INSTALLED, en cuanto tu servidor canjea el código. |
APP_SUBSCRIPTION_TRIAL_ENDING | Faltan tres días para el fin de la prueba gratis, por si quieres avisar dentro de tu aplicación. |
APP_SUBSCRIPTION_RENEWED | Se cobra un periodo nuevo: una renovación o el primer cobro al terminar la prueba. |
APP_SUBSCRIPTION_CHANGED | La empresa cambia de plan o de periodicidad, en el acto o para la próxima renovación (subscription.pendingChange). |
APP_SUBSCRIPTION_PRICE_CHANGE_SCHEDULED | La subida de precio que aprobaste se le aplicará a esta empresa en su renovación de effectiveAt, con al menos 30 días de aviso. |
APP_SUBSCRIPTION_PAST_DUE | No se ha podido cobrar un periodo. La aplicación sigue funcionando mientras dura el plazo para pagar. |
APP_SUBSCRIPTION_PAUSED | La suscripción queda en pausa: tu aplicación se detiene en esa empresa y no se le cobra. El motivo, en reason. |
APP_SUBSCRIPTION_RESUMED | Vuelve a estar al corriente tras un pago pendiente o una pausa. |
APP_SUBSCRIPTION_CANCELED | La empresa la cancela: funciona hasta endsAt y no se renueva. |
APP_SUBSCRIPTION_REACTIVATED | La empresa deshace la cancelación antes de que surta efecto. |
APP_SUBSCRIPTION_REFUNDED | UTILIA devuelve a la empresa todo o parte de un cobro. Lo que se descuenta de tu parte lo ves en Ingresos. |
APP_SUBSCRIPTION_ENDED | La suscripción termina. El motivo, en reason. Sale antes de APP_UNINSTALLED. |
Todos traen en data el mismo bloque subscription que devuelve GET /external/v1/app-installations/:id, y cada uno solo lo que le toca:
- occurredAtstringobligatorio
- Cuándo pasó, en ISO 8601.
- subscriptionobjectobligatorio
- La suscripción después del cambio: plan, precio sin impuestos, estado, prueba, periodo, cancelación y cambio programado.
- simulatedbooleanobligatorio
- true en una compra simulada de una de tus empresas de prueba: recorre los mismos avisos sin cobrar nada.
- previousobject
- Estado, plan y periodicidad de antes (status, planKey, interval), solo cuando cambia alguno.
- reasonstring
- En APP_SUBSCRIPTION_PAUSED, el motivo de la pausa (PAYMENT_FAILED, PLAN_REQUIREMENT_MISSING, LISTING_SUSPENDED, ORGANIZATION_SUSPENDED o DISPUTE_OPEN, si la empresa ha abierto una disputa con su banco por un cargo de esta suscripción; esa pausa no tiene fecha de fin y dura lo que dure la disputa). En APP_SUBSCRIPTION_ENDED, el del fin (CANCELED, TRIAL_CANCELED, UNINSTALLED, UNPAID, PAUSE_EXPIRED, LISTING_DISCONTINUED, LISTING_REMOVED, DEVELOPER_CLOSED, ORGANIZATION_CANCELED o REFUNDED, por un reembolso o una disputa perdida).
- endsAtstring
- En APP_SUBSCRIPTION_CANCELED: hasta cuándo funciona.
- effectiveAtstring
- En APP_SUBSCRIPTION_PRICE_CHANGE_SCHEDULED: cuándo se aplica el precio nuevo.
- refundobject
- En APP_SUBSCRIPTION_REFUNDED: amountCents, lo devuelto a la empresa con impuestos y sin desglose, y full, si ese cobro queda devuelto entero.
{
"event": "APP_SUBSCRIPTION_PAUSED",
"timestamp": "2026-12-12T08:00:03.000Z",
"app": { "id": "cm3k9x2a50002qz8hm4n5p6q7", "slug": "acme-recordatorios-k7f2" },
"installation": {
"id": "cm3k9x2a40001qz8h7d1v2b3c",
"organizationId": "cm1a2b3c40000ab12cd34ef56",
"organizationName": "Ferretería López"
},
"data": {
"occurredAt": "2026-12-12T08:00:00.000Z",
"subscription": {
"id": "cm4a1b2c30000xy98zt76uv54",
"status": "PAUSED",
"accessAllowed": false,
"plan": { "key": "pro", "name": "Pro" },
"interval": "MONTHLY",
"priceCents": 1900,
"currency": "EUR",
"trialEndsAt": null,
"currentPeriodStart": "2026-11-05T10:12:00.000Z",
"currentPeriodEnd": "2026-12-05T10:12:00.000Z",
"cancelAtPeriodEnd": false,
"pendingChange": null,
"pauseReason": "PAYMENT_FAILED",
"endedAt": null,
"endedReason": null,
"simulated": false
},
"previous": { "status": "PAST_DUE", "planKey": "pro", "interval": "MONTHLY" },
"reason": "PAYMENT_FAILED",
"simulated": false
},
"meta": { "schemaVersion": "2026-05" }
}Nunca llevan quién compró o canceló, el motivo que dio la empresa al darse de baja, códigos del banco, impuestos, facturas, la tarjeta ni datos fiscales de la empresa. Una compra que no llega a cobrarse no te avisa.
APP_SUBSCRIPTION_ENDED y APP_SUBSCRIPTION_PAUSED llegan aunque la empresa haya perdido el aplicativo Integraciones, como los otros avisos de corte. Mientras UTILIA tenga suspendida tu aplicación en una empresa, los de su suscripción siguen llegando: son los únicos.
Con los SDK 4.8.0 (sin publicar todavía), la carga se tipa con AppSubscriptionWebhookData y isAccessAllowed(instalacion) (is_access_allowed en Python) dice si la empresa puede usar tu aplicación.
Si algo falla
- No llega ningún aviso. La ficha no tiene dirección de avisos, o la instalación aún no se ha conectado. Comprueba que el canje respondió bien.
- Llegan los del ciclo de vida, pero no los de datos. El evento no está en Avisos que recibirá de la ficha publicada, la empresa no concedió el permiso que lo cubre o su plan no incluye ese aplicativo. Mira
webhookEventsde la instalación. - No llega
APP_SUBSCRIPTION_STARTEDal comprar. Llega cuando tu servidor canjea el código de la instalación, no antes: hasta entonces no tienes su clave de firma. Si el canje falló, pide la instalación y leesubscription. - La firma no coincide. Estás usando la clave de firma de otra instalación o la de tu propia aplicación. Elige la clave por
X-Utilia-Installation-Idy calcula la firma sobre el cuerpo crudo.
Ponlo en práctica
Lo encontrarás en Creadas por tu empresa. Para abrirlo, entra con tu cuenta de UTILIA OS.