Skip to content

Notificaciones

Alertas a nivel de tenant que se muestran a los usuarios del sistema. La API produce dos categorías de notificaciones:

  • Basadas en eventos — creadas automáticamente cuando ocurre algo (por ejemplo, un comprobante es autorizado por el SRI).
  • Programadas — creadas o actualizadas por el propio job en segundo plano de la API (por ejemplo, vencimiento de certificado). No requieren acción del consumidor.
GET   /v1/notifications
POST  /v1/notifications/:id/read
GET   /v1/notifications/preferences
PATCH /v1/notifications/preferences

Consulta Webhooks para registrar URLs de callback que reciban notificaciones casi en tiempo real. Consultar este endpoint con ?sinceId= es el mecanismo de respaldo para consumidores que no pueden exponer una URL de callback HTTPS pública.

Autenticación

Authorization: Bearer <api-key> — cualquier llave activa del tenant. No se requiere X-Issuer-Id por defecto; proporciónalo para acotar los resultados a un emisor específico (ver Filtro por emisor).


Objeto de notificación

Todas las respuestas de listado y de notificación individual usan la misma estructura:

json
{
  "id": "00000000-0000-0000-0000-000000000042",
  "type":      "DOCUMENT_AUTHORIZED",
  "severity":  "INFO",
  "title":     "Factura autorizada",
  "message":   "La factura 001-001-000000012 de ACME Corp fue autorizada por el SRI.",
  "metadata":  { "accessKey": "...", "sequential": "001-001-000000012", "total": "118.00" },
  "issuerId": "00000000-0000-0000-0000-000000000003",
  "readAt":    null,
  "expiresAt": null,
  "createdAt": "2026-05-28T14:30:00.000Z"
}
CampoTipoDescripción
idstring (UUID)Identificador estable. Úsalo para eliminar duplicados entre consultas y rastrear el estado de lectura por usuario.
typestringCódigo de tipo legible por máquina — ver Tipos de notificación.
severitystringINFO · WARNING · ERROR
titlestringEncabezado breve y legible para humanos.
messagestringDescripción completa y legible para humanos.
metadataobject|nullDatos estructurados específicos del tipo (ver Tipos de notificación).
issuerIdstring (UUID)|nullEl emisor al que concierne esta notificación, o null para alertas a nivel de tenant.
readAtstring|nullMarca de tiempo ISO de cuando la notificación fue marcada como leída, o null si sigue sin leer.
expiresAtstring|nullMarca de tiempo ISO después de la cual la notificación debería ocultarse, o null si nunca expira.
createdAtstringMarca de tiempo ISO de creación.

Tipos de notificación

DOCUMENT_AUTHORIZED

Creada automáticamente (fire-and-forget) dentro de GET /:accessKey/authorize cuando el SRI confirma la autorización. Varias autorizaciones dentro de una ventana de 60 segundos se agregan en una sola fila para evitar saturar la lista durante el procesamiento por lotes. La misma notificación id puede tener un count actualizado en consultas sucesivas dentro de esa ventana — el frontend debería actualizar (upsert) por id en lugar de agregar (append).

Se dispara un payload de webhook por cada actualización de la fila agregada (incluyendo los incrementos de count).

Severidad: INFO

Metadata:

json
{
  "documents": [
    {
      "accessKey": "...",
      "sequential": "001-001-000000012",
      "buyerName": "ACME Corp",
      "buyerId": "0901234567001",
      "total": "118.00",
      "issueDate": "2026-05-28",
      "authorizationNumber": "2605202615..."
    }
  ],
  "count": 5
}

documents se limita a 50 entradas cuando un lote es grande; count siempre refleja el total real.


CERT_EXPIRING

Creada o actualizada por el job programado de la API cuando el certificado de un emisor está a menos de 30 días de su fecha notAfter. Como máximo una fila no leída por emisor — la misma fila se actualiza en su lugar en ejecuciones sucesivas del job (los días restantes se actualizan, la severidad puede escalar). Se descarta automáticamente cuando el certificado se renueva y le quedan más de 30 días.

Severidad: WARNING (> 7 días) · ERROR (≤ 7 días)

Metadata:

json
{
  "issuerId": "00000000-0000-0000-0000-000000000003",
  "certExpiry": "2026-06-15T00:00:00.000Z",
  "daysRemaining": 18,
  "branchCode": "001",
  "issuePointCode": "001"
}

CERT_EXPIRED

