Skip to main content

Resumen

Los webhooks te permiten recibir notificaciones en tiempo real cuando cambia el estado de verificación KYC. Gu1 envía automáticamente solicitudes HTTP POST a tu endpoint webhook configurado cada vez que se actualiza el estado de una validación.

¿Por Qué Usar Webhooks?

Actualizaciones en Tiempo Real

Recibe notificaciones instantáneas cuando cambia el estado de verificación

Eficiente

No necesitas consultar la API repetidamente

Flujos Automáticos

Actualiza automáticamente cuentas de usuario según los resultados de verificación

Mejor UX

Notifica a los clientes inmediatamente después de la verificación

Eventos de Webhook

Gu1 envía webhooks para los siguientes eventos de validación KYC:
Tipo de EventoDescripciónCuándo se Dispara
kyc.validation_createdSesión de validación creadaCuando creas una nueva validación KYC
kyc.validation_in_progressCliente inició verificaciónCliente comienza el proceso de verificación
kyc.validation_approvedVerificación aprobadaIdentidad verificada exitosamente
kyc.validation_rejectedVerificación rechazadaVerificación de identidad falló
kyc.validation_abandonedCliente abandonó el procesoCliente abandonó sin completar
kyc.validation_expiredSesión de validación expiróSesión expiró (típicamente después de 7 días)

Configurar Webhooks

Paso 1: Configurar Webhook en el Dashboard

Configura la URL de tu webhook en el dashboard de Gu1:
  1. Navega a Configuración de Webhooks
    • Inicia sesión en tu dashboard de Gu1
    • Ve a ConfiguraciónWebhooks
  2. Crear Nuevo Webhook
    • Haz clic en Agregar Webhook o Crear Webhook
    • Ingresa un nombre descriptivo (ej., “Webhook KYC Producción”)
    • Ingresa la URL de tu webhook (debe ser HTTPS): https://tuapp.com/webhooks/kyc
  3. Seleccionar Ambiente
    • Elige Sandbox para pruebas
    • Elige Producción para eventos en vivo
  4. Suscribirse a Eventos
    • Selecciona todos los eventos KYC o específicos:
      • kyc.validation_created
      • kyc.validation_in_progress
      • kyc.validation_approved
      • kyc.validation_rejected
      • kyc.validation_abandoned
      • kyc.validation_expired
  5. Guardar tu Secreto
    • Se generará automáticamente un secreto de webhook
    • Copia y guarda este secreto - lo necesitarás para verificar las firmas de webhook
    • No podrás verlo nuevamente (pero puedes regenerarlo después)
  6. Activar el Webhook
    • Activa el webhook marcándolo como Habilitado
    • Haz clic en Guardar
Puedes crear webhooks separados para los ambientes sandbox y producción con URLs diferentes.

Paso 2: Crear un Endpoint de Webhook

Crea un endpoint en tu aplicación para recibir solicitudes POST de webhook:
const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANTE: Usar body raw para verificación de firma
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/kyc', async (req, res) => {
  try {
    // Verificar firma del webhook
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GUENO_WEBHOOK_SECRET;

    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Firma de webhook inválida');
      return res.status(401).json({ error: 'Firma inválida' });
    }

    // Extraer datos del webhook
    const { event, timestamp, organizationId, payload } = req.body;

    console.log('Webhook KYC recibido:', {
      event,
      validationId: payload.validationId,
      status: payload.status
    });

    // Procesar el webhook según el tipo de evento
    await handleKycWebhook(event, payload);

    // Retornar 200 para confirmar recepción
    res.status(200).json({
      success: true,
      message: 'Webhook recibido'
    });
  } catch (error) {
    console.error('Error en webhook:', error);
    // Aún así retornar 200 para prevenir reintentos
    res.status(200).json({
      success: false,
      error: error.message
    });
  }
});

// Verificar firma HMAC
function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

