#Webhooks y eventos
Los webhooks te avisan cuando cambia algo en los datos fiscales de una FiscalEntity — el caso central es un CFDI que el SAT marca como cancelado. En lugar de hacer polling, Clarisfy hace un POST a tu endpoint.
#Registra un endpoint
/webhooksRegistra una URL y los eventos que quieres recibir. La respuesta incluye un signing_secret (whsec_…) que solo se devuelve una vez: el server guarda únicamente su sha256, así que si lo pierdes hay que crear otro endpoint.
#Cuerpo de la petición
| Campo | Tipo | Descripción |
|---|---|---|
name | string | Requerido. Nombre para identificarlo en la app. |
type | string | Opcional. Único valor: custom. Puedes omitirlo. |
url | string | Requerido. URL que recibirá los eventos. |
event_keys | string[] | Requerido. Claves del catálogo a las que te suscribes (mínimo una). |
curl https://api.clarisfy.com/v1/webhooks \
-H "Authorization: Bearer clf_live_secret_9K3mZ1pQ7rTx8vB4nH6dLwYe" \
-H "Content-Type: application/json" \
-d '{
"name": "Cobranza",
"url": "https://tu-app.com/webhooks/clarisfy",
"event_keys": ["cfdi.cancelled"]
}'El catálogo vigente siempre se puede consultar:
/webhooks/event-catalog#Eventos y cuándo se disparan
| Evento | Se dispara cuando | Disponible |
|---|---|---|
cfdi.cancelled | El SAT reporta que un CFDI que teníamos como vigente pasó a cancelado. Uno por CFDI: re-reportar la misma cancelación no vuelve a disparar. | Sí |
cfdi.cancelled_after_payment | Se cancela un CFDI que ya tenía pagos aplicados, o un ingreso emitido ya contado como tal. Es un subconjunto de cfdi.cancelled. | Sí |
sync.completed | Termina bien un job de sincronización. Grano: un evento por RFC × periodo × tipo de documento, así que un backfill amplio genera muchos. | Sí |
sync.failed | Un job de sincronización falla. Incluye error_class. | Sí |
connection.ciec_invalid | El SAT rechaza la CIEC guardada al abrir sesión: caducó o la cambiaron. Las conexiones a ese RFC pasan a invalid_ciec y la descarga se detiene hasta que se actualice. Una alerta por invalidación, no una por intento fallido. | Sí |
cfdi.issued | Se ingesta un CFDI nuevo. | No |
document.csf.updated | Se descarga una Constancia de Situación Fiscal nueva. | No |
document.opinion_32d.changed | La Opinión de Cumplimiento cambia de estado. | No |
#Estructura del payload
El cuerpo es plano: los campos del evento van en la raíz, sin envelope. El tipo de evento viaja tanto en el header Clarisfy-Event como en el campo event.
cfdi.cancelled:
{
"event": "cfdi.cancelled",
"fiscal_entity_id": "9a1c2b3d-4e5f-6071-8293-a4b5c6d7e8f9",
"rfc": "VECJ880326XYZ",
"cfdi_uuid": "5f2c1a9b-8d3e-4a70-b1c2-9e8f7a6b5c4d",
"cancelled_at": "2026-07-25T18:39:12+00:00",
"cancellation_reason": "cancelacion_sat",
"severity": "critical",
"direction": "emitida",
"cfdi_type": "I",
"payment_status": "pagada",
"total": "1160.00000000",
"currency": "MXN",
"emisor_rfc": "VECJ880326XYZ",
"receptor_rfc": "ACME920101AA1",
"issued_at": "2026-04-15T10:30:00+00:00"
}severity es la lectura de riesgo de flujo ya calculada: critical cuando el CFDI cancelado traía pagos aplicados o era un ingreso emitido ya contado como ingreso —el caso "cancelado después de pago" que rompe la cobranza— y warning en los demás. Puedes rutear con ese campo sin re-derivarlo.
#Headers de cada entrega
| Header | Contenido |
|---|---|
Clarisfy-Event | Clave del evento, p. ej. cfdi.cancelled. |
Clarisfy-Delivery-Id | Id del evento lógico. Igual en todos los reintentos del mismo evento. |
Clarisfy-Attempt | Número de intento, empezando en 1. |
Clarisfy-Signature | v1,<hmac-sha256-hex> del cuerpo, con tu signing_secret. |
#Verifica la firma
Clarisfy-Signature es v1, seguido del HMAC-SHA256 del cuerpo crudo. Calcúlalo sobre los bytes exactos que recibiste — si serializas de nuevo el JSON antes de firmar, el resultado no coincide.
import crypto from "node:crypto";
/** Verifica la firma HMAC de un webhook de Clarisfy. */
function isValidSignature(rawBody, header, secret) {
const [version, sig] = header.split(",");
if (version !== "v1") return false;
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(sig);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}#Reintentos e idempotencia
Responde 2xx en cuanto recibas el evento; procesa después. Si tu endpoint no contesta a tiempo (5 s) o devuelve 5xx, Clarisfy reintenta:
| Intento | Espera desde el anterior |
|---|---|
| 1 | inmediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 6 horas |
Seis intentos en total, repartidos en ~8 horas. Después de eso la entrega se da por perdida y queda en el log (GET /webhooks/{id}/deliveries).
Un 4xx no se reintenta: significa que leíste el payload y lo rechazaste, y los mismos bytes no van a cambiar de resultado. Las dos excepciones son 408 y 429, que se tratan como "más despacio", no como rechazo.