Mismas condiciones que CERT_EXPIRING pero para un certificado cuya fecha notAfter ya pasó.

Severidad: ERROR
Metadata: misma estructura que CERT_EXPIRING, con daysRemaining: 0.


PAYMENT_VERIFIED

Creada automáticamente (fire-and-forget) cuando tu proveedor verifica un comprobante de pago que subiste — cubre de forma uniforme una suscripción inicial, una mejora de plan y una renovación; solo cambia el texto. Se envía un correo equivalente al mismo tiempo.

Severidad: INFO

Metadata:

json
{
  "paymentId": "00000000-0000-0000-0000-000000000018",
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "tier": "STARTER",
  "billingInterval": "MONTHLY",
  "purpose": "INITIAL",
  "amount": "20.00",
  "rejectionReasonCode": null
}

purpose es INITIAL, TIER_CHANGE, o RENEWAL. Para un pago TIER_CHANGE, tier/billingInterval son el plan objetivo que se está comprando, no el actual de la suscripción — por ejemplo, en un pago por un cambio de STARTER mensual a GROWTH anual, esto muestra "tier": "GROWTH", "billingInterval": "YEARLY". amount es el total completo con IVA incluido (lo que realmente se transfiere vía SPI), no la base imponible antes de IVA.


PAYMENT_REJECTED

Mismo disparador que PAYMENT_VERIFIED, pero para una decisión de rechazo.

Severidad: WARNING

Metadata: misma estructura que PAYMENT_VERIFIED, con rejectionReasonCode poblado (uno de AMOUNT_MISMATCH, TRANSFER_NOT_FOUND, WRONG_ACCOUNT, ILLEGIBLE_PROOF, DUPLICATE_SUBMISSION, OTHER) — reenvía el comprobante para el mismo paymentId vía Submit Payment Proof.


SUBSCRIPTION_RENEWAL_DUE

Creada automáticamente por el job programado del proveedor unos 7 días antes del current_period_end de tu suscripción. Ya hay un nuevo pago RENEWAL abierto en el momento en que se dispara esto — envía el comprobante vía Submit Payment Proof usando el paymentId de la metadata. Un correo equivalente incluye las instrucciones de transferencia bancaria.

Severidad: WARNING

Metadata:

json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "paymentId": "00000000-0000-0000-0000-000000000025",
  "tier": "STARTER",
  "amount": "17.39",
  "ivaAmount": "2.61",
  "totalAmount": "20.00",
  "currentPeriodEnd": "2026-07-15T00:00:00.000Z"
}

SUBSCRIPTION_PAST_DUE_WARNING

Creada automáticamente por el mismo job programado, más adelante en el período de gracia que SUBSCRIPTION_RENEWAL_DUE (por defecto, 5 días después de current_period_end, con 7 días de gracia en total) — un aviso más urgente y distinto del recordatorio de renovación, antes de que la cuenta pase efectivamente a estado PAST_DUE. Envía el comprobante del pago RENEWAL ya abierto vía Submit Payment Proof, o inicia una nueva suscripción vía Create Subscription — ambas rutas quedan disponibles incluso después de pasar a PAST_DUE (ver más abajo).

Severidad: WARNING

Metadata:

json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "tier": "STARTER",
  "currentPeriodEnd": "2026-07-15T00:00:00.000Z",
  "suspendsAt": "2026-07-22T00:00:00.000Z"
}

SUBSCRIPTION_EXPIRED

Creada automáticamente por el mismo job programado cuando una suscripción pasa unos 7 días de current_period_end sin que se verifique ninguna renovación. Para cuando esto se dispara, el tenant ya fue movido al plan FREE y su cuenta (GET /v1/tenants/me's status) pasa a PAST_DUE — distinto de SUSPENDED: es una cuenta que puede recuperarse por sí misma iniciando una nueva suscripción vía Create Subscription y pagándola; no requiere contactar soporte. Un correo equivalente explica lo ocurrido.

Severidad: ERROR

Metadata:

json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000012",
  "previousTier": "STARTER"
}

PRICE_CHANGE_ANNOUNCED

Creada automáticamente cuando el operador publica un cambio de precio para un plan pagado, para cada tenant activo que aún esté dentro de la ventana de aviso de 30 días. Este tipo es obligatorio — no puede desuscribirse en ningún canal (ver Obtener preferencias) porque constituye el aviso legal de cambio de precio de 30 días, no una alerta operativa opcional. Cualquier renovación que venza antes de effectiveAt sigue facturándose al precio actual.

