Eventos de webhook

Referencia completa de cada tipo de evento de webhook de Certivant: esquema del sobre, formas del payload por evento, patrón de idempotencia y reglas de orden.

Todos los payloads de webhook comparten el mismo sobre externo. Solo cambia el bloque data según el tipo de evento.

{
  "event": "kyc.request.approved",
  "requestId": "req_01J...",
  "organizationId": "org_01H...",
  "externalRef": "user-9281",
  "subTenantName": "Acme Corp",
  "subTenantExternalId": "cust_8842",
  "status": "APPROVED",
  "timestamp": "2026-05-27T10:04:21.000Z",
  "data": { /* event-specific */ }
}
CampoTipoNotas
eventstringEl tipo de evento. Siempre empieza por kyc.
requestIdstringEl ID de solicitud de Certivant
organizationIdstringLa organización propietaria de la solicitud
externalRefstring | nullLa clave externa que estableció al crear la solicitud
subTenantNamestring | nullModo socio: el nombre visible del cliente final establecido en la solicitud, si lo hay
subTenantExternalIdstring | nullModo socio: el id de su cliente final establecido en la solicitud; permite enrutar el evento al cliente correcto sin hacer búsquedas
statusenumEstado actual de la solicitud en el momento del evento
timestampstring (ISO-8601)Momento en que se puso en cola el evento
dataobject | nullPayload específico del evento (consulte más abajo)

Catálogo de eventos

kyc.webhook.test

Una comprobación de conectividad: se envía al pulsar Probar en una URL de webhook (a nivel de inquilino, en Configuración de KYC → Desarrollador, o por formulario en el editor de formularios). No es un evento real de verificación: no lleva requestId y no cambia nada en su lado. Su único propósito es confirmar que su endpoint es accesible.

Incluye un indicador explícito test: true de nivel superior, para que su manejador pueda responder 2xx de inmediato, sin buscar ninguna solicitud, sin comprobar el status ni siquiera sin verificar la firma (no hay nada que proteger, ya que un ping no modifica ningún estado).

{
  "event": "kyc.webhook.test",
  "test": true,
  "organizationId": "org_...",
  "formId": "form_...",        // present only for the per-form test
  "formName": "KYC — Identity Verification",
  "timestamp": "...",
  "data": { "message": "This is a test webhook from your KYC platform." }
}

Manejador recomendado:

const payload = JSON.parse(rawBody);
// Reachability ping — ack immediately, no auth needed.
if (payload.test === true || payload.event === "kyc.webhook.test") {
  return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
// …verify the signature and process real events below…

Los eventos de resultado reales (con un status del enum del ciclo de vida) deben verificarse igualmente con la firma antes de actuar sobre ellos; consulte Verificación de firma. El evento de prueba también va firmado (cuando hay un secreto configurado), con la misma cabecera X-Certivant-Signature-V2 que los eventos reales y una marca de tiempo en segundos Unix; por tanto, puede verificarlo, pero no está obligado a hacerlo.

El botón Enviar un evento de prueba del panel también puede enviar un payload de ejemplo kyc.request.created, kyc.request.approved, kyc.request.rejected o kyc.request.escalated. Los ejemplos tienen la forma real del sobre, llevan "test": true, usan un requestId falso (req_sample_…) y data.sample: true, y nunca se almacenan como eventos reales. No actúe sobre ellos en las rutas de código de producción: compruebe primero payload.test === true.

kyc.request.created

Se dispara inmediatamente después de que POST /api/v1/kyc/requests devuelva 201. Es útil si quiere confirmar la recepción sin esperar a que el solicitante envíe la información.

{
  "event": "kyc.request.created",
  "requestId": "req_...",
  "organizationId": "org_...",
  "externalRef": "user-9281",
  "status": "PENDING_UPLOAD",
  "timestamp": "...",
  "data": null
}

kyc.request.processing

Se dispara en el momento en que la solicitud pasa a PROCESSING, es decir, cuando el solicitante (mediante una sesión alojada o un formulario) o usted (mediante POST /requests/:id/submit) la envía. Es la señal de «enviada»: llega antes de cualquier decisión, así que su aplicación puede pasar al usuario a un estado de «en revisión» en cuanto termine de subir los documentos.

{
  "event": "kyc.request.processing",
  "requestId": "req_...",
  "externalRef": "user-9281",
  "status": "PROCESSING",
  "timestamp": "...",
  "data": null
}

Actúe sobre él cuando quiera mostrar progreso («Su verificación está en revisión») entre el envío y el evento final approved / rejected / escalated. Incluye externalRef y los campos de subtenant, así que puede enrutarlo al usuario correcto sin hacer búsquedas.


kyc.document.uploaded

Se dispara cada vez que se adjunta un documento a una solicitud. Es útil si su aplicación muestra en tiempo real un indicador de «documentos recibidos hasta ahora».

{
  "event": "kyc.document.uploaded",
  "requestId": "req_...",
  "status": "PENDING_UPLOAD",
  "data": {
    "documentId": "doc_...",
    "documentType": "GOVERNMENT_ID",
    "side": "front",
    "mimeType": "image/jpeg",
    "fileSize": 412938
  }
}

kyc.check.completed

Se dispara cuando termina cada comprobación de verificación. Una sola solicitud puede generar varios de estos: uno para la autenticidad del documento, otro para la comparación facial, otro para la prueba de vida, etc.

{
  "event": "kyc.check.completed",
  "requestId": "req_...",
  "status": "PROCESSING",
  "data": {
    "checkType": "face_match",
    "result": "pass",
    "score": 0.91,
    "details": { "method": "deep_face_match" }
  }
}

Valores posibles de checkType: document_authenticity, ocr, face_match, liveness, sanctions_screening, pep_screening, adverse_media, proof_of_address, phone_verification, email_verification, registry_lookup (KYB).


kyc.request.approved

El evento principal: la solicitud está aprobada. Se dispara tanto cuando el motor automatizado la aprueba como cuando un revisor humano aprueba manualmente una solicitud escalada (o rechazada automáticamente antes) desde el panel o mediante la API /decision. En las aprobaciones manuales, data.decision es "approve" y data.reason contiene la nota del revisor; los campos completos del motor (score, factors, …) solo están presentes en las decisiones automatizadas.

{
  "event": "kyc.request.approved",
  "requestId": "req_...",
  "status": "APPROVED",
  "data": {
    "score": 94,
    "decision": "approve",
    "factors": [
      { "name": "document_authenticity", "weight": 0.3, "score": 0.96 },
      { "name": "face_match", "weight": 0.3, "score": 0.91 },
      { "name": "liveness", "weight": 0.2, "score": 0.98 },
      { "name": "sanctions_screening", "weight": 0.2, "score": 1.0 }
    ],
    "document": {
      "type": "GOVERNMENT_ID",
      "issuingCountry": "GB",
      "expiryDate": "2031-04-12",
      "isExpired": false
    },
    "extracted": {
      "fullName": "Alex Singh",
      "dateOfBirth": "1988-11-03",
      "documentNumber": "GBR-1234567",
      "address": "1 Example Street, London",
      "nationality": "GB"
    },
    "liveness": { "isLive": true, "confidence": 0.98 },
    "faceMatch": { "isMatch": true, "confidence": 0.91 },
    "sanctions": { "isMatch": false, "topScore": 0, "listsChecked": ["OFAC", "UN", "EU", "UK_HMT"] }
  }
}

El bloque data.extracted es su mina de oro: guarde esos valores en el registro del usuario para no pedirle al solicitante los mismos datos dos veces.


kyc.request.rejected

La solicitud está rechazada: o bien el motor automatizado la rechazó (consulte data.disqualifiers y data.factors para ver los motivos), o bien un revisor humano rechazó manualmente una solicitud escalada (data.decision = "reject", data.reason = la nota del revisor).

{
  "event": "kyc.request.rejected",
  "requestId": "req_...",
  "status": "REJECTED",
  "data": {
    "score": 18,
    "decision": "reject",
    "disqualifiers": ["document_expired", "liveness_failed"],
    "reviewTriggers": [],
    "reason": "Document expired (2024-01-01) and liveness confidence below threshold (0.22).",
    "factors": [
      { "name": "document_authenticity", "weight": 0.3, "score": 0.0, "verdict": "fail" },
      { "name": "liveness", "weight": 0.2, "score": 0.22, "verdict": "fail" }
    ],
    "document": {
      "type": "GOVERNMENT_ID",
      "expiryDate": "2024-01-01",
      "isExpired": true
    },
    "liveness": { "isLive": false, "confidence": 0.22 }
  }
}

Puede volver a enviar la solicitud mediante POST /requests/:id/resubmit si su política lo permite.


kyc.request.escalated

El motor no tuvo suficiente confianza: puntuaciones limítrofes, una coincidencia parcial en sanciones, un documento de baja calidad o cualquier caso que superó el umbral de escalado de su organización. Una persona de su organización debe revisarlo.

{
  "event": "kyc.request.escalated",
  "requestId": "req_...",
  "status": "ESCALATED",
  "data": {
    "score": 62,
    "decision": "escalate",
    "reviewTriggers": ["sanctions_partial_match", "low_face_match_confidence"],
    "factors": [...],
    "sanctions": {
      "isMatch": true,
      "topScore": 0.72,
      "listsChecked": ["OFAC", "UN", "EU", "UK_HMT"]
    },
    "faceMatch": { "isMatch": true, "confidence": 0.68 }
  }
}

Un revisor abre la solicitud en el panel y decide. Cuando lo hace, recibe un segundo webhook (kyc.request.approved o kyc.request.rejected) con la decisión humana registrada en el registro de auditoría.


kyc.request.cancelled

Se dispara cuando una solicitud se cancela explícitamente (actualmente solo desde el panel: no hay endpoint de API).

{
  "event": "kyc.request.cancelled",
  "requestId": "req_...",
  "status": "CANCELLED",
  "data": { "reason": "applicant_withdrew", "cancelledBy": "user_..." }
}

Eventos de verificación en vivo: conference.*

Los eventos de verificación de Live ID (sesión de vídeo en vivo dirigida por una persona) usan su propio cuerpo con forma de conferencia (conferenceId, kycRequestId, applicantId, organizationId, su externalRef, el status de la conferencia y las marcas de tiempo), en lugar del sobre kyc.request.* anterior. Se disparan tanto para las conferencias programadas desde el panel como desde la API.

EventoSe dispara cuando
conference.scheduledSe crea una verificación en vivo.
conference.rescheduledSe traslada a una nueva hora (previousScheduledFor).
conference.cancelledSe cancela, ya sea por API o porque el solicitante no dio su consentimiento (cancellation).
conference.startedEl anfitrión inicia la sesión (una vez por inicio).
conference.completedEl anfitrión registra la decisión (decision.decision, decision.outcome, notas, indicador de grabación, certificado).
{
  "event": "conference.completed",
  "conferenceId": "cmf...",
  "kycRequestId": "req_...",
  "applicantId": "cma...",
  "organizationId": "org_...",
  "externalRef": "order-10042",
  "status": "completed",
  "decision": { "decision": "verified", "outcome": "approved", "requestStatus": "APPROVED", "notes": "..." },
  "timestamp": "..."
}

Payload completo y API de programación: API de verificación en vivo.


Idempotencia

Certivant puede entregar el mismo evento más de una vez (microcortes de red, reintentos; consulte Reintentos). Haga que su manejador sea idempotente.

La cabecera X-Certivant-Event-Id (también enviada como X-KYC-Event-Id) es única para cada evento. Patrón habitual:

async function handleWebhook(rawBody: string, headers: Headers) {
  const eventId = headers.get("X-KYC-Event-Id")!;

  // Skip if already processed
  const seen = await db.webhookEvent.findUnique({ where: { id: eventId } });
  if (seen) return;

  const payload = JSON.parse(rawBody);

  // Do the work + record the event in a single transaction
  await db.$transaction([
    db.webhookEvent.create({ data: { id: eventId, eventType: payload.event } }),
    ...yourBusinessLogic(payload),
  ]);
}

X-KYC-Event-Id se mantiene estable entre reintentos: el mismo evento lógico conserva el mismo id sin importar cuántos intentos de entrega se realicen.

Orden

Los eventos de solicitudes distintas pueden llegar en cualquier orden. Los eventos de la misma solicitud se ponen en cola en el orden en que se generaron, pero los reintentos pueden entrelazarlos: por ejemplo, un evento processing atascado podría llegar después de un evento approved de la misma solicitud.

Patrón defensivo: use siempre el campo status como fuente de verdad e ignore los eventos cuyo status sea «anterior» a lo que ya ha registrado. El ciclo de vida es estrictamente hacia adelante (PENDING_UPLOAD → PROCESSING → APPROVED/REJECTED/ESCALATED), así que si su base de datos indica que la solicitud está APPROVED y llega un kyc.request.processing retrasado, descártelo.

Probar los manejadores de eventos en local

Dos botones del panel ayudan en esto:

  • Probar (junto a cualquier URL de webhook, a nivel de inquilino o por formulario) envía un ping de conectividad kyc.webhook.test (test: true). Úselo para confirmar que su endpoint es accesible y devuelve 2xx: no necesita datos de solicitud ni un secreto configurado.
  • Enviar un evento de prueba envía un payload completo kyc.request.approved con datos de ejemplo, para que pueda probar su flujo real de tratamiento de resultados.

Para probar otros tipos de evento, escriba un pequeño script que construya el mismo sobre y lo envíe por POST a su URL de desarrollo mediante un túnel:

# tunnel localhost:3000 to a public URL
ngrok http 3000

# Then in the dashboard webhook settings, paste the ngrok URL,
# trigger real test requests via the API, and watch your logs.