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

# Validar a assinatura

> Confira o HMAC-SHA256 do header X-Webhook-Signature antes de confiar no conteúdo de um webhook.

Cada entrega leva o header `X-Webhook-Signature`:

```text theme={null}
X-Webhook-Signature: t=1786881906,v1=8f3a...
```

* `t` é o timestamp Unix em segundos.
* `v1` é o HMAC-SHA256, em hexadecimal, calculado sobre o timestamp, um ponto e o corpo bruto (`t + "." + corpo`), usando o segredo do webhook como chave.

<Warning>
  Calcule sobre o **corpo bruto** da requisição, antes de qualquer parse. Reserializar o JSON muda os bytes e a assinatura não fecha.
</Warning>

<CodeGroup>
  ```js Node.js theme={null}
  import { createHmac, timingSafeEqual } from 'crypto'
  import express from 'express'

  const app = express()

  // O 'verify' guarda o corpo bruto sem impedir o parse do JSON.
  app.use(express.json({
    verify: (req, _res, buf) => { req.rawBody = buf },
  }))

  function assinaturaValida(header, rawBody, secret) {
    const partes = Object.fromEntries(
      String(header || '').split(',').map((p) => p.split('=', 2)),
    )
    if (!partes.t || !partes.v1) return false

    // Rejeita entregas antigas: sem isso, uma captura da requisição poderia ser
    // reenviada mais tarde com a assinatura ainda válida.
    const idadeEmSegundos = Math.abs(Date.now() / 1000 - Number(partes.t))
    if (!Number.isFinite(idadeEmSegundos) || idadeEmSegundos > 300) return false

    const esperado = createHmac('sha256', secret)
      .update(`${partes.t}.${rawBody}`)
      .digest('hex')

    const a = Buffer.from(esperado, 'hex')
    const b = Buffer.from(partes.v1, 'hex')
    return a.length === b.length && timingSafeEqual(a, b)
  }

  app.post('/webhooks/bukiopay', (req, res) => {
    const ok = assinaturaValida(
      req.headers['x-webhook-signature'],
      req.rawBody,
      process.env.BUKIOPAY_WEBHOOK_SECRET,
    )
    if (!ok) return res.status(401).send('assinatura inválida')

    // Responda antes de processar: o tempo limite da entrega é de 15s.
    res.sendStatus(200)
  })
  ```

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


  def assinatura_valida(header: str, raw_body: bytes, secret: str) -> bool:
      partes = dict(
          p.split("=", 1) for p in (header or "").split(",") if "=" in p
      )
      t, v1 = partes.get("t"), partes.get("v1")
      if not t or not v1:
          return False

      if abs(time.time() - int(t)) > 300:
          return False

      esperado = hmac.new(
          secret.encode(),
          f"{t}.".encode() + raw_body,
          hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(esperado, v1)
  ```

  ```php PHP theme={null}
  <?php
  function assinatura_valida(string $header, string $rawBody, string $secret): bool
  {
      $partes = [];
      foreach (explode(',', $header) as $par) {
          [$chave, $valor] = array_pad(explode('=', $par, 2), 2, null);
          $partes[$chave] = $valor;
      }

      if (empty($partes['t']) || empty($partes['v1'])) {
          return false;
      }

      if (abs(time() - (int) $partes['t']) > 300) {
          return false;
      }

      $esperado = hash_hmac('sha256', $partes['t'] . '.' . $rawBody, $secret);

      return hash_equals($esperado, $partes['v1']);
  }
  ```
</CodeGroup>

## Checklist


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.