Severidad: INFO

Metadata:

json
{
  "tierPriceId": "00000000-0000-0000-0000-000000000030",
  "tier": "STARTER",
  "billingInterval": "MONTHLY",
  "previousPriceUsd": 20,
  "newPriceUsd": 25,
  "effectiveAt": "2026-08-25T00:00:00.000Z"
}

Tipos reservados

Los siguientes tipos están definidos en el esquema y son aceptados por el endpoint de preferencias, pero aún no son producidos por la API. Están reservados para implementación futura:

TipoDescripción
SRI_SUBMISSION_FAILEDEl SRI rechazó permanentemente el envío de un comprobante
EMAIL_DELIVERY_FAILEDMailgun reportó un fallo de entrega permanente
QUOTA_WARNINGEl tenant se está acercando a su cuota de comprobantes

Listar notificaciones

GET /v1/notifications

Devuelve las notificaciones activas (no expiradas) del tenant, de la más reciente a la más antigua. Se incluyen tanto las leídas como las no leídas. Usa readAt para decidir qué mostrar como nuevo.

Parámetros de consulta

ParámetroTipoDescripción
sinceIdstring (UUID)Opcional. Cuando se proporciona, devuelve solo las notificaciones con id > sinceId. Úsalo para consultas de actualización incremental eficientes: guarda el id más alto visto en cada consulta y pásalo en la siguiente solicitud.

Filtro por emisor

Proporciona X-Issuer-Id: <id> para restringir los resultados a un emisor específico. Cuando el encabezado está presente, la respuesta incluye:

  • Notificaciones cuyo issuerId coincide con el valor proporcionado.
  • Notificaciones a nivel de tenant (issuerId: null), como futuras alertas de cuota.

Omite el encabezado para recibir todas las notificaciones de todos los emisores (útil para páginas de administración o resumen).

Respuesta

200 OK

json
{
  "notifications": [ ... ],
  "unreadCount": 3
}

Errores

Estado HTTPCódigoCuándo ocurre
400ISSUER_ID_INVALIDEl encabezado X-Issuer-Id está presente pero no es un entero positivo válido
400ISSUER_ID_INVALIDsinceId está presente pero no es un entero positivo válido
401UNAUTHORIZEDAPI key faltante o inválida

Marcar como leída

POST /v1/notifications/:id/read

Marca una sola notificación como leída (readAt se establece al momento actual). La notificación queda excluida de unreadCount en todas las consultas posteriores.

Cuándo llamarlo: el frontend gestiona el estado de lectura por usuario en su propia base de datos. Llama a este endpoint solo cuando todos los usuarios con acceso a la notificación la hayan marcado como leída de su lado. Después de esta llamada, la notificación se considera leída globalmente y ya no aparecerá en unreadCount.

Parámetros de ruta

ParámetroDescripción
idUUID de la notificación

Respuesta

200 OK

json
{
  "notification": { ... }
}

Errores

Estado HTTPCódigoCuándo ocurre
400VALIDATION_FAILEDid no es un entero positivo
401UNAUTHORIZEDAPI key faltante o inválida
404NOT_FOUNDLa notificación no existe, pertenece a otro tenant, o ya está leída

Obtener preferencias

GET /v1/notifications/preferences

Devuelve la preferencia de notificación para cada combinación (tipo, canal) suscribible. Los canales son IN_APP (controla si la notificación aparece en GET /v1/notifications) y EMAIL (controla si se envía el correo equivalente, para los tipos que tienen uno). Las combinaciones que el tenant nunca ha configurado explícitamente tienen por defecto enabled: true (modelo de exclusión voluntaria).

Los tipos obligatorios (actualmente solo PRICE_CHANGE_ANNOUNCED, el aviso legal de 30 días — ver su sección arriba) nunca aparecen en esta lista: no pueden desuscribirse en ningún canal. Un tipo sin correo equivalente (DOCUMENT_AUTHORIZED, CERT_EXPIRING, CERT_EXPIRED) solo aparece con el canal IN_APP.

Respuesta

200 OK

