Skip to main content

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 EventoDescripciónCuándo se Activa
kyc.validation_createdSesión de validación creadaCuando crea una nueva validación KYC
kyc.validation_in_progressCliente comenzó verificaciónEl cliente está completando activamente la verificación (llenando formulario)
kyc.validation_in_reviewVerificación en revisiónVerificación completada, requiere revisión manual del equipo de compliance
kyc.validation_approvedVerificación aprobadaIdentidad verificada con éxito
kyc.validation_rejectedVerificación rechazadaFalló la verificación de identidad
kyc.validation_abandonedCliente abandonó procesoEl 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 canceladaValidació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

event
string
El tipo de evento (ej., kyc.validation_approved)
timestamp
string
Timestamp ISO 8601 cuando ocurrió el evento
organizationId
string
Su ID de organización
payload.validationId
string
El ID de validación KYC en Gu1
payload.entityId
string
El ID de entidad (persona) siendo verificada
payload.entity
object
Información de entidad incluyendo su externalId para búsqueda fácil
payload.status
string
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

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 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' });
}
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
extractedData y verifiedFields solo se incluyen en:
  • kyc.validation_approved
  • kyc.validation_rejected
No están presentes en otros tipos de eventos como validation_created o validation_in_progress.
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.
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