Skip to main content

Configuración y Seguridad

Los webhooks permiten que Trébol te notifique automáticamente cuando ocurren eventos importantes, como la finalización de una verificación.
  • URL de entrega: Debe aceptar solicitudes HTTPS POST.
  • Autenticación: Firma HMAC-SHA256 con un secreto por webhook.

Obtención del secreto de webhook

Cada webhook tiene un secreto único que se genera automáticamente cuando lo creas. Este secreto es esencial para verificar la autenticidad de los webhooks que recibes.

Cómo obtener tu secreto

El secreto del webhook se genera y se muestra únicamente una vez cuando creas el webhook a través de la API. Es crucial que guardes este secreto de forma segura, ya que no podrás recuperarlo posteriormente.
1

Crear webhook con API

Usa el endpoint POST /v2/webhooks para crear un nuevo webhook. Consulta la documentación completa del endpoint, incluyendo parámetros y ejemplos de respuesta, en API Reference → Gestión de Webhooks.
¡Importante! El secreto del webhook se genera y se muestra únicamente una vez en la respuesta de creación. Copia y guarda el secreto inmediatamente, ya que no podrás recuperarlo posteriormente por razones de seguridad.
2

Guardar el secreto

En la respuesta de creación, encontrarás el campo secret que contiene tu secreto único. Este secreto es esencial para verificar la autenticidad de los webhooks que recibes.
3

Configurar en tu aplicación

Almacena el secreto de forma segura en tu aplicación:
Environment Variables
# .env
WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXXXX
Configuration
// config.js
const config = {
  webhookSecret: process.env.WEBHOOK_SECRET,
};

Gestión de secretos

Si pierdes tu secreto de webhook, deberás crear un nuevo webhook para obtener un nuevo secreto. No es posible recuperar o regenerar el secreto de un webhook existente.
  1. Crea un nuevo webhook con la misma configuración
  2. Actualiza tu aplicación con el nuevo secreto
  3. Elimina el webhook anterior una vez que confirmes que el nuevo funciona correctamente
Para mayor seguridad, puedes implementar una rotación periódica de secretos:
  1. Crea un nuevo webhook con un nuevo secreto
  2. Actualiza tu aplicación para aceptar ambos secretos temporalmente
  3. Una vez confirmado que el nuevo webhook funciona, elimina el anterior
  4. Actualiza tu aplicación para usar solo el nuevo secreto
Es recomendable usar diferentes webhooks (y por tanto diferentes secretos) para cada entorno:
  • Desarrollo: whsec_dev_XXXXXXXXXXXXXXXXXXXXXXXX
  • Staging: whsec_staging_XXXXXXXXXXXXXXXXXXXXXXXX
  • Producción: whsec_prod_XXXXXXXXXXXXXXXXXXXXXXXX
Esto te permite probar webhooks sin afectar tu entorno de producción.

Verificación de firmas de webhook

Para garantizar la seguridad y autenticidad de los webhooks, Trébol incluye una firma HMAC-SHA256 en el header Trebol-Signature de cada solicitud. Esta firma te permite verificar que el webhook proviene realmente de Trébol y que el payload no ha sido modificado.

Formato del header de firma

El header Trebol-Signature contiene múltiples elementos separados por comas:
Trebol-Signature: t=1640995200,v1=abc123def456...
  • t: Timestamp Unix de cuando se generó la firma
  • v1: Firma HMAC-SHA256 del payload

Proceso de verificación

  1. Extraer elementos: Separa el header por comas y extrae el timestamp (t) y la firma (v1)
  2. Construir payload: Concatena el timestamp con el payload: {timestamp}.{payload}
  3. Generar firma esperada: Calcula HMAC-SHA256 usando tu secreto de webhook
  4. Comparar firmas: Usa comparación de tiempo constante para evitar ataques de timing
Siempre verifica las firmas de webhook en producción. Nunca proceses webhooks sin verificar su autenticidad.

Ejemplos de implementación

import crypto from "node:crypto";

function verifyWebhookSignature(payload, signature, secret) {
const elements = signature.split(",");
const timestamp = elements.find((el) => el.startsWith("t="))?.split("=")[1];
const v1 = elements.find((el) => el.startsWith("v1="))?.split("=")[1];

if (!timestamp || !v1) return false;

const payloadToSign = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payloadToSign, "utf8")
.digest("hex");

// Convert both signatures to Buffer for constant time comparison
const receivedSignatureBuffer = Buffer.from(v1, "hex");
const expectedSignatureBuffer = Buffer.from(expectedSignature, "hex");

// Ensure both buffers are the same length
if (receivedSignatureBuffer.length !== expectedSignatureBuffer.length) {
return false;
}

// Constant time comparison
return crypto.timingSafeEqual(
receivedSignatureBuffer,
expectedSignatureBuffer
);
}