json
{
  "preferences": [
    { "type": "DOCUMENT_AUTHORIZED",           "channel": "IN_APP", "enabled": true },
    { "type": "CERT_EXPIRING",                 "channel": "IN_APP", "enabled": true },
    { "type": "CERT_EXPIRED",                  "channel": "IN_APP", "enabled": true },
    { "type": "PAYMENT_VERIFIED",              "channel": "IN_APP", "enabled": true },
    { "type": "PAYMENT_VERIFIED",              "channel": "EMAIL",  "enabled": true },
    { "type": "PAYMENT_REJECTED",              "channel": "IN_APP", "enabled": true },
    { "type": "PAYMENT_REJECTED",              "channel": "EMAIL",  "enabled": true },
    { "type": "SUBSCRIPTION_RENEWAL_DUE",      "channel": "IN_APP", "enabled": true },
    { "type": "SUBSCRIPTION_RENEWAL_DUE",      "channel": "EMAIL",  "enabled": true },
    { "type": "SUBSCRIPTION_PAST_DUE_WARNING", "channel": "IN_APP", "enabled": true },
    { "type": "SUBSCRIPTION_PAST_DUE_WARNING", "channel": "EMAIL",  "enabled": true },
    { "type": "SUBSCRIPTION_EXPIRED",          "channel": "IN_APP", "enabled": true },
    { "type": "SUBSCRIPTION_EXPIRED",          "channel": "EMAIL",  "enabled": true }
  ]
}

Actualizar preferencias

PATCH /v1/notifications/preferences

Actualiza en lote (upsert) una o más preferencias por (tipo, canal). Envía solo las combinaciones que quieres cambiar; las no mencionadas permanecen sin cambios.

Cuerpo de la solicitud

Un arreglo de objetos de preferencia:

json
[
  { "type": "PAYMENT_VERIFIED", "channel": "EMAIL", "enabled": false }
]
CampoTipoRequeridoDescripción
typestringUno de los tipos de notificación suscribibles (no obligatorios)
channelstringIN_APP o EMAIL — debe ser un canal que el tipo soporte
enabledbooleantrue para habilitar, false para suprimir

Cuando enabled es false para (type, IN_APP), la notificación deja de aparecer en GET /v1/notifications para ese tipo — la fila sigue creándose internamente (por ejemplo, para el historial de webhooks), simplemente se filtra de la lista. Cuando enabled es false para (type, EMAIL), no se envía el correo equivalente para ese tipo, pero la notificación en la app no se ve afectada.

Respuesta

200 OK — misma estructura que GET /v1/notifications/preferences, reflejando el estado completo actualizado.

Errores

Estado HTTPCódigoCuándo ocurre
400VALIDATION_FAILEDEl cuerpo no es un arreglo, o una entrada tiene un type/channel inválido, un channel que ese type no soporta, un type obligatorio (no puede desuscribirse), o un enabled que no es booleano
401UNAUTHORIZEDAPI key faltante o inválida

Patrón de integración recomendado

┌─────────────────────────────────────────────────────────────────────┐
│  Backend del consumidor (por ejemplo, Next.js)                      │
│                                                                     │
│  Principal (casi en tiempo real):                                   │
│    Registra un endpoint de webhook → recibe eventos vía POST        │
│    Verifica X-Comprobify-Signature en cada solicitud entrante       │
│                                                                     │
│  Respaldo / actualización incremental:                              │
│    Consulta GET /v1/notifications?sinceId=<lastSeenId> cada 60–300s │
│    Guarda el id más alto visto → pásalo como sinceId en la próxima  │
│    consulta                                                         │
│                                                                     │
│  Cuando el usuario abre el panel de notificaciones:                 │
│    Marca como leída en la BD del frontend por usuario               │
│    Cuando todos los usuarios la han leído → POST                    │
│    /v1/notifications/:id/read                                       │
└─────────────────────────────────────────────────────────────────────┘

Elegir un intervalo de consulta

El tipo de notificación DOCUMENT_AUTHORIZED agrega varias autorizaciones que ocurren dentro de una ventana de 60 segundos en una sola fila. Consultar con más frecuencia que cada 60 segundos capturaría la fila a mitad de la agregación — de todos modos se actualizará de nuevo dentro de la misma ventana, así que no hay beneficio en consultar más rápido que eso.

EscenarioIntervalo recomendado
Webhooks configurados (la consulta es solo respaldo)300 s (5 min) — cualquier evento perdido se recupera en el siguiente ciclo
Sin webhooks, la consulta es el único mecanismo de entrega60 s — coincide con la ventana de agregación; ir más abajo no da ningún beneficio

No consultes por debajo de 60 segundos — no hará que los datos nuevos aparezcan antes y añade carga innecesaria.

Documentación de la API de Comprobify