async function handleKycWebhook(event, data) {
  const { validationId, entityId, entity, status } = data;

  // Actualizar tu base de datos con el ID de validación de Gu1
  await db.updateEntity(entity.externalId, {
    kycValidationId: validationId,
    kycStatus: status,
    lastUpdated: new Date()
  });

  // Realizar acciones según el tipo de evento
  switch (event) {
    case 'kyc.validation_created':
      console.log('Validación KYC creada para:', entity.name);
      break;

    case 'kyc.validation_in_progress':
      await notifyCustomer(entity.externalId, 'verificacion-iniciada');
      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
      });

      await activateCustomerAccount(entity.externalId);
      await notifyCustomer(entity.externalId, 'verificacion-aprobada');
      break;

    case 'kyc.validation_rejected':
      await db.updateEntity(entity.externalId, {
        isVerified: false,
        rejectionReasons: data.warnings
      });

      await notifyCustomer(entity.externalId, 'verificacion-rechazada');
      break;

    case 'kyc.validation_abandoned':
      await notifyCustomer(entity.externalId, 'verificacion-incompleta');
      break;

    case 'kyc.validation_expired':
      await notifyCustomer(entity.externalId, 'verificacion-expirada');
      break;
  }
}

Paso 3: Hacer tu Endpoint Públicamente Accesible

Tu endpoint de webhook debe:
  • Ser públicamente accesible vía HTTPS
  • Poder recibir solicitudes POST
  • Retornar código de estado 200 rápidamente (dentro de 30 segundos)
Para desarrollo local, usa herramientas como ngrok para crear una URL pública que haga túnel a tu servidor local.

Seguridad: Verificar Firmas de Webhook

Siempre verifica las firmas de webhook para asegurar que las solicitudes provienen de Gu1.

Cómo Funciona la Verificación de Firmas

  1. Gu1 genera una firma HMAC SHA-256 del payload del webhook usando tu secreto
  2. La firma se envía en el header X-Webhook-Signature
  3. Tu servidor recalcula la firma usando el mismo secreto
  4. Compara las firmas - si coinciden, el webhook es auténtico

Ejemplos de Verificación de Firma

const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

// En tu endpoint de webhook:
app.post('/webhooks/kyc', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const secret = process.env.GUENO_WEBHOOK_SECRET;

  if (!verifySignature(req.rawBody, signature, secret)) {
    return res.status(401).json({ error: 'Firma inválida' });
  }

  // Procesar webhook...
});
Nunca omitas la verificación de firma en producción. Sin ella, cualquiera puede enviar webhooks falsos a tu endpoint.

Headers HTTP

Cada solicitud de webhook incluye estos headers:
HeaderDescripciónEjemplo
Content-TypeSiempre application/jsonapplication/json
X-Webhook-EventTipo de eventokyc.validation_approved
X-Webhook-IDID de configuración del webhook550e8400-e29b-41d4-a716-446655440000
X-Webhook-TimestampTimestamp ISO 86012025-01-15T10:30:00.000Z
X-Webhook-SignatureFirma HMAC SHA-256abc123...

Estructura del Payload de Webhook

Todos los webhooks siguen esta estructura estándar:
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-15T11: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
Tu 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 la entidad incluyendo tu externalId para fácil búsqueda
payload.status
string
Estado actual de validación: pending, in_progress, approved, rejected, abandoned, expired

Payloads Específicos por Evento

kyc.validation_created

Enviado cuando se crea una nueva validación KYC.
{
  "event": "kyc.validation_created",
  "timestamp": "2025-01-15T10: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"
  }
}

kyc.validation_in_progress

Enviado cuando un cliente inicia el proceso de verificación.
{
  "event": "kyc.validation_in_progress",
  "timestamp": "2025-01-15T10:35:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "in_progress"
  }
}

kyc.validation_approved