// Ejemplo de uso en Express.js
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['trebol-signature'];
const payload = req.body.toString();

if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

// Procesar webhook...
res.status(200).send('OK');
});

Para mayor seguridad, también puedes verificar que el timestamp no sea demasiado antiguo (por ejemplo, no más de 5 minutos) para prevenir ataques de replay.

Direcciones IP de origen

Trébol envía todos los webhooks desde las siguientes direcciones IP:
  • 35.170.236.123
  • 54.162.134.233
Estas IPs son estáticas y no cambiarán. Puedes configurar tu firewall para permitir únicamente estas direcciones si necesitas restricciones adicionales de seguridad.
Aunque puedes usar las IPs para validación adicional, siempre debes verificar la firma del webhook. Las IPs pueden cambiar en el futuro o ser falsificadas, pero la firma HMAC-SHA256 es la única forma garantizada de verificar la autenticidad.

Reintentos automáticos

Cuando tu endpoint no responde con un código de estado exitoso (2xx), Trébol reintentará automáticamente la entrega del webhook usando un esquema de backoff exponencial.

Comportamiento de reintentos

ReintentoTiempo de esperaTiempo acumulado
130 segundos30 segundos
260 segundos1.5 minutos
3120 segundos3.5 minutos
4240 segundos7.5 minutos
5480 segundos15.5 minutos
Después de 5 reintentos fallidos, el webhook se marca como fallido y no se reintentará más.

¿Cuándo se reintenta un webhook?

Trébol reintentará la entrega cuando:
  • Tu servidor responde con un código de error (4xx o 5xx)
  • Tu servidor no responde (timeout de conexión)
  • Hay un error de red que impide la entrega
Importante: Si tu servidor recibe el webhook pero responde con un error (por ejemplo, 500 Internal Server Error), Trébol reintentará el envío. Esto puede resultar en que tu servidor reciba el mismo evento múltiples veces, aunque lo haya procesado parcialmente antes de fallar.

Evitar reintentos innecesarios

Para evitar que Trébol reintente webhooks que ya procesaste:
  1. Responde con 200 OK inmediatamente después de validar la firma
  2. Procesa el evento de forma asíncrona (ver Mejores prácticas)
  3. Maneja errores internamente sin retornar códigos de error HTTP
@PostMapping("/webhook")
public ResponseEntity<String> webhook(@RequestBody String payload,
                                      @RequestHeader("Trebol-Signature") String signature) {
    // Validar firma
    if (!verifySignature(payload, signature)) {
        return ResponseEntity.status(401).body("Invalid signature");
    }
    
    try {
        // Encolar para procesamiento asíncrono
        eventQueue.enqueue(payload);
    } catch (Exception e) {
        // Loguear el error pero NO retornar 500
        logger.error("Error enqueueing event", e);
    }
    
    // Siempre retornar 200 si la firma es válida
    return ResponseEntity.ok("OK");
}
Si necesitas indicar que hubo un problema pero no quieres reintentos, considera retornar 200 OK y manejar el error internamente con alertas o logs.

Mejores prácticas

Responde rápidamente con un código 2xx

Tu endpoint debe retornar un código de estado exitoso (2xx) inmediatamente antes de ejecutar cualquier lógica compleja que pueda causar un timeout. Si tu servidor tarda demasiado en responder, Trébol asumirá que la entrega falló y reintentará el envío.
Importante: Si tu endpoint no responde dentro de un tiempo razonable, Trébol reintentará el webhook automáticamente, lo que puede resultar en eventos duplicados.
❌ Incorrecto - Procesar antes de responder:
@PostMapping("/webhook")
public ResponseEntity<String> webhook(@RequestBody String payload) {
    // Esto puede tardar y causar timeout
    webhookService.processEvent(payload);
    orchestratorClient.sendEvent(payload);
    databaseService.saveEvent(payload);
    
    return ResponseEntity.ok("OK");
}
✅ Correcto - Responder inmediatamente, procesar después:
@PostMapping("/webhook")
public ResponseEntity<String> webhook(@RequestBody String payload,
                                      @RequestHeader("Trebol-Signature") String signature) {
    // 1. Validar firma (rápido)
    if (!verifySignature(payload, signature)) {
        return ResponseEntity.status(401).body("Invalid signature");
    }
    
    // 2. Encolar para procesamiento asíncrono (rápido)
    eventQueue.enqueue(payload);
    
    // 3. Responder inmediatamente
    return ResponseEntity.ok("OK");
}

// El procesamiento ocurre en un worker separado
@Async
public void processQueuedEvent(String payload) {
    orchestratorClient.sendEvent(payload);
    databaseService.saveEvent(payload);
}

Maneja eventos duplicados

