Descripción General
Los eventos de webhook KYC le permiten recibir notificaciones en tiempo real cuando cambia el estado de una verificación KYC. Gu1 envía automáticamente solicitudes HTTP POST a su endpoint de webhook configurado cada vez que se actualiza un estado de validación, permitiéndole automatizar flujos de trabajo de incorporación de clientes y mantener el cumplimiento.
¿Por Qué Usar Webhooks KYC?
Actualizaciones en Tiempo Real Reciba notificaciones instantáneas cuando cambie el estado de verificación
Eficiente No es necesario consultar la API repetidamente
Flujos de Trabajo Automatizados Actualice automáticamente cuentas de usuario según resultados de verificación
Mejor UX Notifique a los clientes inmediatamente después de la verificación
Eventos Disponibles
Gu1 envía webhooks para los siguientes eventos de validación KYC:
Tipo de Evento Descripción Cuándo se Activa kyc.validation_createdSesión de validación creada Cuando crea una nueva validación KYC kyc.validation_in_progressCliente comenzó verificación El cliente está completando activamente la verificación (llenando formulario) kyc.validation_in_reviewVerificación en revisión Verificación completada, requiere revisión manual del equipo de compliance kyc.validation_approvedVerificación aprobada Identidad verificada con éxito kyc.validation_rejectedVerificación rechazada Falló la verificación de identidad kyc.validation_abandonedCliente abandonó proceso El cliente se fue sin completar kyc.validation_expiredSesión de validación expiró Sesión expirada (típicamente después de 7 días) kyc.validation_cancelledValidación cancelada Validación cancelada manualmente por la organización
Estructura del Payload de Evento
Todos los eventos de webhook KYC siguen esta estructura estándar:
{
"event" : "kyc.validation_approved" ,
"timestamp" : "2025-01-07T11:00:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "approved"
// ... campos específicos del evento
}
}
Campos Comunes del Payload
El tipo de evento (ej., kyc.validation_approved)
Timestamp ISO 8601 cuando ocurrió el evento
El ID de validación KYC en Gu1
El ID de entidad (persona) siendo verificada
Información de entidad incluyendo su externalId para búsqueda fácil
Estado actual de validación: pending, in_progress, in_review, approved, rejected, abandoned, expired, cancelled
Payloads Específicos de Eventos
kyc.validation_created
Enviado cuando se crea una nueva validación KYC.
{
"event" : "kyc.validation_created" ,
"timestamp" : "2025-01-07T10:30:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "pending" ,
"validationUrl" : "https://kyc.gu1.io/validate/abc123" ,
"expiresAt" : "2025-01-14T10:30:00Z"
}
}
Caso de uso : Envíe la URL de validación a su cliente por correo electrónico o SMS.
kyc.validation_in_progress
Enviado cuando un cliente inicia el proceso de verificación.
{
"event" : "kyc.validation_in_progress" ,
"timestamp" : "2025-01-07T10:35:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "in_progress" ,
"startedAt" : "2025-01-07T10:35:00Z"
}
}
Caso de uso : Actualice la UI para mostrar estado “Verificación en progreso”.
kyc.validation_in_review
Enviado cuando un cliente completa la verificación y requiere revisión manual del equipo de compliance.
{
"event" : "kyc.validation_in_review" ,
"timestamp" : "2025-01-07T10:50:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "in_review" ,
"completedAt" : "2025-01-07T10:50:00Z"
}
}
Caso de uso : Notifique al equipo de compliance para revisión manual. Actualice la UI para mostrar “En revisión por equipo de compliance”.
kyc.validation_approved
Enviado cuando la verificación se completa con éxito.
{
"event" : "kyc.validation_approved" ,
"timestamp" : "2025-01-07T11:00:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "approved" ,
"verifiedAt" : "2025-01-07T11:00:00Z" ,
"extractedData" : {
"firstName" : "John" ,
"lastName" : "Doe" ,
"dateOfBirth" : "1990-05-20" ,
"nationality" : "US" ,
"documentNumber" : "AB123456" ,
"documentType" : "passport" ,
"documentExpiry" : "2030-05-20" ,
"address" : {
"street" : "123 Main St" ,
"city" : "New York" ,
"state" : "NY" ,
"postalCode" : "10001" ,
"country" : "US"
}
},
"verifiedFields" : [
"firstName" ,
"lastName" ,
"dateOfBirth" ,
"nationality" ,
"documentNumber"
],
"warnings" : [],
"decision" : {
"id_verification" : "pass" ,
"face_match" : "pass" ,
"liveness" : "pass" ,
"document_authenticity" : "pass"
}
}
}
Campos Adicionales :
verifiedAt: Timestamp cuando se aprobó la verificación
extractedData: Información personal extraída del documento
verifiedFields: Array de campos que se verificaron con éxito
warnings: Array de advertencias detectadas durante la verificación (vacío si fue aprobado)
decision: Resultados de verificación para cada verificación
Caso de uso : Active la cuenta del cliente y otorgue acceso a servicios.
kyc.validation_rejected
Enviado cuando falla la verificación.
{
"event" : "kyc.validation_rejected" ,
"timestamp" : "2025-01-07T11:00:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "rejected" ,
"verifiedAt" : "2025-01-07T11:00:00Z" ,
"extractedData" : {},
"verifiedFields" : [],
"warnings" : [
"Document authenticity check failed" ,
"Face match confidence low" ,
"Liveness detection failed"
],
"decision" : {
"id_verification" : "fail" ,
"face_match" : "fail" ,
"liveness" : "fail" ,
"document_authenticity" : "fail"
},
"rejectionReason" : "Document authenticity could not be verified"
}
}
Campos Adicionales :
warnings: Array de razones por las que falló la verificación
rejectionReason: Razón principal del rechazo
Caso de uso : Notifique al cliente que la verificación falló y proporcione orientación sobre los próximos pasos.
kyc.validation_abandoned
Enviado cuando un cliente inicia pero no completa la verificación.
{
"event" : "kyc.validation_abandoned" ,
"timestamp" : "2025-01-07T10:45:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "abandoned" ,
"abandonedAt" : "2025-01-07T10:45:00Z" ,
"lastStep" : "document_upload"
}
}
Campos Adicionales :
lastStep: El último paso que el cliente completó antes de abandonar
Caso de uso : Envíe un correo de recordatorio para completar la verificación.
kyc.validation_expired
Enviado cuando una sesión de validación expira sin completarse.
{
"event" : "kyc.validation_expired" ,
"timestamp" : "2025-01-14T10:30:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "expired" ,
"expiresAt" : "2025-01-14T10:30:00Z" ,
"createdAt" : "2025-01-07T10:30:00Z"
}
}
Caso de uso : Limpie validaciones pendientes y notifique al cliente para reiniciar la verificación.
kyc.validation_cancelled
Enviado cuando una validación es cancelada manualmente por la organización.
{
"event" : "kyc.validation_cancelled" ,
"timestamp" : "2025-01-07T12:00:00Z" ,
"organizationId" : "org-123" ,
"payload" : {
"validationId" : "550e8400-e29b-41d4-a716-446655440000" ,
"entityId" : "123e4567-e89b-12d3-a456-426614174000" ,
"entity" : {
"id" : "123e4567-e89b-12d3-a456-426614174000" ,
"externalId" : "customer_xyz789" ,
"name" : "John Doe" ,
"type" : "person"
},
"status" : "cancelled" ,
"cancelledAt" : "2025-01-07T12:00:00Z" ,
"cancelledBy" : "user_123"
}
}
Caso de uso : Notifique al cliente que la validación fue cancelada. Limpie recursos asociados y actualice el estado en su sistema.
Ejemplos de Código
Node.js - Manejo de Eventos KYC
const express = require ( 'express' );
const crypto = require ( 'crypto' );
const app = express ();
app . use ( express . json ({
verify : ( req , res , buf ) => {
req . rawBody = buf . toString ( 'utf8' );
}
}));
app . post ( '/webhooks/kyc' , async ( req , res ) => {
try {
// Verificar firma de webhook (ver guía de seguridad)
const signature = req . headers [ 'x-webhook-signature' ];
const webhookSecret = process . env . GU1_WEBHOOK_SECRET ;
if ( ! verifySignature ( req . rawBody , signature , webhookSecret )) {
console . error ( 'Invalid webhook signature' );
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Extraer datos del webhook
const { event , timestamp , organizationId , payload } = req . body ;
console . log ( 'Received KYC webhook:' , {
event ,
validationId: payload . validationId ,
status: payload . status
});
// Procesar el webhook según tipo de evento
await handleKycWebhook ( event , payload );
// Devolver 200 para confirmar recepción
res . status ( 200 ). json ({
success: true ,
message: 'Webhook received'
});
} catch ( error ) {
console . error ( 'Webhook error:' , error );
res . status ( 500 ). json ({
error: error . message
});
}
});
async function handleKycWebhook ( event , data ) {
const { validationId , entityId , entity , status } = data ;
// Actualizar su base de datos con ID de validación de Gu1
await db . updateEntity ( entity . externalId , {
kycValidationId: validationId ,
kycStatus: status ,
lastUpdated: new Date ()
});
// Realizar acciones según tipo de evento
switch ( event ) {
case 'kyc.validation_created' :
console . log ( 'KYC validation created for:' , entity . name );
// Enviar URL de validación al cliente
await sendValidationEmail ( entity , data . validationUrl );
break ;
case 'kyc.validation_in_progress' :
await notifyCustomer ( entity . externalId , 'verification-started' );
break ;
case 'kyc.validation_in_review' :
// Notificar al equipo de compliance
await notifyComplianceTeam ( entity , validationId );
await notifyCustomer ( entity . externalId , 'verification-in-review' );
break ;
case 'kyc.validation_approved' :
// Extraer datos verificados
const { extractedData , verifiedFields } = data ;
await db . updateEntity ( entity . externalId , {
verifiedData: extractedData ,
verifiedFields: verifiedFields ,
verifiedAt: data . verifiedAt ,
isVerified: true
});
// Activar cuenta de cliente
await activateCustomerAccount ( entity . externalId );
await notifyCustomer ( entity . externalId , 'verification-approved' );
break ;
case 'kyc.validation_rejected' :
await db . updateEntity ( entity . externalId , {
isVerified: false ,
rejectionReasons: data . warnings ,
rejectionReason: data . rejectionReason
});
await notifyCustomer ( entity . externalId , 'verification-rejected' , {
reasons: data . warnings
});
break ;
case 'kyc.validation_abandoned' :
await notifyCustomer ( entity . externalId , 'verification-incomplete' , {
lastStep: data . lastStep
});
break ;
case 'kyc.validation_expired' :
await notifyCustomer ( entity . externalId , 'verification-expired' );
// Limpiar validación expirada
await db . deleteValidation ( validationId );
break ;
case 'kyc.validation_cancelled' :
await notifyCustomer ( entity . externalId , 'verification-cancelled' );
// Limpiar recursos asociados
await db . deleteValidation ( validationId );
break ;
}
}
function verifySignature ( rawBody , signature , secret ) {
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( rawBody )
. digest ( 'hex' );
return signature === expectedSignature ;
}
app . listen ( 3000 );
Python - Manejo de Eventos KYC
from flask import Flask, request, jsonify
import hmac
import hashlib
import logging
app = Flask( __name__ )
@app.route ( '/webhooks/kyc' , methods = [ 'POST' ])
def kyc_webhook ():
try :
# Obtener firma del header
signature = request.headers.get( 'X-Webhook-Signature' )
webhook_secret = os.getenv( 'GU1_WEBHOOK_SECRET' )
# Obtener cuerpo raw para verificación de firma
raw_body = request.get_data( as_text = True )
# Verificar firma
if not verify_signature(raw_body, signature, webhook_secret):
logging.error( 'Invalid webhook signature' )
return jsonify({ 'error' : 'Invalid signature' }), 401
# Analizar payload
payload = request.json
event = payload.get( 'event' )
data = payload.get( 'payload' )
logging.info( f 'Received KYC webhook: { event } ' )
# Procesar el webhook
handle_kyc_webhook(event, data)
return jsonify({
'success' : True ,
'message' : 'Webhook received'
}), 200
except Exception as e:
logging.error( f 'Webhook error: { e } ' )
return jsonify({
'error' : str (e)
}), 500
def verify_signature ( raw_body , signature , secret ):
"""Verificar firma HMAC SHA-256"""
expected_signature = hmac.new(
secret.encode( 'utf-8' ),
raw_body.encode( 'utf-8' ),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def handle_kyc_webhook ( event , data ):
validation_id = data[ 'validationId' ]
entity_id = data[ 'entityId' ]
entity = data[ 'entity' ]
status = data[ 'status' ]
# Actualizar base de datos
db.update_entity(
external_id = entity[ 'externalId' ],
kyc_validation_id = validation_id,
kyc_status = status,
last_updated = datetime.now()
)
# Manejar diferentes eventos
if event == 'kyc.validation_created' :
send_validation_email(entity, data[ 'validationUrl' ])
elif event == 'kyc.validation_in_review' :
notify_compliance_team(entity, validation_id)
notify_customer(entity[ 'externalId' ], 'verification-in-review' )
elif event == 'kyc.validation_approved' :
db.update_entity(
external_id = entity[ 'externalId' ],
verified_data = data.get( 'extractedData' ),
verified_fields = data.get( 'verifiedFields' ),
verified_at = data.get( 'verifiedAt' ),
is_verified = True
)
activate_customer_account(entity[ 'externalId' ])
notify_customer(entity[ 'externalId' ], 'verification-approved' )
elif event == 'kyc.validation_rejected' :
db.update_entity(
external_id = entity[ 'externalId' ],
is_verified = False ,
rejection_reasons = data.get( 'warnings' )
)
notify_customer(entity[ 'externalId' ], 'verification-rejected' )
elif event == 'kyc.validation_abandoned' :
notify_customer(entity[ 'externalId' ], 'verification-incomplete' )
elif event == 'kyc.validation_expired' :
notify_customer(entity[ 'externalId' ], 'verification-expired' )
db.delete_validation(validation_id)
elif event == 'kyc.validation_cancelled' :
notify_customer(entity[ 'externalId' ], 'verification-cancelled' )
db.delete_validation(validation_id)
if __name__ == '__main__' :
app.run( port = 3000 )
Casos de Uso
Caso de Uso 1: Activación Automática de Cuenta
Active automáticamente cuentas de clientes cuando se aprueba el KYC:
async function handleKycApproved ( data ) {
const { entity , extractedData , verifiedFields } = data ;
// Actualizar registro de cliente
await db . customers . update ({
where: { externalId: entity . externalId },
data: {
kycStatus: 'approved' ,
verifiedAt: new Date (),
verifiedData: extractedData ,
isActive: true
}
});
// Habilitar inicio de sesión
await auth . enableUser ( entity . externalId );
// Otorgar acceso a servicios
await services . grantAccess ( entity . externalId , [ 'trading' , 'withdrawal' ]);
// Enviar correo de bienvenida
await email . send ({
to: extractedData . email || entity . attributes ?. email ,
subject: 'Welcome! Your account is verified' ,
template: 'kyc-approved' ,
data: {
name: entity . name ,
verifiedAt: new Date (). toISOString ()
}
});
// Registrar evento
await audit . log ({
action: 'kyc_approved' ,
entityId: entity . id ,
timestamp: new Date ()
});
}
Caso de Uso 2: Monitoreo de Cumplimiento
Rastree rechazos de KYC para revisión de cumplimiento:
async function handleKycRejected ( data ) {
const { entity , warnings , rejectionReason } = data ;
// Registrar rechazo
await compliance . logRejection ({
entityId: entity . id ,
externalId: entity . externalId ,
reasons: warnings ,
primaryReason: rejectionReason ,
timestamp: new Date ()
});
// Alertar al equipo de cumplimiento si hay múltiples rechazos
const rejectCount = await db . rejections . count ({
where: { entityId: entity . id }
});
if ( rejectCount >= 3 ) {
await slack . send ({
channel: '#compliance-alerts' ,
message: `🚨 Multiple KYC rejections for ${ entity . name } ( ${ entity . externalId } )` ,
fields: {
'Entity ID' : entity . id ,
'Rejection Count' : rejectCount ,
'Latest Reason' : rejectionReason
}
});
}
// Notificar al cliente con orientación
await email . send ({
to: entity . attributes ?. email ,
subject: 'Verification unsuccessful' ,
template: 'kyc-rejected' ,
data: {
name: entity . name ,
reasons: warnings ,
supportUrl: 'https://support.example.com/kyc'
}
});
}
Caso de Uso 3: Recuperación de Verificación Abandonada
Envíe recordatorios a clientes que abandonan la verificación:
async function handleKycAbandoned ( data ) {
const { entity , validationId , lastStep } = data ;
// Programar correo de recordatorio
await queue . scheduleEmail ({
to: entity . attributes ?. email ,
subject: 'Complete your verification' ,
template: 'kyc-reminder' ,
data: {
name: entity . name ,
lastStep ,
validationUrl: `https://kyc.gu1.io/validate/ ${ validationId } `
},
sendAt: new Date ( Date . now () + 24 * 60 * 60 * 1000 ) // Enviar en 24 horas
});
// Rastrear abandono
await analytics . track ({
event: 'kyc_abandoned' ,
entityId: entity . id ,
lastStep ,
timestamp: new Date ()
});
}
Mejores Prácticas
Use entity.externalId para Búsqueda
El webhook incluye entity.externalId que es el ID que proporcionó al crear la entidad. Úselo para buscar al cliente en su base de datos. const customer = await db . findCustomer ({
externalId: data . entity . externalId
});
Almacene IDs de Validación
Almacene el validationId de Gu1 en su base de datos. Esto le permite consultar detalles de validación más tarde si es necesario. await db . updateCustomer ( customer . id , {
kycValidationId: data . validationId ,
kycStatus: data . status
});
Podría recibir el mismo webhook múltiples veces. Use el validationId para asegurar que procese cada evento solo una vez. async function handleWebhook ( webhook ) {
const alreadyProcessed = await db . checkWebhookProcessed (
webhook . payload . validationId ,
webhook . event
);
if ( alreadyProcessed ) {
return ; // Omitir duplicado
}
// Procesar webhook
await processValidation ( webhook . payload );
// Marcar como procesado
await db . markWebhookProcessed (
webhook . payload . validationId ,
webhook . event
);
}
Siempre devuelva un código de estado 200 lo más rápido posible para confirmar recepción. Procese el webhook asincrónicamente si es necesario. app . post ( '/webhooks/kyc' , async ( req , res ) => {
// Confirmar inmediatamente
res . status ( 200 ). send ( 'OK' );
// Procesar asincrónicamente
processWebhook ( req . body ). catch ( console . error );
});
Siempre verifique el header X-Webhook-Signature para asegurar que el webhook sea auténtico. Vea la guía de seguridad para detalles. const signature = req . headers [ 'x-webhook-signature' ];
if ( ! verifySignature ( req . rawBody , signature , secret )) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
Maneje Errores con Gracia
Si el procesamiento falla, registre el error pero aún devuelva 200 para evitar reintentos. Almacene webhooks fallidos para revisión manual. try {
await processWebhook ( payload );
} catch ( error ) {
await db . saveFailedWebhook ({
payload ,
error: error . message ,
receivedAt: new Date ()
});
// Aún así devolver 200
res . status ( 200 ). json ({ success: false });
}
Solución de Problemas
Verificar estos elementos:
La URL del webhook es públicamente accesible vía HTTPS
El webhook está configurado y habilitado en el dashboard
Suscrito a tipos de eventos KYC correctos
El endpoint devuelve código de estado 200 dentro de 30 segundos
Verificar logs del servidor para solicitudes entrantes
Falla la Verificación de Firma
Causas comunes:
Usar secreto incorrecto (verificar dashboard para secreto actual)
Verificar firma en JSON analizado en lugar de cuerpo raw
Secreto no guardado correctamente después de creación del webhook
Problemas de codificación (asegurar UTF-8)
Vea la guía de seguridad para implementación adecuada.
Recibo Webhooks Duplicados
Este es un comportamiento normal. Los webhooks pueden enviarse múltiples veces debido a problemas de red, tiempos de espera o reintentos. Siempre implemente idempotencia usando el validationId del webhook y tipo de event.
Próximos Pasos