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

# Integração de Webhooks

> Receber notificações em tempo real quando o status de verificação KYC mudar — na API KYC da gu1 para fluxos de verificação de identidade.

## Resumo

Os webhooks permitem que você receba notificações em tempo real quando o status de verificação KYC mudar. Gu1 envia automaticamente requisições HTTP POST para seu endpoint webhook configurado sempre que o status de uma validação for atualizado.

## Por Que Usar Webhooks?

<CardGroup cols={2}>
  <Card title="Atualizações em Tempo Real" icon="bolt">
    Receba notificações instantâneas quando o status de verificação mudar
  </Card>

  <Card title="Eficiente" icon="gauge-high">
    Não é necessário consultar a API repetidamente
  </Card>

  <Card title="Fluxos Automáticos" icon="robot">
    Atualize automaticamente contas de usuário com base nos resultados de verificação
  </Card>

  <Card title="Melhor UX" icon="face-smile">
    Notifique os clientes imediatamente após a verificação
  </Card>
</CardGroup>

## Eventos de Webhook

Gu1 envia webhooks para os seguintes eventos de validação KYC:

| Tipo de Evento               | Descrição                    | Quando é Disparado                       |
| ---------------------------- | ---------------------------- | ---------------------------------------- |
| `kyc.validation_created`     | Sessão de validação criada   | Quando você cria uma nova validação KYC  |
| `kyc.validation_in_progress` | Cliente iniciou verificação  | Cliente começa o processo de verificação |
| `kyc.validation_approved`    | Verificação aprovada         | Identidade verificada com sucesso        |
| `kyc.validation_rejected`    | Verificação rejeitada        | Verificação de identidade falhou         |
| `kyc.validation_abandoned`   | Cliente abandonou o processo | Cliente abandonou sem completar          |
| `kyc.validation_expired`     | Sessão de validação expirou  | Sessão expirou (tipicamente após 7 dias) |

## Configurar Webhooks

### Passo 1: Configurar Webhook no Dashboard

Configure a URL do seu webhook no dashboard do Gu1:

1. **Navegar para Configurações de Webhooks**
   * Faça login no seu dashboard do Gu1
   * Vá para **Configurações** → **Webhooks**

2. **Criar Novo Webhook**
   * Clique em **Adicionar Webhook** ou **Criar Webhook**
   * Insira um nome descritivo (ex., "Webhook KYC Produção")
   * Insira a URL do seu webhook (deve ser HTTPS): `https://seuapp.com/webhooks/kyc`

3. **Selecionar Ambiente**
   * Escolha **Sandbox** para testes
   * Escolha **Produção** para eventos ao vivo

4. **Inscrever-se em Eventos**
   * Selecione todos os eventos KYC ou específicos:
     * `kyc.validation_created`
     * `kyc.validation_in_progress`
     * `kyc.validation_approved`
     * `kyc.validation_rejected`
     * `kyc.validation_abandoned`
     * `kyc.validation_expired`

5. **Copiar o secret gerado (não é preciso fornecer um)**
   * Você **não** precisa criar nem fornecer um secret de webhook. Ao salvar o webhook, o Gu1 **gera automaticamente** um secret.
   * **Copie e guarde o secret** quando for exibido — você precisará dele no seu servidor para verificar o header `X-Webhook-Signature`. Não será possível vê-lo novamente (mas você pode regenerá-lo depois nas configurações do webhook).

6. **Ativar o Webhook**
   * Ative o webhook marcando-o como **Habilitado**
   * Clique em **Salvar**

<Note>
  Você pode criar webhooks separados para os ambientes sandbox e produção com URLs diferentes.
</Note>

### Passo 2: Criar um Endpoint de Webhook