Ocasionalmente, tu endpoint puede recibir el mismo evento más de una vez. Para protegerte contra el procesamiento duplicado, debes implementar idempotencia en tu sistema. Estrategia recomendada: Usa la firma del header Trebol-Signature como clave de deduplicación, ya que es única por evento.
@PostMapping("/webhook")
public ResponseEntity<String> webhook(@RequestBody String payload,
                                      @RequestHeader("Trebol-Signature") String signature) {
    // Extraer la firma v1 como identificador único
    String eventId = extractV1Signature(signature);
    
    // Verificar si ya procesamos este evento
    if (processedEvents.contains(eventId)) {
        return ResponseEntity.ok("Already processed");
    }
    
    // Marcar como procesado ANTES de procesar
    processedEvents.add(eventId);
    
    // Procesar el evento
    eventQueue.enqueue(payload);
    
    return ResponseEntity.ok("OK");
}
Almacena los IDs de eventos procesados en una base de datos o cache (como Redis) con un TTL de al menos 24 horas para manejar reintentos tardíos.

Procesa eventos de forma asíncrona

Configura tu handler para procesar eventos entrantes con una cola asíncrona. Podrías encontrar problemas de escalabilidad si procesas eventos de forma síncrona, especialmente durante picos de tráfico.
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('Trebol-Signature')
    payload = request.get_data(as_text=True)
    
    if not verify_signature(payload, signature):
        abort(401)
    
    # Encolar para procesamiento asíncrono
    message_queue.send({
        'payload': payload,
        'signature': signature,
        'received_at': datetime.utcnow().isoformat()
    })
    
    return 'OK', 200
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('Trebol-Signature')
    payload = request.get_data(as_text=True)
    
    if not verify_signature(payload, signature):
        abort(401)
    
    # Encolar tarea asíncrona
    process_webhook_event.delay(payload, signature)
    
    return 'OK', 200

@celery.task
def process_webhook_event(payload, signature):
    # Lógica de procesamiento aquí
    data = json.loads(payload)
    handle_event(data)

No dependas del orden de los eventos

Trébol no garantiza que los eventos lleguen en el orden en que fueron generados. Por ejemplo, podrías recibir verification_item.v2.completed antes de verification.v2.created. Asegúrate de que tu integración pueda manejar eventos en cualquier orden y usa la API para obtener el estado actual si es necesario.

Crear y gestionar webhooks por API

Usa los endpoints en API Reference → Gestión de Webhooks para administrar tus webhooks.
  • Crear: POST /v2/webhooks
  • Listar: GET /v2/webhooks
  • Obtener: GET /v2/webhooks/{webhookId}
  • Actualizar: PUT /v2/webhooks/{webhookId}
  • Eliminar: DELETE /v2/webhooks/{webhookId}

Tipos de eventos

verification.v2.created

Se dispara cuando se crea una nueva verificación en el sistema. Payload:
{
  "event_name": "verification.v2.created",
  "data": {
    "verification_id": "string",
    "account_id": "string",
    "account_name": "string",
    "created_at": "string (ISO 8601)",
    "status": "string",
    "verification_tag": "string"
  }
}

verification.v2.finished

Se dispara cuando una verificación ha sido completada exitosamente. Payload:
{
  "event_name": "verification.v2.finished",
  "data": {
    "verification_id": "string",
    "account_id": "string",
    "account_name": "string",
    "created_at": "string (ISO 8601)",
    "status": "string",
    "verification_tag": "string"
  }
}

verification_item.v2.completed

Se dispara cuando un item específico dentro de una verificación ha sido completado. Payload:
{
  "event_name": "verification_item.v2.completed",
  "data": {
    "verification_id": "string",
    "item_id": "number",
    "account_id": "string",
    "account_name": "string",
    "item_type": "string",
    "item_error": "password_protected_pdf|get_input_file_info_failed",
    "completed_at": "string (ISO 8601)",
    "verification_tag": "string"
  }
}
Campos:
  • item_error (opcional): Indica errores públicos relacionados con el procesamiento del ítem. Valores permitidos:
    • password_protected_pdf: El PDF subido está protegido con contraseña y no puede ser procesado.
    • get_input_file_info_failed: Falló la obtención de información del archivo de entrada.

verification_item.v2.internal_status_changed

Se dispara cuando un item específico dentro de una verificación ha cambiado su estado. Payload:
{
  "event_name": "verification_item.v2.internal_status_changed",
  "data": {
    "verification_id": "string",
    "item_id": "number",
    "item_type": "string",
    "item_error": "password_protected_pdf|get_input_file_info_failed",
    "internal_status": "string",
    "updated_at": "string (ISO 8601)",
    "account_name": "string",
    "account_id": "string",
    "verification_tag": "string"
  }
}
Campos:
  • item_error (opcional): Indica errores públicos relacionados con el procesamiento del ítem. Valores permitidos:
    • password_protected_pdf: El PDF subido está protegido con contraseña y no puede ser procesado.
    • get_input_file_info_failed: Falló la obtención de información del archivo de entrada.

