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

# Integração com webhook

> Fluxo completo end-to-end — o caminho principal para produção

Webhook é o caminho recomendado para produção. O fluxo é simples:

```
1. Configure o webhook endpoint (uma vez, no dashboard)
2. POST /api/v1/bureau/runs → receba run_id
3. kycert processa o bureau (5 a 30 segundos)
4. kycert faz POST para seu endpoint com o resultado
5. Verifique a assinatura
6. Aja com base na decision
```

## Passo 1: Configure o endpoint (uma vez)

No dashboard kycert, vá em **Integrações & API → Webhooks** e cadastre o URL HTTPS do seu servidor. O kycert enviará todos os resultados para esse endpoint.

Alternativamente, passe `webhook_url` no corpo de cada `POST /runs` para sobrescrever o endpoint por requisição.

<Warning>
  Se você tiver um endpoint permanente configurado no dashboard **e** passar `webhook_url` na requisição, o kycert usará a URL do override mas assinará com o segredo do endpoint permanente. Evite misturar as duas abordagens — prefira o endpoint permanente em produção e omita `webhook_url` completamente nas chamadas da API.
</Warning>

## Passo 2: Criar o run

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://admin.kycert.com.br/api/v1/bureau/runs \
    -H "x-api-key: sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "template_id": "550e8400-e29b-41d4-a716-446655440000",
      "subject": {
        "type": "pf",
        "doc": "12345678901",
        "name": "João Silva"
      },
      "external_id": "cust_abc123"
    }'
  ```

  ```typescript Node.js theme={null}
  const res = await fetch('https://admin.kycert.com.br/api/v1/bureau/runs', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.KYCERT_API_KEY!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      template_id: '550e8400-e29b-41d4-a716-446655440000',
      subject: { type: 'pf', doc: '12345678901', name: 'João Silva' },
      external_id: 'cust_abc123',
    }),
  })

  if (!res.ok) {
    const err = await res.json()
    throw new Error(`kycert error: ${err.error.code} — ${err.error.message}`)
  }

  const { run_id } = await res.json()
  console.log(`Run criado: ${run_id}`)
  ```

  ```python Python theme={null}
  import os, requests

  response = requests.post(
      'https://admin.kycert.com.br/api/v1/bureau/runs',
      headers={
          'x-api-key': os.environ['KYCERT_API_KEY'],
          'Content-Type': 'application/json',
      },
      json={
          'template_id': '550e8400-e29b-41d4-a716-446655440000',
          'subject': {'type': 'pf', 'doc': '12345678901', 'name': 'João Silva'},
          'external_id': 'cust_abc123',
      }
  )

  if not response.ok:
      err = response.json()
      raise Exception(f"kycert error: {err['error']['code']} — {err['error']['message']}")

  data = response.json()
  print(f"Run criado: {data['run_id']}")
  ```
</CodeGroup>

Resposta:

```json theme={null}
{
  "run_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued",
  "livemode": true
}
```

## Passo 3–4: kycert processa e entrega o resultado

O kycert consulta as fontes em paralelo e entrega o resultado no seu endpoint quando pronto. Você não precisa fazer nada neste passo.

## Passo 5: Receber e verificar o webhook

<Warning>
  **Sempre verifique a assinatura.** Qualquer pessoa na internet pode fazer POST para o seu endpoint — a verificação garante que o evento veio realmente do kycert.
</Warning>

<CodeGroup>
  ```typescript Node.js (Express) theme={null}
  import express from 'express'
  import crypto from 'crypto'

  function verifyKycertSignature(
    payload: Buffer,
    signature: string,
    secret: string,
  ): boolean {
    // Formato: "t=1234567890,v1=abc123..."
    const parts = Object.fromEntries(
      signature.split(',').map(p => p.split('=') as [string, string])
    )
    const timestamp = parts['t']
    const expected  = parts['v1']
    if (!timestamp || !expected) return false

    // Rejeitar eventos com mais de 5 minutos de diferença
    if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false

    const computed = crypto
      .createHmac('sha256', secret)           // secret é raw string, não hex-decoded
      .update(`${timestamp}.${payload.toString()}`, 'utf8')
      .digest('hex')

    if (computed.length !== expected.length) return false
    return crypto.timingSafeEqual(
      Buffer.from(computed, 'hex'),
      Buffer.from(expected, 'hex'),
    )
  }

  const app = express()

  app.post(
    '/webhooks/kycert',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const signature = req.headers['kycert-signature'] as string
      const secret    = process.env.KYCERT_WEBHOOK_SECRET!

      if (!verifyKycertSignature(req.body, signature, secret)) {
        return res.sendStatus(401)
      }

      // Responda 200 imediatamente — processe de forma assíncrona
      res.sendStatus(200)

      const event = JSON.parse(req.body.toString())
      setImmediate(() => handleKycertEvent(event))
    }
  )

  async function handleKycertEvent(event: Record<string, unknown>) {
    if (event['event'] === 'run.completed') {
      const data = event['data'] as Record<string, unknown>
      const { decision, risk_band, external_id } = data as {
        decision: string
        risk_band: string
        external_id: string
      }
      console.log(`Run ${external_id}: ${decision} (${risk_band})`)
      // ex: atualizar status do cliente no banco de dados
    }
  }
  ```

  ```python Python (Flask) theme={null}
  import hmac, hashlib, time, json
  from flask import Flask, request, abort

  app = Flask(__name__)

  def verify_kycert_signature(payload: bytes, signature: str, secret: str) -> bool:
      parts = dict(p.split('=', 1) for p in signature.split(','))
      ts  = parts.get('t', '')
      sig = parts.get('v1', '')
      if not ts or not sig:
          return False
      # Rejeitar eventos com mais de 5 minutos
      if abs(time.time() - int(ts)) > 300:
          return False
      expected = hmac.new(
          secret.encode(),                    # secret é raw string, não hex-decoded
          f"{ts}.{payload.decode()}".encode(),
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, sig)

  @app.route('/webhooks/kycert', methods=['POST'])
  def webhook():
      import os
      signature = request.headers.get('kycert-signature', '')
      secret    = os.environ['KYCERT_WEBHOOK_SECRET']

      if not verify_kycert_signature(request.data, signature, secret):
          abort(401)

      event = json.loads(request.data)

      # Responder 200 imediatamente
      # Processar de forma assíncrona (Celery, RQ, etc.)
      if event.get('event') == 'run.completed':
          data = event['data']
          print(f"Run {data['external_id']}: {data['decision']} ({data['risk_band']})")

      return '', 200
  ```
</CodeGroup>

## Passo 6: Agir com base na decision

| `decision` | `risk_band` típico | O que fazer                                             |
| ---------- | ------------------ | ------------------------------------------------------- |
| `approved` | `baixo` / `medio`  | Prosseguir com onboarding ou operação                   |
| `rejected` | `alto`             | Recusar — não reverter sem análise manual do compliance |
| `review`   | `medio` / `alto`   | Aguardar decisão do analista no dashboard kycert        |

## Payload completo do webhook

```json theme={null}
{
  "id": "evt_01J4...",
  "object": "event",
  "event": "run.completed",
  "created": 1718200818,
  "livemode": true,
  "data": {
    "object": "run",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "completed",
    "decision": "approved",
    "risk_band": "baixo",
    "subject_type": "pf",
    "template_id": "661e9511-f3ac-52e5-b827-557766551111",
    "external_id": "cust_abc123",
    "metadata": { "channel": "app_mobile" },
    "checks_summary": { "total": 12, "valid": 12, "invalid": 0, "no_data": 0, "error": 0 },
    "created_at": "2026-06-12T14:00:00Z",
    "completed_at": "2026-06-12T14:00:18Z"
  }
}
```

<Note>
  Ver [referência completa de campos →](/webhooks#campos-de-data) — descrição semântica de cada campo do envelope e do objeto `data`, incluindo todos os valores possíveis de `status`, `decision` e `checks_summary`.
</Note>

## Boas práticas

* **Responda 200 imediatamente** e processe de forma assíncrona — seu endpoint tem 30 segundos antes do kycert considerar falha
* **Seja idempotente** — o mesmo evento pode ser entregue mais de uma vez em caso de retry
* **Registre o `id` do evento** para deduplicação
* **Retorne sempre 2xx** — o kycert retenta em qualquer non-2xx (incluindo 4xx e 5xx). Se o processamento falhar internamente, responda 200 e trate o erro de forma assíncrona para não consumir as 3 tentativas desnecessariamente

Consulte a política completa de retry em [Webhooks → Overview](/webhooks/overview).
