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

# Verificar a assinatura

> Confirme que o evento veio mesmo da Vext antes de agir sobre ele. São poucas linhas de código, prontas nas quatro linguagens.

Conferir a assinatura leva poucas linhas de código e é o que garante que o evento veio mesmo de nós.
Sem ela, qualquer pessoa que descubra a URL do seu endpoint consegue simular um pagamento — então
vale fazer isso antes de qualquer outra coisa no seu handler.

O código pronto está logo abaixo, nas quatro linguagens. Se quiser só copiar e seguir,
[pule para a implementação](#implementação).

## Como a assinatura é montada

Toda entrega chega com o header:

```
X-Vext-Signature: t=1785931392,v1=8f3c1d0a5b7e9042c6a1b3d5e7f9012345678901abcdef234567890abcdef1234
```

Onde `v1` é:

```
HMAC-SHA256("<timestamp>.<corpo bruto>", <segredo do endpoint>)
```

O segredo é o do endpoint que recebeu a entrega, disponível em **Desenvolvedores**.

## Dois detalhes que fazem a conferência funcionar

**1. Use o corpo bruto, antes de desserializar.**

O HMAC é calculado sobre os bytes exatos que enviamos. Desserializar e reserializar muda espaçamento
e ordem de chaves, e aí a conta não fecha — é a causa mais comum de "minha assinatura nunca bate".

Na prática: `$request->getContent()` no Laravel, `express.raw()` no Express e `request.get_data()`
no Flask, em vez de `->all()`, `express.json()` ou `request.json`.

**2. Aceite só timestamps dos últimos 5 minutos.**

A assinatura de uma entrega continua válida para sempre, então a janela de tempo é o que impede que
uma entrega antiga de `charge.paid` seja reenviada mais tarde. São duas linhas, e é o que separa uma
verificação decorativa de uma que protege de verdade.

## Implementação

<CodeGroup>
  ```php PHP (Laravel) theme={null}
  use Illuminate\Http\Request;

  public function receber(Request $request)
  {
      // Corpo BRUTO. $request->all() desserializa, e reserializar quebra o HMAC.
      $bruto = $request->getContent();

      if (! preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $request->header('X-Vext-Signature', ''), $m)) {
          abort(400);
      }

      [, $timestamp, $assinatura] = $m;

      // Janela de 5 minutos: sem isto, uma entrega interceptada pode ser reenviada para sempre.
      if (abs(time() - (int) $timestamp) > 300) {
          abort(400);
      }

      $esperada = hash_hmac('sha256', $timestamp.'.'.$bruto, config('services.vext.webhook_secret'));

      // hash_equals compara em tempo constante. '===' vaza o prefixo correto pelo tempo de resposta.
      if (! hash_equals($esperada, $assinatura)) {
          abort(400);
      }

      $evento = json_decode($bruto, true); // só agora o evento é confiável

      return response()->noContent();
  }
  ```

  ```javascript Node.js (Express) theme={null}
  import crypto from 'node:crypto';

  // express.raw preserva o corpo bruto. express.json() o destruiria antes de você conferir.
  app.post('/webhooks/vext', express.raw({ type: 'application/json' }), (req, res) => {
    const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.get('X-Vext-Signature') ?? '');
    if (!m) return res.sendStatus(400);

    const [, timestamp, assinatura] = m;

    // Janela de 5 minutos: sem isto, uma entrega interceptada pode ser reenviada para sempre.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);

    const esperada = crypto
      .createHmac('sha256', process.env.VEXT_WEBHOOK_SECRET)
      .update(`${timestamp}.${req.body}`)
      .digest('hex');

    // timingSafeEqual exige buffers do mesmo tamanho — compare o comprimento antes.
    const a = Buffer.from(esperada, 'hex');
    const b = Buffer.from(assinatura, 'hex');
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);

    const evento = JSON.parse(req.body); // só agora o evento é confiável

    res.sendStatus(200);
  });
  ```

  ```python Python (Flask) theme={null}
  import hashlib
  import hmac
  import os
  import re
  import time

  from flask import Flask, abort, request

  app = Flask(__name__)
  PADRAO = re.compile(r"^t=(\d+),v1=([0-9a-f]{64})$")


  @app.post("/webhooks/vext")
  def receber():
      # Bytes crus. request.json desserializa, e reserializar quebra o HMAC.
      bruto = request.get_data()

      correspondencia = PADRAO.match(request.headers.get("X-Vext-Signature", ""))
      if not correspondencia:
          abort(400)

      timestamp, assinatura = correspondencia.groups()

      # Janela de 5 minutos: sem isto, uma entrega interceptada pode ser reenviada para sempre.
      if abs(time.time() - int(timestamp)) > 300:
          abort(400)

      esperada = hmac.new(
          os.environ["VEXT_WEBHOOK_SECRET"].encode(),
          f"{timestamp}.".encode() + bruto,
          hashlib.sha256,
      ).hexdigest()

      # compare_digest compara em tempo constante. '==' vaza o prefixo correto pelo tempo de resposta.
      if not hmac.compare_digest(esperada, assinatura):
          abort(400)

      evento = request.get_json()  # só agora o evento é confiável

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

<Note>
  Repare que os três exemplos comparam em tempo constante — `hash_equals`,
  `crypto.timingSafeEqual`, `hmac.compare_digest`. Uma comparação comum de strings para no primeiro
  byte diferente, e essa diferença de tempo é suficiente para alguém descobrir a assinatura correta
  byte a byte. Vale manter essa parte como está ao adaptar o código.
</Note>

## Depois de conferir

Com a assinatura confirmada, desserialize e siga: responda `2xx` rápido e deixe o processamento para
uma fila. O padrão está em [Entregas e retentativas](/webhooks/entregas-e-retentativas).

Se a assinatura não bater, responda `400` e registre o `X-Vext-Delivery` no seu log. Os dois motivos
possíveis são um evento adulterado ou um segredo desatualizado, e o log com o `X-Vext-Delivery` é o
que nos permite dizer qual dos dois foi.

## Veja também

<CardGroup cols={2}>
  <Card title="Visão geral" icon="webhook" href="/webhooks/visao-geral">
    Os três eventos e o envelope.
  </Card>

  <Card title="Checkout PIX" icon="qr-code" href="/receitas/checkout-pix">
    O fluxo completo, da criação à liberação.
  </Card>
</CardGroup>

***

Ficou algo de fora? Escreva para [suporte@usevext.com](mailto:suporte@usevext.com). Se o assunto for
uma chamada específica, informe o horário dela; se for uma entrega de webhook, informe o
`X-Vext-Delivery` — é por ele que localizamos a tentativa, a resposta do seu servidor e o horário.
