> ## Documentation Index
> Fetch the complete documentation index at: https://gotrebol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

# 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.

<Steps>
  <Step title="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](https://docs.gotrebol.com/api-reference).

    <Warning>
      **¡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.
    </Warning>
  </Step>

  <Step title="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.
  </Step>

  <Step title="Configurar en tu aplicación">
    Almacena el secreto de forma segura en tu aplicación:

    ```bash Environment Variables theme={"dark"}
    # .env
    WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXXXX
    ```

    ```javascript Configuration theme={"dark"}
    // config.js
    const config = {
      webhookSecret: process.env.WEBHOOK_SECRET,
    };
    ```
  </Step>
</Steps>

### Gestión de secretos

<AccordionGroup>
  <Accordion title="¿Qué hacer si pierdes tu secreto?">
    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
  </Accordion>

  <Accordion title="Rotación de secretos">
    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
  </Accordion>

  <Accordion title="Secretos en diferentes entornos">
    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.
  </Accordion>
</AccordionGroup>

## 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

<Warning>
  Siempre verifica las firmas de webhook en producción. Nunca proceses webhooks
  sin verificar su autenticidad.
</Warning>

### Ejemplos de implementación

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  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');
  });

  ```

  ```python Python theme={"dark"}
  import hmac
  import hashlib
  import time
  from typing import Optional

  def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
      """
      Verifica la firma HMAC-SHA256 de un webhook de Trébol.

      Args:
          payload: El cuerpo del webhook como string
          signature: El header Trebol-Signature
          secret: El secreto del webhook

      Returns:
          True si la firma es válida, False en caso contrario
      """
      try:
          # Extraer elementos del header
          elements = signature.split(',')
          timestamp = None
          v1 = None

          for element in elements:
              if element.startswith('t='):
                  timestamp = element.split('=')[1]
              elif element.startswith('v1='):
                  v1 = element.split('=')[1]

          if not timestamp or not v1:
              return False

          # Construir payload para firmar
          payload_to_sign = f"{timestamp}.{payload}"

          # Generar firma esperada
          expected_signature = hmac.new(
              secret.encode('utf-8'),
              payload_to_sign.encode('utf-8'),
              hashlib.sha256
          ).hexdigest()

          # Comparación de tiempo constante
          return hmac.compare_digest(v1, expected_signature)

      except Exception:
          return False

  # Ejemplo de uso en Flask
  from flask import Flask, request, abort

  app = Flask(__name__)

  @app.route('/webhook', methods=['POST'])
  def webhook():
      signature = request.headers.get('Trebol-Signature')
      payload = request.get_data(as_text=True)

      if not verify_webhook_signature(payload, signature, app.config['WEBHOOK_SECRET']):
          abort(401)

      # Procesar webhook...
      return 'OK', 200
  ```

  ```java Java theme={"dark"}
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import java.nio.charset.StandardCharsets;
  import java.security.InvalidKeyException;
  import java.security.NoSuchAlgorithmException;
  import java.util.Arrays;

  public class WebhookSignatureVerifier {

      /**
       * Verifica la firma HMAC-SHA256 de un webhook de Trébol.
       *
       * @param payload El cuerpo del webhook como string
       * @param signature El header Trebol-Signature
       * @param secret El secreto del webhook
       * @return true si la firma es válida, false en caso contrario
       */
      public static boolean verifyWebhookSignature(String payload, String signature, String secret) {
          try {
              // Extraer elementos del header
              String[] elements = signature.split(",");
              String timestamp = null;
              String v1 = null;

              for (String element : elements) {
                  if (element.startsWith("t=")) {
                      timestamp = element.split("=")[1];
                  } else if (element.startsWith("v1=")) {
                      v1 = element.split("=")[1];
                  }
              }

              if (timestamp == null || v1 == null) {
                  return false;
              }

              // Construir payload para firmar
              String payloadToSign = timestamp + "." + payload;

              // Generar firma esperada
              Mac mac = Mac.getInstance("HmacSHA256");
              SecretKeySpec secretKeySpec = new SecretKeySpec(
                  secret.getBytes(StandardCharsets.UTF_8),
                  "HmacSHA256"
              );
              mac.init(secretKeySpec);

              byte[] expectedSignatureBytes = mac.doFinal(
                  payloadToSign.getBytes(StandardCharsets.UTF_8)
              );
              String expectedSignature = bytesToHex(expectedSignatureBytes);

              // Comparación de tiempo constante
              return constantTimeEquals(v1, expectedSignature);

          } catch (NoSuchAlgorithmException | InvalidKeyException e) {
              return false;
          }
      }

      private static String bytesToHex(byte[] bytes) {
          StringBuilder result = new StringBuilder();
          for (byte b : bytes) {
              result.append(String.format("%02x", b));
          }
          return result.toString();
      }

      private static boolean constantTimeEquals(String a, String b) {
          if (a.length() != b.length()) {
              return false;
          }

          byte[] aBytes = a.getBytes(StandardCharsets.UTF_8);
          byte[] bBytes = b.getBytes(StandardCharsets.UTF_8);

          return Arrays.equals(aBytes, bBytes);
      }
  }

  // Ejemplo de uso en Spring Boot
  @RestController
  public class WebhookController {

      @Value("${webhook.secret}")
      private String webhookSecret;

      @PostMapping("/webhook")
      public ResponseEntity<String> webhook(@RequestBody String payload,
                                          @RequestHeader("Trebol-Signature") String signature) {

          if (!WebhookSignatureVerifier.verifyWebhookSignature(payload, signature, webhookSecret)) {
              return ResponseEntity.status(401).body("Invalid signature");
          }

          // Procesar webhook...
          return ResponseEntity.ok("OK");
      }
  }
  ```
</CodeGroup>

<Tip>
  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.
</Tip>

## Direcciones IP de origen

Trébol envía todos los webhooks desde las siguientes direcciones IP:

* `35.170.236.123`
* `54.162.134.233`

<Info>
  Estas IPs son estáticas y no cambiarán. Puedes configurar tu firewall para
  permitir únicamente estas direcciones si necesitas restricciones adicionales
  de seguridad.
</Info>

<Warning>
  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.
</Warning>

## 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

| Reintento | Tiempo de espera | Tiempo acumulado |
| --------- | ---------------- | ---------------- |
| 1         | 30 segundos      | 30 segundos      |
| 2         | 60 segundos      | 1.5 minutos      |
| 3         | 120 segundos     | 3.5 minutos      |
| 4         | 240 segundos     | 7.5 minutos      |
| 5         | 480 segundos     | 15.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

<Warning>
  **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.
</Warning>

### 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](#mejores-prácticas))
3. **Maneja errores internamente** sin retornar códigos de error HTTP

```java theme={"dark"}
@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");
}
```

<Tip>
  Si necesitas indicar que hubo un problema pero no quieres reintentos, considera retornar `200 OK` y manejar el error internamente con alertas o logs.
</Tip>

## 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.

<Warning>
  **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.
</Warning>

**❌ Incorrecto** - Procesar antes de responder:

```java theme={"dark"}
@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:

```java theme={"dark"}
@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.

```java theme={"dark"}
@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");
}
```

<Tip>
  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.
</Tip>

### 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.

<AccordionGroup>
  <Accordion title="Ejemplo con RabbitMQ/SQS">
    ```python theme={"dark"}
    @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
    ```
  </Accordion>

  <Accordion title="Ejemplo con Celery (Python)">
    ```python theme={"dark"}
    @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)
    ```
  </Accordion>
</AccordionGroup>

### 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:**

```json theme={"dark"}
{
  "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:**

```json theme={"dark"}
{
  "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.v2.extraction_completed`

Se dispara cuando todos los items de extracción dentro de una verificación han completado su proceso de extracción. Esto ocurre antes de que la verificación sea marcada como finalizada (`verification.v2.finished`), permitiendo acceder a los datos extraídos de forma anticipada.

**Payload:**

```json theme={"dark"}
{
  "event_name": "verification.v2.extraction_completed",
  "data": {
    "verification_id": "string",
    "account_id": "string",
    "account_name": "string",
    "created_at": "string (ISO 8601)",
    "status": "string",
    "verification_tag": "string",
    "extraction_completed_at": "string (ISO 8601)"
  }
}
```

**Campos:**

* `extraction_completed_at` (string): Timestamp ISO 8601 de cuando se completó la extracción de todos los items.

<h3 id="verification-v2-document_status_updated">
  `verification.v2.document_status_updated`
</h3>

Se dispara cada vez que cambia el estado documental (`documents_status`) de una
verificación; por ejemplo, cuando se completa el expediente (`full_upload`).
Aplica a cualquier verificación, sea creada por API o vía el widget.
Para recibirlo, incluye `"verification.v2.document_status_updated"` en el array
`events` al [crear tu webhook](#crear-y-gestionar-webhooks-por-api):

```bash theme={"dark"}
curl -X POST "https://api.gotrebol.com/v2/webhooks" \
     -H "x-api-key: {api_key}" \
     -H "Content-Type: application/json" \
     -d '{
       "url": "https://tu-app.com/webhooks/trebol",
       "events": ["verification.v2.document_status_updated"],
       "description": "Transiciones del estado documental"
     }'
```

También se dispara por las cargas posteriores a una reapertura. Una
verificación finalizada se reabre automáticamente cuando le agregas nuevos
items o personas clave por API; Trébol también puede reabrirla en flujos
operativos internos. Al reabrirse, su `status` vuelve a `pending`.

A diferencia de los demás eventos `verification.v2.*`, el payload de este
evento no incluye `account_name`.

**Payload:**

```json theme={"dark"}
{
  "event_name": "verification.v2.document_status_updated",
  "data": {
    "verification_id": "string",
    "account_id": "string",
    "created_at": "string (ISO 8601)",
    "status": "string",
    "verification_tag": "string",
    "documents_status": "pending_upload | partial_upload | pending_external | full_upload",
    "previous_documents_status": "pending_upload | partial_upload | pending_external | full_upload | null",
    "updated_at": "string (ISO 8601)"
  }
}
```

**Ejemplos** (escenarios alternativos del mismo payload):

<CodeGroup>
  ```json Expediente completo (partial_upload → full_upload) theme={"dark"}
  {
    "event_name": "verification.v2.document_status_updated",
    "data": {
      "verification_id": "c8dc41fc-c477-404e-aff7-b9074f86d6d1",
      "account_id": "66b4b55e-0e4f-42ec-9727-2bff88965562",
      "created_at": "2026-07-01T15:04:12.318Z",
      "status": "pending",
      "verification_tag": "cliente-12345",
      "documents_status": "full_upload",
      "previous_documents_status": "partial_upload",
      "updated_at": "2026-07-01T16:22:40.907Z"
    }
  }
  ```

  ```json Regresión tras reapertura (full_upload → partial_upload) theme={"dark"}
  {
    "event_name": "verification.v2.document_status_updated",
    "data": {
      "verification_id": "c8dc41fc-c477-404e-aff7-b9074f86d6d1",
      "account_id": "66b4b55e-0e4f-42ec-9727-2bff88965562",
      "created_at": "2026-07-01T15:04:12.318Z",
      "status": "pending",
      "verification_tag": "cliente-12345",
      "documents_status": "partial_upload",
      "previous_documents_status": "full_upload",
      "updated_at": "2026-07-03T09:14:02.113Z"
    }
  }
  ```

  ```json Primer evento (previous_documents_status null) theme={"dark"}
  {
    "event_name": "verification.v2.document_status_updated",
    "data": {
      "verification_id": "c8dc41fc-c477-404e-aff7-b9074f86d6d1",
      "account_id": "66b4b55e-0e4f-42ec-9727-2bff88965562",
      "created_at": "2026-07-01T15:04:12.318Z",
      "status": "pending",
      "verification_tag": "cliente-12345",
      "documents_status": "partial_upload",
      "previous_documents_status": null,
      "updated_at": "2026-07-01T15:18:03.442Z"
    }
  }
  ```
</CodeGroup>

**Campos:**

<ResponseField name="verification_id" type="string">
  ID único de la verificación asociada al evento.
</ResponseField>

<ResponseField name="account_id" type="string">
  ID único de la cuenta asociada a la verificación.
</ResponseField>

<ResponseField name="created_at" type="string">
  Timestamp ISO 8601 de creación de la verificación.
</ResponseField>

<ResponseField name="status" type="string">
  Estado del ciclo de vida de la verificación al momento de generar el evento:
  `pending`, `finished`, `error` o `pending_validation`. Es una dimensión
  independiente de `documents_status`: este evento puede llegar con cualquier
  `status` (por ejemplo, con `pending` de nuevo después de una reapertura).
</ResponseField>

<ResponseField name="verification_tag" type="string">
  Valor personalizado (`tag`) que enviaste al crear la verificación. Úsalo para
  correlacionar el evento con tu sistema.
</ResponseField>

<ResponseField name="documents_status" type="string">
  Nuevo estado documental de la verificación, el mismo que se describe en
  [Estados del expediente](/docs/guia-devs/crear-verificaciones/via-widget/estados-expediente).
  Valores posibles:

  * `pending_upload`: aún no se sube ningún documento requerido.
  * `partial_upload`: se subieron algunos documentos requeridos, pero faltan otros.
  * `pending_external`: todos los documentos requeridos están cargados, pero hay
    pasos externos pendientes, como formularios o beneficiarios finales (UBOs).
  * `full_upload`: el expediente quedó completo.

  El estado puede retroceder, por ejemplo de `full_upload` a `partial_upload`.
  Esto ocurre cuando una reapertura o un cambio en los requisitos vuelve a
  dejar documentos pendientes.
</ResponseField>

<ResponseField name="previous_documents_status" type="string | null">
  Estado documental anterior a la transición. Es `null` cuando la verificación
  aún no tenía un estado documental.
</ResponseField>

<ResponseField name="updated_at" type="string">
  Timestamp ISO 8601 del momento de la transición.
</ResponseField>

<Tip>
  Si solo te interesa saber cuándo se completa el expediente, filtra los
  eventos con `documents_status: "full_upload"`. El primer evento con ese valor
  marca el momento en que el expediente quedó completo por primera vez. En ese
  evento, `updated_at` − `created_at` mide el tiempo desde que se creó la
  verificación hasta que el expediente quedó completo (el tiempo del
  prospecto). No mide el tiempo de carga de un documento individual.
</Tip>

Este evento es independiente de `verification.v2.finished`: `full_upload`
indica que el expediente está completo, mientras que `finished` indica que
Trébol terminó el análisis. Lo habitual es que el expediente se complete
primero (con `status: pending`) y la verificación finalice después. Usa este
evento para medir la carga documental y `verification.v2.finished` para saber
cuándo leer los resultados.

Como con el resto de los eventos, las entregas pueden llegar fuera de orden.
Para reconstruir la secuencia de un `verification_id`, ordena sus eventos por
`updated_at` y aplica siempre el más reciente. Si llega un evento con
`updated_at` anterior al último que aplicaste, descártalo. Como validación
adicional, el `previous_documents_status` de cada evento debe coincidir con el
`documents_status` del evento anterior en la cadena.

### `verification_item.v2.completed`

Se dispara cuando un item específico dentro de una verificación ha sido completado.

**Payload:**

```json theme={"dark"}
{
  "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:**

```json theme={"dark"}
{
  "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_item.v2.extraction_completed`

Se dispara cuando un item específico dentro de una verificación ha completado su proceso de extracción de información. Esto permite acceder a los datos extraídos del documento antes de que el item sea marcado como completado.

**Payload:**

```json theme={"dark"}
{
  "event_name": "verification_item.v2.extraction_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",
    "extraction_completed_at": "string (ISO 8601)",
    "success": "boolean",
    "verification_tag": "string"
  }
}
```

**Campos:**

* `extraction_completed_at` (string): Timestamp ISO 8601 de cuando se completó la extracción del item.
* `success` (boolean): Indica si la extracción fue exitosa.
* `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.

<h3 id="verification-v2-label_added">
  `verification.v2.label_added`
</h3>

Trébol dispara este evento cuando asignas una etiqueta a una verificación o
cuando Trébol la asigna automáticamente. Las etiquetas pertenecen al catálogo de
la cuenta y te permiten marcar condiciones de negocio sobre una verificación.

**Payload:**

```json theme={"dark"}
{
  "event_name": "verification.v2.label_added",
  "data": {
    "verification_id": "string",
    "account_id": "string",
    "account_name": "string",
    "created_at": "string (ISO 8601)",
    "status": "string",
    "verification_tag": "string",
    "entity": {
      "type": "label",
      "label": "string",
      "display_name": "string",
      "category": "string | null"
    },
    "comment": "string | null",
    "added_at": "string (ISO 8601)"
  }
}
```

**Campos:**

<ResponseField name="verification_id" type="string">
  ID único de la verificación asociada al evento.
</ResponseField>

<ResponseField name="account_id" type="string">
  ID único de la cuenta asociada a la verificación.
</ResponseField>

<ResponseField name="account_name" type="string">
  Nombre de la cuenta asociada a la verificación.
</ResponseField>

<ResponseField name="created_at" type="string">
  Timestamp ISO 8601 de creación de la verificación.
</ResponseField>

<ResponseField name="status" type="string">
  Estado de la verificación al momento de generar el evento, no de la etiqueta.
  Valores posibles: `pending`, `finished`, `error`, `pending_validation`.
</ResponseField>

<ResponseField name="verification_tag" type="string">
  Valor personalizado (`tag`) que enviaste al crear la verificación. Úsalo para
  correlacionar el evento con tu sistema.
</ResponseField>

<ResponseField name="entity.type" type="string">
  Discriminador del tipo de entidad asociada al evento. Para estos webhooks
  siempre es `"label"`.
</ResponseField>

<ResponseField name="entity.label" type="string">
  Clave interna no traducible de la etiqueta.
</ResponseField>

<ResponseField name="entity.display_name" type="string">
  Nombre legible de la etiqueta.
</ResponseField>

<ResponseField name="entity.category" type="string | null">
  Categoría de la etiqueta cuando el catálogo la define. Es `null` si la
  etiqueta no tiene categoría.
</ResponseField>

<ResponseField name="comment" type="string | null">
  Comentario asociado a la asignación de la etiqueta.
</ResponseField>

<ResponseField name="added_at" type="string">
  Timestamp ISO 8601 de cuando se asignó la etiqueta.
</ResponseField>

<Warning>
  Cuando actualizas el comentario de una etiqueta ya asignada, Trébol genera dos
  eventos: `verification.v2.label_removed` con el comentario anterior y
  `verification.v2.label_added` con el comentario nuevo. La entrega puede llegar
  fuera de orden. Procesa ambos eventos de forma idempotente y concilia por
  `verification_id`, `entity.label` y timestamps.
</Warning>

<Note>
  El payload no incluye `reason` ni `correlation_id`. Trata el par
  `label_removed` + `label_added` para el mismo `entity.label` como una
  conciliación del estado final de esa etiqueta.
</Note>

<h3 id="verification-v2-label_removed">
  `verification.v2.label_removed`
</h3>

Trébol dispara este evento cuando remueves una etiqueta de una verificación o
cuando Trébol remueve una asignación automática. También se emite como parte de
la actualización del comentario de una etiqueta existente.

**Payload:**

```json theme={"dark"}
{
  "event_name": "verification.v2.label_removed",
  "data": {
    "verification_id": "string",
    "account_id": "string",
    "account_name": "string",
    "created_at": "string (ISO 8601)",
    "status": "string",
    "verification_tag": "string",
    "entity": {
      "type": "label",
      "label": "string",
      "display_name": "string",
      "category": "string | null"
    },
    "comment": "string | null",
    "removed_at": "string (ISO 8601)"
  }
}
```

**Campos:**

<ResponseField name="verification_id" type="string">
  ID único de la verificación asociada al evento.
</ResponseField>

<ResponseField name="account_id" type="string">
  ID único de la cuenta asociada a la verificación.
</ResponseField>

<ResponseField name="account_name" type="string">
  Nombre de la cuenta asociada a la verificación.
</ResponseField>

<ResponseField name="created_at" type="string">
  Timestamp ISO 8601 de creación de la verificación.
</ResponseField>

<ResponseField name="status" type="string">
  Estado de la verificación al momento de generar el evento, no de la etiqueta.
  Valores posibles: `pending`, `finished`, `error`, `pending_validation`.
</ResponseField>

<ResponseField name="verification_tag" type="string">
  Valor personalizado (`tag`) que enviaste al crear la verificación. Úsalo para
  correlacionar el evento con tu sistema.
</ResponseField>

<ResponseField name="entity.type" type="string">
  Discriminador del tipo de entidad asociada al evento. Para estos webhooks
  siempre es `"label"`.
</ResponseField>

<ResponseField name="entity.label" type="string">
  Clave interna no traducible de la etiqueta.
</ResponseField>

<ResponseField name="entity.display_name" type="string">
  Nombre legible de la etiqueta.
</ResponseField>

<ResponseField name="entity.category" type="string | null">
  Categoría de la etiqueta cuando el catálogo la define. Es `null` si la
  etiqueta no tiene categoría.
</ResponseField>

<ResponseField name="comment" type="string | null">
  Comentario asociado a la etiqueta al momento de removerla.
</ResponseField>

<ResponseField name="removed_at" type="string">
  Timestamp ISO 8601 de cuando se removió la etiqueta.
</ResponseField>

<Note>
  Este evento también se emite cuando actualizas el comentario de una etiqueta.
  En ese caso, Trébol genera después `verification.v2.label_added` con el
  comentario nuevo. Como la entrega puede llegar fuera de orden, concilia por
  `verification_id`, `entity.label` y timestamps.
</Note>

<h3 id="verification_people-curp_search_completed">
  `verification_people.curp_search_completed`
</h3>

**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.

<Info>
  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.
</Info>

**Payload:**

<CodeGroup>
  ```json Éxito theme={"dark"}
  {
    "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"
    }
  }
  ```

  ```json Error theme={"dark"}
  {
    "event_name": "verification_people.curp_search_completed",
    "data": {
      "verification_id": "c0361bc7-9318-4b09-8186-4ee451cc569f",
      "item_id": 32640,
      "people_error": "curp_format_error",
      "people_error_message": "CURP is required and must be a string",
      "people_id": 3908,
      "account_name": "trebol",
      "account_id": "212457cc-09bb-4308-b69b-f719e6f2eb03",
      "verification_tag": "personidgeneric-uuid"
    }
  }
  ```
</CodeGroup>

**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.

<Steps>
  <Step title="Recibir el webhook">
    Extrae `verification_id` y `people_id` del payload del webhook:

    ```json theme={"dark"}
    {
      "event_name": "verification_people.curp_search_completed",
      "data": {
        "verification_id": "30b2bc30-275c-4cf1-98e1-cdb1f9cce3e2",
        "people_id": 2382
      }
    }
    ```
  </Step>

  <Step title="Consultar el endpoint de personas">
    Usa el `verification_id` para llamar al endpoint de personas:

    ```bash theme={"dark"}
    GET /v2/verifications/{verification_id}/people
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="Acceder a los datos de CURP">
    Una vez que tengas la persona, lee los datos de CURP en el objeto `external_identities.curp`:

    ```json theme={"dark"}
    {
      "external_identities": {
        "curp": {
          "success": true,
          "message": null,
          "search_number": "ROMC850315HDFRRS07",
          "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"
          },
          "curp_file": "https://files.gotrebol.com/mx/curps/ROMC850315HDFRRS07_file.pdf?Expires=…&Signature=…"
        }
      }
    }
    ```

    <Info>
      La URL en `curp_file` es firmada e incluye `Expires=…`, por lo que caduca. Si necesitas reabrir el PDF después de la expiración, vuelve a consultar el endpoint de personas para obtener una URL renovada.
    </Info>

    <Info>
      Cuando `success` es `false`, `applicant_data` y `evidentiary_document_data` pueden llegar en `null` y `message` describe la causa. Cuando además llega el campo `people_error` en el webhook, consulta la [referencia de errores en consultas públicas](/docs/guia-devs/referencia/errores-consultas-publicas#renapo) para decidir si reintentas.
    </Info>
  </Step>
</Steps>