Crie um endpoint na sua aplicação para receber requisições POST de webhook:

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const express = require('express');
  const crypto = require('crypto');

  const app = express();

  // IMPORTANTE: Usar body raw para verificação de assinatura
  app.use(express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString('utf8');
    }
  }));

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

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

      // Extrair dados do webhook
      const { event, timestamp, organizationId, payload } = req.body;

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

      // Processar o webhook de acordo com o tipo de evento
      await handleKycWebhook(event, payload);

      // Retornar 200 para confirmar recepção
      res.status(200).json({
        success: true,
        message: 'Webhook recebido'
      });
    } catch (error) {
      console.error('Erro no webhook:', error);
      // Ainda assim retornar 200 para prevenir tentativas
      res.status(200).json({
        success: false,
        error: error.message
      });
    }
  });

  // Verificar assinatura 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;

    // Atualizar seu banco de dados com o ID de validação do Gu1
    await db.updateEntity(entity.externalId, {
      kycValidationId: validationId,
      kycStatus: status,
      lastUpdated: new Date()
    });

    // Realizar ações de acordo com o tipo de evento
    switch (event) {
      case 'kyc.validation_created':
        console.log('Validação KYC criada para:', entity.name);
        break;

      case 'kyc.validation_in_progress':
        await notifyCustomer(entity.externalId, 'verificacao-iniciada');
        break;

      case 'kyc.validation_approved':
        // Extrair dados 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, 'verificacao-aprovada');
        break;

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

        await notifyCustomer(entity.externalId, 'verificacao-rejeitada');
        break;

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

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

  ```python Python (Flask) theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib
  import json
  import logging
  import os

  app = Flask(__name__)

  @app.route('/webhooks/kyc', methods=['POST'])
  def kyc_webhook():
      try:
          # Obter assinatura do header
          signature = request.headers.get('X-Webhook-Signature')
          webhook_secret = os.getenv('GUENO_WEBHOOK_SECRET')

          # Obter body raw para verificação de assinatura
          raw_body = request.get_data(as_text=True)

          # Verificar assinatura
          if not verify_signature(raw_body, signature, webhook_secret):
              logging.error('Assinatura de webhook inválida')
              return jsonify({'error': 'Assinatura inválida'}), 401

          # Fazer parsing do payload
          payload = request.json
          event = payload.get('event')
          data = payload.get('payload')

          logging.info(f'Webhook KYC recebido: {event}')

          # Processar o webhook
          handle_kyc_webhook(event, data)

          return jsonify({
              'success': True,
              'message': 'Webhook recebido'
          }), 200

      except Exception as e:
          logging.error(f'Erro no webhook: {e}')
          # Ainda assim retornar 200 para prevenir tentativas
          return jsonify({
              'success': False,
              'error': str(e)
          }), 200

  def verify_signature(raw_body, signature, secret):
      """Verificar assinatura HMAC SHA-256"""
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          raw_body.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      return signature == expected_signature

  def handle_kyc_webhook(event, data):
      validation_id = data['validationId']
      entity_id = data['entityId']
      entity = data['entity']
      status = data['status']

      # Atualizar banco de dados
      db.update_entity(
          external_id=entity['externalId'],
          kyc_validation_id=validation_id,
          kyc_status=status,
          last_updated=datetime.now()
      )

      # Lidar com diferentes eventos
      if 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'], 'verificacao-aprovada')

      elif event == 'kyc.validation_rejected':
          notify_customer(entity['externalId'], 'verificacao-rejeitada')

      elif event == 'kyc.validation_abandoned':
          notify_customer(entity['externalId'], 'verificacao-incompleta')
  ```

  ```go Go (Gin) theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "io/ioutil"
      "log"
      "net/http"
      "os"

      "github.com/gin-gonic/gin"
  )

  type WebhookPayload struct {
      Event          string                 `json:"event"`
      Timestamp      string                 `json:"timestamp"`
      OrganizationID string                 `json:"organizationId"`
      Payload        map[string]interface{} `json:"payload"`
  }

  func main() {
      r := gin.Default()
      r.POST("/webhooks/kyc", handleKYCWebhook)
      r.Run(":8080")
  }

  func handleKYCWebhook(c *gin.Context) {
      // Ler body raw para verificação de assinatura
      rawBody, err := ioutil.ReadAll(c.Request.Body)
      if err != nil {
          c.JSON(500, gin.H{"error": "Erro ao ler body"})
          return
      }

      // Verificar assinatura
      signature := c.GetHeader("X-Webhook-Signature")
      webhookSecret := os.Getenv("GUENO_WEBHOOK_SECRET")

      if !verifySignature(rawBody, signature, webhookSecret) {
          log.Println("Assinatura de webhook inválida")
          c.JSON(401, gin.H{"error": "Assinatura inválida"})
          return
      }

      // Fazer parsing do payload
      var payload WebhookPayload
      if err := json.Unmarshal(rawBody, &payload); err != nil {
          c.JSON(400, gin.H{"error": "JSON inválido"})
          return
      }

      log.Printf("Webhook recebido: %s", payload.Event)

      // Processar webhook
      handleWebhook(payload)

      c.JSON(200, gin.H{
          "success": true,
          "message": "Webhook recebido",
      })
  }

  func verifySignature(rawBody []byte, signature, secret string) bool {
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(rawBody)
      expectedSignature := hex.EncodeToString(mac.Sum(nil))

      return signature == expectedSignature
  }

  func handleWebhook(payload WebhookPayload) {
      switch payload.Event {
      case "kyc.validation_approved":
          // Lidar com aprovação
          log.Println("KYC aprovado")
      case "kyc.validation_rejected":
          // Lidar com rejeição
          log.Println("KYC rejeitado")
      // ... lidar com outros eventos
      }
  }
  ```
</CodeGroup>

### Passo 3: Tornar seu Endpoint Publicamente Acessível

Seu endpoint de webhook deve:

* Ser **publicamente acessível** via HTTPS
* **Poder receber requisições POST**
* **Retornar código de status 200** rapidamente (dentro de 30 segundos)

<Note>
  Para desenvolvimento local, use ferramentas como [ngrok](https://ngrok.com/) para criar uma URL pública que faça túnel para seu servidor local.
</Note>

## Segurança: Verificar Assinaturas de Webhook

**Sempre verifique as assinaturas de webhook** para garantir que as requisições vêm do Gu1.

### Como Funciona a Verificação de Assinaturas

1. Gu1 gera uma assinatura HMAC SHA-256 do payload do webhook usando seu segredo
2. A assinatura é enviada no header `X-Webhook-Signature`
3. Seu servidor recalcula a assinatura usando o mesmo segredo
4. Compara as assinaturas - se coincidirem, o webhook é autêntico

### Exemplos de Verificação de Assinatura

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

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

    return signature === expectedSignature;
  }

  // No seu 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: 'Assinatura inválida' });
    }

    // Processar webhook...
  });
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_signature(raw_body: str, signature: str, secret: str) -> bool:
      expected_signature = hmac.new(
          secret.encode('utf-8'),
          raw_body.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      return signature == expected_signature

  # Na sua rota Flask:
  @app.route('/webhooks/kyc', methods=['POST'])
  def webhook():
      signature = request.headers.get('X-Webhook-Signature')
      secret = os.getenv('GUENO_WEBHOOK_SECRET')
      raw_body = request.get_data(as_text=True)

      if not verify_signature(raw_body, signature, secret):
          return jsonify({'error': 'Assinatura inválida'}), 401

      # Processar webhook...
  ```
</CodeGroup>

<Warning>
  Nunca omita a verificação de assinatura em produção. Sem ela, qualquer pessoa pode enviar webhooks falsos para seu endpoint.
</Warning>

## Headers HTTP

Cada requisição de webhook inclui estes headers:

| Header                | Descrição                     | Exemplo                                |
| --------------------- | ----------------------------- | -------------------------------------- |
| `Content-Type`        | Sempre `application/json`     | `application/json`                     |
| `X-Webhook-Event`     | Tipo de evento                | `kyc.validation_approved`              |
| `X-Webhook-ID`        | ID de configuração do webhook | `550e8400-e29b-41d4-a716-446655440000` |
| `X-Webhook-Timestamp` | Timestamp ISO 8601            | `2025-01-15T10:30:00.000Z`             |
| `X-Webhook-Signature` | Assinatura HMAC SHA-256       | `abc123...`                            |

## Estrutura do Payload de Webhook

Todos os webhooks seguem esta estrutura padrão:

```json theme={null}
{
  "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 do evento
  }
}
```

### Campos Comuns do Payload

<ResponseField name="event" type="string">
  O tipo de evento (ex., `kyc.validation_approved`)
</ResponseField>

<ResponseField name="timestamp" type="string">
  Timestamp ISO 8601 quando o evento ocorreu
</ResponseField>

<ResponseField name="organizationId" type="string">
  Seu ID de organização
</ResponseField>

<ResponseField name="payload.validationId" type="string">
  O ID de validação KYC no Gu1
</ResponseField>

<ResponseField name="payload.entityId" type="string">
  O ID da entidade (pessoa) sendo verificada
</ResponseField>

<ResponseField name="payload.entity" type="object">
  Informações da entidade incluindo seu `externalId` para fácil busca
</ResponseField>

<ResponseField name="payload.status" type="string">
  Status atual de validação: `pending`, `in_progress`, `in_review`, `approved`, `rejected`, `abandoned`, `expired`, `cancelled`
</ResponseField>

## Payloads Específicos por Evento

### kyc.validation\_created

Enviado quando uma nova validação KYC é criada.

```json theme={null}
{
  "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 quando um cliente inicia o processo de verificação.

```json theme={null}
{
  "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 quando a verificação é completada com sucesso.

```json theme={null}
{
  "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": {
      "status": "Approved",
      "workflow_type": "standard",
      "session_id": "7c0fa22d-0cfa-4a78-8090-7c842397e788",
      "session_number": 921,
      "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH", "IP_ANALYSIS"],
      "images": {
        "documentFront": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "documentBack": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
        "selfie": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg"
      },
      "id_verification": {
        "status": "Approved",
        "node_id": "feature_ocr",
        "document_type": "Passport",
        "document_number": "AB123456",
        "first_name": "John",
        "last_name": "Doe",
        "full_name": "John Doe",
        "date_of_birth": "1990-05-20",
        "nationality": "US",
        "gender": "M",
        "age": 35,
        "issuing_state": "US",
        "expiration_date": "2030-05-20",
        "date_of_issue": "2020-05-20",
        "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "back_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
        "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": [],
        "matches": []
      },
      "id_verifications": [
        {
          "status": "Approved",
          "node_id": "feature_ocr",
          "document_type": "Passport",
          "document_number": "AB123456",
          "first_name": "John",
          "last_name": "Doe",
          "full_name": "John Doe",
          "date_of_birth": "1990-05-20",
          "nationality": "US",
          "gender": "M",
          "age": 35,
          "issuing_state": "US",
          "expiration_date": "2030-05-20",
          "date_of_issue": "2020-05-20",
          "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "back_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-back.jpg",
          "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": [],
          "matches": []
        }
      ],
      "liveness": {
        "status": "Approved",
        "node_id": "feature_liveness",
        "score": 98,
        "method": "PASSIVE",
        "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
        "video_url": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-video.webm",
        "face_quality": 92.5,
        "warnings": [],
        "matches": []
      },
      "liveness_checks": [
        {
          "status": "Approved",
          "node_id": "feature_liveness",
          "score": 98,
          "method": "PASSIVE",
          "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
          "video_url": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-video.webm",
          "face_quality": 92.5,
          "warnings": [],
          "matches": []
        }
      ],
      "face_match": {
        "status": "Approved",
        "node_id": "feature_face_match",
        "score": 95,
        "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": []
      },
      "face_matches": [
        {
          "status": "Approved",
          "node_id": "feature_face_match",
          "score": 95,
          "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": []
        }
      ],
      "aml_screening": {
        "status": "Approved",
        "node_id": "feature_aml",
        "warnings": []
      },
      "aml_screenings": [
        {
          "status": "Approved",
          "node_id": "feature_aml",
          "warnings": []
        }
      ],
      "ip_analysis": {
        "status": "Approved",
        "node_id": "feature_ip_analysis",
        "ip_address": "203.0.113.10",
        "country": "US",
        "region": "New York",
        "city": "New York",
        "is_vpn": false,
        "is_proxy": false,
        "warnings": []
      },
      "ip_analyses": [
        {
          "status": "Approved",
          "node_id": "feature_ip_analysis",
          "ip_address": "203.0.113.10",
          "country": "US",
          "region": "New York",
          "city": "New York",
          "is_vpn": false,
          "is_proxy": false,
          "warnings": []
        }
      ]
    }
  }
}
```

**Campos Adicionais:**

* `verifiedAt`: Timestamp quando a verificação foi aprovada
* `extractedData`: Informações pessoais extraídas do documento
* `verifiedFields`: Array de campos que foram verificados com sucesso
* `warnings`: Array de avisos detectados durante a verificação
* `decision`: Resultados de verificação (imagens/URLs removidas por segurança)

### kyc.validation\_rejected

Enviado quando a verificação falha.

```json theme={null}
{
  "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": [
      "Falhou a verificação de autenticidade do documento",
      "Confiança baixa na correspondência facial"
    ],
    "decision": {
      "status": "Declined",
      "workflow_type": "standard",
      "session_id": "7c0fa22d-0cfa-4a78-8090-7c842397e788",
      "session_number": 921,
      "features": ["ID_VERIFICATION", "LIVENESS", "FACE_MATCH"],
      "images": {
        "documentFront": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "selfie": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg"
      },
      "id_verification": {
        "status": "Declined",
        "node_id": "feature_ocr",
        "document_type": "Passport",
        "document_number": "AB123456",
        "first_name": "John",
        "last_name": "Doe",
        "full_name": "John Doe",
        "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": [
          {
            "risk": "DOCUMENT_AUTHENTICITY_FAILED",
            "feature": "ID_VERIFICATION",
            "short_description": "Document authenticity could not be verified",
            "long_description": "The document failed authenticity checks.",
            "log_type": "error"
          }
        ],
        "matches": []
      },
      "id_verifications": [
        {
          "status": "Declined",
          "node_id": "feature_ocr",
          "document_type": "Passport",
          "document_number": "AB123456",
          "first_name": "John",
          "last_name": "Doe",
          "full_name": "John Doe",
          "front_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "portrait_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": [
            {
              "risk": "DOCUMENT_AUTHENTICITY_FAILED",
              "feature": "ID_VERIFICATION",
              "short_description": "Document authenticity could not be verified",
              "long_description": "The document failed authenticity checks.",
              "log_type": "error"
            }
          ],
          "matches": []
        }
      ],
      "liveness": {
        "status": "Declined",
        "node_id": "feature_liveness",
        "score": 42,
        "method": "PASSIVE",
        "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
        "warnings": [
          {
            "risk": "LIVENESS_FAILED",
            "feature": "LIVENESS",
            "short_description": "Liveness detection failed",
            "long_description": "The liveness check did not pass the required threshold.",
            "log_type": "error"
          }
        ],
        "matches": []
      },
      "liveness_checks": [
        {
          "status": "Declined",
          "node_id": "feature_liveness",
          "score": 42,
          "method": "PASSIVE",
          "reference_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/liveness-reference.jpg",
          "warnings": [
            {
              "risk": "LIVENESS_FAILED",
              "feature": "LIVENESS",
              "short_description": "Liveness detection failed",
              "long_description": "The liveness check did not pass the required threshold.",
              "log_type": "error"
            }
          ],
          "matches": []
        }
      ],
      "face_match": {
        "status": "Declined",
        "node_id": "feature_face_match",
        "score": 38,
        "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
        "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
        "warnings": [
          {
            "risk": "FACE_MATCH_LOW_CONFIDENCE",
            "feature": "FACE_MATCH",
            "short_description": "Face match confidence below threshold",
            "long_description": "The selfie did not match the document portrait with sufficient confidence.",
            "log_type": "error"
          }
        ]
      },
      "face_matches": [
        {
          "status": "Declined",
          "node_id": "feature_face_match",
          "score": 38,
          "source_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/document-front.jpg",
          "target_image": "kyc/global_gueno_validation_kyc/org-uuid/entity-uuid/validation-uuid/selfie.jpg",
          "warnings": [
            {
              "risk": "FACE_MATCH_LOW_CONFIDENCE",
              "feature": "FACE_MATCH",
              "short_description": "Face match confidence below threshold",
              "long_description": "The selfie did not match the document portrait with sufficient confidence.",
              "log_type": "error"
            }
          ]
        }
      ]
    }
  }
}
```

### kyc.validation\_abandoned

Enviado quando um cliente inicia mas não completa a verificação.

```json theme={null}
{
  "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 quando uma sessão de validação expira sem ser completada.

```json theme={null}
{
  "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 Tentativas

Se seu endpoint de webhook falhar em responder com um código de status 2xx, Gu1 automaticamente tentará novamente a entrega.

**Política de Tentativas Padrão:**

* **Tentativas Máximas**: 3 tentativas
* **Delay Inicial**: 1000ms (1 segundo)
* **Multiplicador de Backoff**: 2x
* **Sequência de Tentativas**: 1s → 2s → 4s

**Exemplo de Timeline:**

1. Tentativa inicial em `T+0s`
2. Primeira nova tentativa em `T+1s`
3. Segunda nova tentativa em `T+3s` (1s + 2s)
4. Terceira nova tentativa em `T+7s` (1s + 2s + 4s)

**Critério de Sucesso:**

* Códigos de status HTTP 200-299 são considerados bem-sucedidos
* Qualquer outro código de status ou erro de rede dispara uma nova tentativa

**Timeout:**

* Cada tentativa tem um timeout de 30 segundos
* Se seu endpoint não responder dentro de 30 segundos, a tentativa é marcada como falhada

<Tip>
  Você pode ver todas as tentativas de entrega de webhook (incluindo novas tentativas) na seção **Webhooks → Histórico** do seu dashboard.
</Tip>

## Melhores Práticas

<AccordionGroup>
  <Accordion title="Retornar 200 Rapidamente">
    Sempre retorne um código de status 200 o mais rápido possível para confirmar a recepção. Processe o webhook assincronamente se necessário.

    ```javascript theme={null}
    app.post('/webhooks/kyc', async (req, res) => {
      // Confirmar imediatamente
      res.status(200).send('OK');

      // Processar assincronamente
      processWebhook(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="Verificar Assinaturas">
    Sempre verifique o header `X-Webhook-Signature` para garantir que o webhook é autêntico.

    ```javascript theme={null}
    const signature = req.headers['x-webhook-signature'];
    if (!verifySignature(req.rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Assinatura inválida' });
    }
    ```
  </Accordion>

  <Accordion title="Lidar com Idempotência">
    Você pode receber o mesmo webhook múltiplas vezes. Use o `validationId` para garantir que você processa cada evento apenas uma vez.

    ```javascript theme={null}
    async function handleWebhook(webhook) {
      const alreadyProcessed = await db.checkWebhookProcessed(
        webhook.payload.validationId,
        webhook.event
      );

      if (alreadyProcessed) {
        return; // Pular duplicado
      }

      // Processar webhook
      await processValidation(webhook.payload);

      // Marcar como processado
      await db.markWebhookProcessed(
        webhook.payload.validationId,
        webhook.event
      );
    }
    ```
  </Accordion>

  <Accordion title="Usar entity.externalId para Busca">
    O webhook inclui `entity.externalId` que é o ID que você forneceu ao criar a entidade. Use-o para buscar o cliente no seu banco de dados.

    ```javascript theme={null}
    const customer = await db.findCustomer({
      externalId: data.entity.externalId
    });
    ```
  </Accordion>

  <Accordion title="Armazenar IDs de Validação">
    Armazene o `validationId` do Gu1 no seu banco de dados. Isso permite que você consulte detalhes de validação posteriormente se necessário.

    ```javascript theme={null}
    await db.updateCustomer(customer.id, {
      kycValidationId: data.validationId,
      kycStatus: data.status
    });
    ```
  </Accordion>

  <Accordion title="Lidar com Erros Graciosamente">
    Se o processamento falhar, registre o erro mas ainda assim retorne 200 para prevenir novas tentativas. Armazene webhooks falhados para revisão manual.

    ```javascript theme={null}
    try {
      await processWebhook(payload);
    } catch (error) {
      await db.saveFailedWebhook({
        payload,
        error: error.message,
        receivedAt: new Date()
      });

      // Ainda assim retornar 200
      res.status(200).json({ success: false });
    }
    ```
  </Accordion>

  <Accordion title="Usar Webhooks Específicos por Ambiente">
    Crie configurações de webhook separadas para os ambientes sandbox e produção.

    * **Sandbox**: Usar para testes com dados de teste
    * **Produção**: Usar para verificações de clientes ao vivo

    Isso permite que você teste o tratamento de webhooks de forma segura sem afetar sistemas de produção.
  </Accordion>
</AccordionGroup>

## Testar Webhooks

### Testar no Dashboard

1. Vá para **Configurações** → **Webhooks**
2. Selecione seu webhook
3. Clique em **Testar Webhook**
4. Gu1 enviará um evento de teste para seu endpoint
5. Verifique o status de resposta e logs

### Desenvolvimento Local

Use ngrok para expor seu servidor local:

```bash theme={null}
# Iniciar ngrok
ngrok http 3000

# Use a URL do ngrok como sua URL de webhook
https://abc123.ngrok.io/webhooks/kyc
```

### Fluxo de Teste

1. Crie um webhook sandbox apontando para seu endpoint de desenvolvimento
2. Crie uma validação KYC de teste
3. Seu endpoint de webhook recebe `kyc.validation_created`
4. Complete a verificação (ou simule diferentes resultados)
5. Seu endpoint de webhook recebe atualizações de status

## Monitoramento e Debugging

### Logs de Webhook

Ver histórico de entrega de webhook no seu dashboard:

1. Vá para **Configurações** → **Webhooks**
2. Selecione seu webhook
3. Clique em **Ver Logs** ou **Histórico**

**Os logs incluem:**

* Timestamp de cada tentativa de entrega
* Código de status HTTP recebido
* Body de resposta
* Tempo de resposta
* Mensagens de erro (se houver)
* Tentativas de nova tentativa

### Estatísticas de Webhook

Cada webhook mostra:

* **Disparos Totais**: Número total de vezes que o webhook foi disparado
* **Contagem de Sucessos**: Entregas bem-sucedidas
* **Contagem de Falhas**: Entregas falhadas
* **Último Disparado**: Timestamp da última tentativa
* **Último Sucesso**: Timestamp da última entrega bem-sucedida
* **Última Falha**: Timestamp da última falha

## Solução de Problemas

<AccordionGroup>
  <Accordion title="Não Receber Webhooks">
    **Verifique estes elementos:**

    * URL do webhook é publicamente acessível via HTTPS
    * Firewall permite requisições POST de entrada do Gu1
    * Endpoint retorna código de status 200 dentro de 30 segundos
    * Webhook está configurado e **habilitado** no dashboard
    * Ambiente correto selecionado (sandbox vs produção)
    * Verifique logs do servidor para requisições de entrada
    * Verifique que o webhook está inscrito nos tipos de evento corretos
  </Accordion>

  <Accordion title="Verificação de Assinatura Falhando">
    **Causas comuns:**

    * Usando segredo incorreto (verifique o dashboard para o segredo atual)
    * Verificando assinatura no JSON com parsing em vez do body raw
    * Segredo não salvo corretamente após criar webhook
    * Problemas de codificação (garantir UTF-8)

    **Solução:**

    ```javascript theme={null}
    // INCORRETO - verificando body com parsing
    const signature = crypto.createHmac('sha256', secret)
      .update(JSON.stringify(req.body))
      .digest('hex');

    // CORRETO - usar body raw antes do parsing
    const signature = crypto.createHmac('sha256', secret)
      .update(req.rawBody)
      .digest('hex');
    ```
  </Accordion>

  <Accordion title="Receber Webhooks Duplicados">
    Este é um comportamento normal. Os webhooks podem ser enviados múltiplas vezes devido a:

    * Problemas de rede
    * Timeouts
    * Novas tentativas após falhas

    **Sempre implemente idempotência** usando o `validationId` do webhook e o tipo de `event`.
  </Accordion>

  <Accordion title="Endpoint de Webhook com Timeout">
    Seu endpoint deve responder dentro de **30 segundos**. Se o processamento levar mais tempo:

    ```javascript theme={null}
    app.post('/webhooks', async (req, res) => {
      // Responder imediatamente
      res.status(200).json({ received: true });

      // Processar em segundo plano
      await queueWebhookProcessing(req.body);
    });
    ```
  </Accordion>

  <Accordion title="Falta extractedData">
    `extractedData` e `verifiedFields` só são incluídos em:

    * `kyc.validation_approved`
    * `kyc.validation_rejected`

    Não estão presentes em outros tipos de evento como `validation_created` ou `validation_in_progress`.
  </Accordion>

  <Accordion title="Segredo de Webhook Perdido">
    Se você perdeu seu segredo de webhook:

    1. Vá para **Configurações** → **Webhooks**
    2. Selecione seu webhook
    3. Clique em **Regenerar Segredo**
    4. Salve o novo segredo nas suas variáveis de ambiente
    5. Atualize sua aplicação com o novo segredo

    **Nota**: O segredo antigo deixará de funcionar imediatamente após a regeneração.
  </Accordion>
</AccordionGroup>

## Próximos Passos

<CardGroup cols={2}>
  <Card title="Verificar Status de Verificação" icon="magnifying-glass" href="/pt/use-cases/kyc/check-status">
    Consultar resultados de validação via API
  </Card>

  <Card title="Criar Validação KYC" icon="play" href="/pt/use-cases/kyc/create-validation">
    Iniciar uma nova verificação
  </Card>

  <Card title="Eventos de Entidade" icon="bell" href="/pt/webhooks/overview">
    Aprender sobre outros eventos de webhook
  </Card>

  <Card title="Referência de API" icon="code" href="/pt/webhooks/configuration">
    Documentação de API de webhooks
  </Card>
</CardGroup>
