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

# Webhook Auth

> Receba notificações em tempo real quando uma autenticação biométrica é concluída.

## Visão Geral

Ao término de cada autenticação, a Campeão Sec faz um `POST` para a URL configurada com o evento `auth.completed`. O webhook é a fonte de verdade — use-o para tomar decisões de negócio, não o callback `onSuccess` do widget.

***

## Configurar URL

**1. No portal** — em **app.campeaosec.com → Webhooks**, configure a URL padrão (compartilhada com o KYC).

**2. Por sessão** — envie `webhook_url` ao criar a sessão para sobrescrever a URL padrão:

```json theme={null}
{
  "external_id": "user-456",
  "ambiente": "sandbox",
  "webhook_url": "https://seu-servidor.com/webhook/auth"
}
```

***

## Evento

| Evento           | Quando ocorre                                     |
| ---------------- | ------------------------------------------------- |
| `auth.completed` | Autenticação concluída (autenticada ou rejeitada) |

***

## Payload

```json theme={null}
{
  "event": "auth.completed",
  "auth_id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
  "external_id": "user-456",
  "authenticated": true,
  "status": "authenticated",
  "score_liveness": 0.91,
  "score_facial": 0.87,
  "reason": null,
  "ambiente": "producao",
  "created_at": "2026-07-31T10:00:00Z",
  "completed_at": "2026-07-31T10:00:03Z"
}
```

### Campos

| Campo            | Tipo           | Descrição                                                           |
| ---------------- | -------------- | ------------------------------------------------------------------- |
| `event`          | string         | Sempre `auth.completed`                                             |
| `auth_id`        | string         | UUID do registro de autenticação                                    |
| `external_id`    | string         | ID enviado na criação da sessão                                     |
| `authenticated`  | boolean        | `true` se autenticado com sucesso                                   |
| `status`         | string         | `authenticated`, `rejected` ou `expired`                            |
| `score_liveness` | number \| null | Score de vivacidade (0–1). Ex: `0.91` = 91%                         |
| `score_facial`   | number \| null | Similaridade facial vs. embedding KYC (0–1). Ex: `0.87` = 87%       |
| `reason`         | string \| null | Motivo da rejeição quando `authenticated: false`. Ver tabela abaixo |
| `ambiente`       | string         | `sandbox` ou `producao`                                             |
| `created_at`     | string         | ISO 8601 — início da sessão                                         |
| `completed_at`   | string         | ISO 8601 — fim da autenticação                                      |

### Motivos de rejeição (`reason`)

| Valor             | Descrição                                               |
| ----------------- | ------------------------------------------------------- |
| `liveness_failed` | Desafio de liveness não superado (score \< threshold)   |
| `facial_mismatch` | Similaridade facial abaixo do threshold (score \< 0.72) |
| `no_embedding`    | Embedding KYC não encontrado para o `external_id`       |
| `session_expired` | Sessão expirou antes da conclusão                       |

***

## Validar Assinatura

O mesmo mecanismo HMAC-SHA256 do KYC. Cada request inclui:

```
X-Campeao-Signature: sha256=a3f8c2d1b4e9...
X-Campeao-Event: auth.completed
X-Campeao-Attempt: 1
```

O segredo fica em **app.campeaosec.com → Webhooks → Secret**.

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

  // Express — use raw body para validação
  app.post('/webhook/auth', express.raw({ type: 'application/json' }), (req, res) => {
    const body = req.body.toString('utf8')
    const esperado = 'sha256=' + crypto
      .createHmac('sha256', process.env.WEBHOOK_SECRET)
      .update(body)
      .digest('hex')

    if (!crypto.timingSafeEqual(
      Buffer.from(req.headers['x-campeao-signature']),
      Buffer.from(esperado)
    )) {
      return res.status(401).send('Assinatura inválida')
    }

    const evento = JSON.parse(body)
    const { auth_id, external_id, authenticated, score_facial } = evento

    if (authenticated) {
      // Liberar operação sensível para o usuário
      await liberarOperacao(external_id, auth_id)
    }

    res.status(200).send('OK')
  })
  ```

  ```python Python theme={null}
  import hashlib, hmac, json
  from fastapi import Request, HTTPException

  async def validar_auth_webhook(request: Request, secret: str) -> dict:
      body = await request.body()
      assinatura = request.headers.get('x-campeao-signature', '')

      esperado = 'sha256=' + hmac.new(
          secret.encode('utf-8'),
          body,
          hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(assinatura, esperado):
          raise HTTPException(status_code=401, detail='Assinatura inválida')

      return json.loads(body)

  @app.post('/webhook/auth')
  async def webhook_auth(request: Request):
      evento = await validar_auth_webhook(request, os.environ['WEBHOOK_SECRET'])

      if evento['authenticated']:
          await liberar_operacao(evento['external_id'], evento['auth_id'])

      return {'ok': True}
  ```

  ```php PHP theme={null}
  $payload    = file_get_contents('php://input');
  $assinatura = $_SERVER['HTTP_X_CAMPEAO_SIGNATURE'] ?? '';
  $secret     = getenv('WEBHOOK_SECRET');

  $esperado = 'sha256=' . hash_hmac('sha256', $payload, $secret);
  if (!hash_equals($esperado, $assinatura)) {
      http_response_code(401);
      exit('Assinatura inválida');
  }

  $evento = json_decode($payload, true);
  if ($evento['authenticated']) {
      liberarOperacao($evento['external_id'], $evento['auth_id']);
  }

  http_response_code(200);
  echo 'OK';
  ```
</CodeGroup>

***

## Retry Automático

Idêntico ao KYC: até 5 tentativas com backoff exponencial.

| Tentativa | Espera      |
| --------- | ----------- |
| 1         | Imediata    |
| 2         | 30 segundos |
| 3         | 5 minutos   |
| 4         | 30 minutos  |
| 5         | 2 horas     |

***

## Fallback: Consultar Resultado

Se o webhook não chegar, consulte diretamente pelo `auth_id` obtido no `onSuccess` do widget:

```bash theme={null}
curl -X GET https://api-kyc.campeaosec.com/v1/auth/{auth_id} \
  -H "Authorization: Bearer sk_test_sua_chave"
```

***

## Boas Práticas

* **Responda `200 OK` imediatamente** — processe de forma assíncrona e retorne 200 antes de qualquer lógica de negócio
* **Use `auth_id` como chave de deduplicação** — o mesmo evento pode ser entregue mais de uma vez
* **Sempre valide a assinatura** — nunca tome decisões de segurança sem confirmar o HMAC
* **Não confie apenas no `onSuccess` do widget** — o widget pode ser interceptado em ambientes comprometidos; o webhook com HMAC é a prova criptográfica
