#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

POST/webhooks

Registra 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

CampoTipoDescripción
namestringRequerido. Nombre para identificarlo en la app.
typestringOpcional. Único valor: custom. Puedes omitirlo.
urlstringRequerido. URL que recibirá los eventos.
event_keysstring[]Requerido. Claves del catálogo a las que te suscribes (mínimo una).
cURL
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:

GET/webhooks/event-catalog

#Eventos y cuándo se disparan

EventoSe dispara cuandoDisponible
cfdi.cancelledEl 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.
cfdi.cancelled_after_paymentSe cancela un CFDI que ya tenía pagos aplicados, o un ingreso emitido ya contado como tal. Es un subconjunto de cfdi.cancelled.
sync.completedTermina bien un job de sincronización. Grano: un evento por RFC × periodo × tipo de documento, así que un backfill amplio genera muchos.
sync.failedUn job de sincronización falla. Incluye error_class.
connection.ciec_invalidEl 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.
cfdi.issuedSe ingesta un CFDI nuevo.No
document.csf.updatedSe descarga una Constancia de Situación Fiscal nueva.No
document.opinion_32d.changedLa 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:

JSON
{
  "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

HeaderContenido
Clarisfy-EventClave del evento, p. ej. cfdi.cancelled.
Clarisfy-Delivery-IdId del evento lógico. Igual en todos los reintentos del mismo evento.
Clarisfy-AttemptNúmero de intento, empezando en 1.
Clarisfy-Signaturev1,<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.

Node
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:

IntentoEspera desde el anterior
1inmediato
21 minuto
35 minutos
430 minutos
52 horas
66 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.