Enviado cuando la verificación se completa exitosamente.
{
  "event": "kyc.validation_approved",
  "timestamp": "2025-01-15T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "approved",
    "verifiedAt": "2025-01-15T11:00:00Z",
    "extractedData": {
      "firstName": "John",
      "lastName": "Doe",
      "dateOfBirth": "1990-05-20",
      "nationality": "US",
      "documentNumber": "AB123456",
      "documentType": "passport"
    },
    "verifiedFields": [
      "firstName",
      "lastName",
      "dateOfBirth",
      "nationality",
      "documentNumber"
    ],
    "warnings": [],
    "decision": {
      "id_verification": "pass",
      "face_match": "pass",
      "liveness": "pass"
    }
  }
}
Campos Adicionales:
  • verifiedAt: Timestamp cuando se aprobó la verificación
  • extractedData: Información personal extraída del documento
  • verifiedFields: Array de campos que fueron verificados exitosamente
  • warnings: Array de advertencias detectadas durante la verificación
  • decision: Resultados de verificación (imágenes/URLs removidas por seguridad)

kyc.validation_rejected

Enviado cuando la verificación falla.
{
  "event": "kyc.validation_rejected",
  "timestamp": "2025-01-15T11:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "rejected",
    "verifiedAt": "2025-01-15T11:00:00Z",
    "extractedData": {},
    "verifiedFields": [],
    "warnings": [
      "Falló la verificación de autenticidad del documento",
      "Confianza baja en coincidencia facial"
    ],
    "decision": {
      "id_verification": "fail",
      "face_match": "fail",
      "liveness": "pass"
    }
  }
}

kyc.validation_abandoned

Enviado cuando un cliente inicia pero no completa la verificación.
{
  "event": "kyc.validation_abandoned",
  "timestamp": "2025-01-15T10:45:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "abandoned"
  }
}

kyc.validation_expired

Enviado cuando una sesión de validación expira sin completarse.
{
  "event": "kyc.validation_expired",
  "timestamp": "2025-01-15T12:00:00Z",
  "organizationId": "org-123",
  "payload": {
    "validationId": "550e8400-e29b-41d4-a716-446655440000",
    "entityId": "123e4567-e89b-12d3-a456-426614174000",
    "entity": { ... },
    "status": "expired",
    "expiresAt": "2025-01-22T10:30:00Z"
  }
}

Política de Reintentos

Si tu endpoint de webhook falla en responder con un código de estado 2xx, Gu1 automáticamente reintentará la entrega. Política de Reintentos Predeterminada:
  • Reintentos Máximos: 3 intentos
  • Delay Inicial: 1000ms (1 segundo)
  • Multiplicador de Backoff: 2x
  • Secuencia de Reintentos: 1s → 2s → 4s
Ejemplo de Timeline:
  1. Intento inicial en T+0s
  2. Primer reintento en T+1s
  3. Segundo reintento en T+3s (1s + 2s)
  4. Tercer reintento en T+7s (1s + 2s + 4s)
Criterio de Éxito:
  • Códigos de estado HTTP 200-299 se consideran exitosos
  • Cualquier otro código de estado o error de red dispara un reintento
Timeout:
  • Cada intento tiene un timeout de 30 segundos
  • Si tu endpoint no responde dentro de 30 segundos, el intento se marca como fallido
Puedes ver todos los intentos de entrega de webhook (incluyendo reintentos) en la sección Webhooks → Historial de tu dashboard.

Mejores Prácticas

Siempre retorna un código de estado 200 lo más rápido posible para confirmar la recepción. Procesa 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 verifica el header X-Webhook-Signature para asegurar que el webhook es auténtico.
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.rawBody, signature, secret)) {
  return res.status(401).json({ error: 'Firma inválida' });
}
Podrías recibir el mismo webhook múltiples veces. Usa el validationId para asegurar que procesas 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
  );
}
El webhook incluye entity.externalId que es el ID que proporcionaste al crear la entidad. Úsalo para buscar al cliente en tu base de datos.
const customer = await db.findCustomer({
  externalId: data.entity.externalId
});
Almacena el validationId de Gu1 en tu base de datos. Esto te permite consultar detalles de validación después si es necesario.
await db.updateCustomer(customer.id, {
  kycValidationId: data.validationId,
  kycStatus: data.status
});
Si el procesamiento falla, registra el error pero aún así retorna 200 para prevenir reintentos. Almacena 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í retornar 200
  res.status(200).json({ success: false });
}
Crea configuraciones de webhook separadas para los ambientes sandbox y producción.
  • Sandbox: Usar para pruebas con datos de prueba
  • Producción: Usar para verificaciones de clientes en vivo