verification_people.curp_search_completed

Cuándo se dispara: Este webhook se envía cuando el proceso de búsqueda de CURP para una persona de verificación ha finalizado. Esto puede ocurrir en los siguientes escenarios:
  1. Después de extraer accionistas mediante procesamiento de tipos de acta: Cuando se procesan documentos como actas constitutivas (ac_mx), actas de asamblea (aa_mx), o poderes notariales (pw_mx), y se extraen accionistas de estos documentos, Trébol realiza automáticamente una búsqueda de CURP para cada persona extraída. Una vez completada la búsqueda, se envía este webhook.
  2. Después de extraer personas desde items person_id: Cuando se procesa un item de tipo person_id (documentos de identidad como INE o pasaportes), y se crea o actualiza un registro de persona en la verificación, Trébol realiza una búsqueda de CURP para esa persona. Al finalizar la búsqueda, se envía este webhook.
Este webhook te permite estar al tanto de cuándo la información de CURP está disponible para las personas en una verificación, lo cual es útil para acceder a los datos de external_identities que contienen la información obtenida de RENAPO. Puedes usar el campo people_id para obtener a la persona en la respuesta del endpoint people.
Payload:
{
  "event_name": "verification_people.curp_search_completed",
  "data": {
    "verification_id": "5853393e-8cf7-4dc7-afd9-a92df69fff2b",
    "item_id": 32644,
    "people_id": 3921,
    "shareholder_id": 18590,
    "account_name": "trebol",
    "account_id": "212457cc-09bb-4308-b69b-f719e6f2eb03",
    "verification_tag": "personidgeneric-uuid"
  }
}
Campos:
  • verification_id (string): ID único de la verificación donde se actualizó la búsqueda de CURP.
  • item_id (number, opcional): ID del item que originó la búsqueda de CURP. Puede ser un item de tipo person_id o un item de tipo acta que extrajo personas.
  • people_id (number): ID único de la persona de verificación para la cual se completó la búsqueda de CURP.
  • people_error (string, opcional): Código de error si la búsqueda de CURP falló. Solo está presente cuando ocurre un error. Valores permitidos:
    • curp_scrapper_error: Error al extraer información del CURP desde el servicio externo.
    • curp_format_error: El formato del CURP proporcionado no es válido.
    • curp_service_unavailable: El servicio de búsqueda de CURP no está disponible.
  • people_error_message (string, opcional): Mensaje descriptivo del error. Solo está presente cuando people_error tiene un valor.
  • shareholder_id (number, opcional): ID del accionista relacionado, si la persona está asociada a un accionista en la verificación.
  • account_name (string): Nombre de la cuenta asociada a la verificación.
  • account_id (string): ID único de la cuenta.
  • verification_tag (string): Etiqueta personalizada de la verificación.

Obtener datos CURP

Una vez que recibas el webhook verification_people.curp_search_completed, puedes obtener los datos de CURP consultando el endpoint de personas.
1

Recibir el webhook

Extrae verification_id y people_id del payload del webhook:
{
  "event_name": "verification_people.curp_search_completed",
  "data": {
    "verification_id": "30b2bc30-275c-4cf1-98e1-cdb1f9cce3e2",
    "people_id": 2382
  }
}
2

Consultar el endpoint de personas

Usa el verification_id para llamar al endpoint de personas:
GET /v2/verifications/{verification_id}/people
3

Buscar la persona en full_list

En la respuesta JSON, localiza la persona cuyo people_id coincida con el del webhook dentro del array full_list. Ahí es donde encontrarás el objeto completo de la persona junto con sus identidades externas.
4

Acceder a los datos de CURP

Una vez que tengas la persona, puedes acceder a los datos de CURP en el objeto external_identities.curp:
{
  "external_identities": {
    "curp": {
      "success": true,
      "applicant_data": {
        "curp": "ROMC850315HDFRRS07",
        "names": "CARLOS ALBERTO",
        "gender": "HOMBRE",
        "birth_date": "1985-03-15",
        "birth_entity": "DISTRITO FEDERAL",
        "nationality": "MEXICO",
        "first_surname": "RODRIGUEZ",
        "second_surname": "MARTINEZ",
        "evidentiary_document": "Acta de nacimiento"
      },
      "evidentiary_document_data": {
        "act_number": "00452",
        "registry_date": "1985",
        "register_entity": "09 DISTRITO FEDERAL",
        "register_municipality": "014 BENITO JUAREZ"
      }
    }
  }
}