Esto te permite probar el manejo de webhooks de forma segura sin afectar sistemas de producción.

Probar Webhooks

Probar en el Dashboard

  1. Ve a ConfiguraciónWebhooks
  2. Selecciona tu webhook
  3. Haz clic en Probar Webhook
  4. Gu1 enviará un evento de prueba a tu endpoint
  5. Verifica el estado de respuesta y logs

Desarrollo Local

Usa ngrok para exponer tu servidor local:
# Iniciar ngrok
ngrok http 3000

# Usa la URL de ngrok como tu URL de webhook
https://abc123.ngrok.io/webhooks/kyc

Flujo de Prueba

  1. Crea un webhook sandbox apuntando a tu endpoint de desarrollo
  2. Crea una validación KYC de prueba
  3. Tu endpoint de webhook recibe kyc.validation_created
  4. Completa la verificación (o simula diferentes resultados)
  5. Tu endpoint de webhook recibe actualizaciones de estado

Monitoreo y Debugging

Logs de Webhook

Ver historial de entrega de webhook en tu dashboard:
  1. Ve a ConfiguraciónWebhooks
  2. Selecciona tu webhook
  3. Haz clic en Ver Logs o Historial
Los logs incluyen:
  • Timestamp de cada intento de entrega
  • Código de estado HTTP recibido
  • Body de respuesta
  • Tiempo de respuesta
  • Mensajes de error (si hay)
  • Intentos de reintento

Estadísticas de Webhook

Cada webhook muestra:
  • Disparos Totales: Número total de veces que se disparó el webhook
  • Conteo de Éxitos: Entregas exitosas
  • Conteo de Fallos: Entregas fallidas
  • Último Disparado: Timestamp del último intento
  • Último Éxito: Timestamp de última entrega exitosa
  • Último Fallo: Timestamp de último fallo

Solución de Problemas

Verifica estos elementos:
  • URL del webhook es públicamente accesible vía HTTPS
  • Firewall permite solicitudes POST entrantes de Gu1
  • Endpoint retorna código de estado 200 dentro de 30 segundos
  • Webhook está configurado y habilitado en el dashboard
  • Ambiente correcto seleccionado (sandbox vs producción)
  • Verifica logs del servidor para solicitudes entrantes
  • Verifica que el webhook está suscrito a los tipos de evento correctos
Causas comunes:
  • Usando secreto incorrecto (verifica el dashboard para el secreto actual)
  • Verificando firma en JSON parseado en lugar de body raw
  • Secreto no guardado correctamente después de crear webhook
  • Problemas de codificación (asegurar UTF-8)
Solución:
// INCORRECTO - verificando body parseado
const signature = crypto.createHmac('sha256', secret)
  .update(JSON.stringify(req.body))
  .digest('hex');

// CORRECTO - usar body raw antes de parsear
const signature = crypto.createHmac('sha256', secret)
  .update(req.rawBody)
  .digest('hex');
Este es un comportamiento normal. Los webhooks pueden enviarse múltiples veces debido a:
  • Problemas de red
  • Timeouts
  • Reintentos después de fallos
Siempre implementa idempotencia usando el validationId del webhook y el tipo de event.
Tu endpoint debe responder dentro de 30 segundos. Si el procesamiento toma más tiempo:
app.post('/webhooks', async (req, res) => {
  // Responder inmediatamente
  res.status(200).json({ received: true });

  // Procesar en segundo plano
  await queueWebhookProcessing(req.body);
});
extractedData y verifiedFields solo se incluyen en:
  • kyc.validation_approved
  • kyc.validation_rejected
No están presentes en otros tipos de evento como validation_created o validation_in_progress.
Si perdiste tu secreto de webhook:
  1. Ve a ConfiguraciónWebhooks
  2. Selecciona tu webhook
  3. Haz clic en Regenerar Secreto
  4. Guarda el nuevo secreto en tus variables de entorno
  5. Actualiza tu aplicación con el nuevo secreto
Nota: El secreto antiguo dejará de funcionar inmediatamente después de la regeneración.

Próximos Pasos