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

# Segurança dos Webhooks (Assinatura HMAC)

> Como validar a autenticidade dos webhooks enviados pela 3X Pay usando assinatura HMAC-SHA256

# Segurança dos Webhooks

## Visão Geral

Todos os webhooks que a 3X Pay envia para a URL de notificação configurada na sua conta
são **assinados criptograficamente** com **HMAC-SHA256**. A assinatura permite que você
confirme, do seu lado, que a requisição:

1. Foi realmente enviada pela 3X Pay (autenticidade)
2. Não foi alterada no caminho (integridade)
3. Não é uma reentrega maliciosa de uma notificação antiga (proteção contra *replay*)

<Warning>
  A URL de webhook é pública: qualquer pessoa que a descubra pode enviar requisições
  forjadas para ela. **Sempre valide a assinatura antes de processar o payload** —
  nunca confie no corpo de um webhook sem verificar o `X-Webhook-Signature`.
</Warning>

## Chave de assinatura

O segredo usado para gerar a assinatura é o seu **`api_secret`** — a mesma credencial
usada para autenticar as chamadas à API. Você a encontra no painel da 3X Pay em
**Integrações > Credenciais de API** (veja [Credenciais de API](/authentication)).

<Warning>
  Trate o `api_secret` como uma senha. Ele assina os seus webhooks e autentica a sua
  API. Em caso de vazamento, revogue e gere novas credenciais imediatamente no painel.
</Warning>

## Headers de segurança

Toda requisição de webhook inclui os seguintes headers:

| Header                | Exemplo                      | Descrição                                                                              |
| --------------------- | ---------------------------- | -------------------------------------------------------------------------------------- |
| `X-Webhook-Signature` | `sha256=9f86d081884c7d65...` | Assinatura HMAC-SHA256 em hexadecimal, prefixada por `sha256=`.                        |
| `X-Webhook-Timestamp` | `1747526400`                 | Momento do envio (Unix timestamp, em **segundos**). Usado no cálculo da assinatura.    |
| `X-Webhook-Event-Id`  | `evt_10293`                  | Identificador único do evento. **Estável entre retentativas** — use para idempotência. |
| `X-Webhook-Attempt`   | `1`                          | Número da tentativa de entrega (começa em `1` e incrementa a cada retentativa).        |

## Como a assinatura é gerada

A 3X Pay monta uma string chamada `signed_payload` e calcula o HMAC-SHA256 dela usando
o seu `api_secret` como chave:

```
signed_payload = X-Webhook-Timestamp + "." + corpo_bruto_da_requisição

assinatura = HMAC_SHA256(signed_payload, api_secret)  // resultado em hexadecimal
```

O valor enviado em `X-Webhook-Signature` é `sha256=` + essa assinatura em hexadecimal.

<Warning>
  **Use o corpo bruto (raw body) da requisição.** A assinatura é calculada sobre os
  bytes exatos do JSON enviado. Se você desserializar o JSON e serializá-lo novamente
  antes de calcular o HMAC, a ordem das chaves e os espaços podem mudar e **a assinatura
  não vai bater**. Capture o corpo cru antes de qualquer *parse*.
</Warning>

## Como validar (passo a passo)

<Steps>
  <Step title="Capture o corpo bruto">
    Leia o corpo da requisição como string/bytes **antes** de fazer o `parse` do JSON.
  </Step>

  <Step title="Verifique o timestamp (anti-replay)">
    Rejeite a requisição se `X-Webhook-Timestamp` estiver fora de uma janela de
    tolerância (recomendado: **±5 minutos / 300 segundos**) em relação ao horário atual.
  </Step>

  <Step title="Recalcule o HMAC">
    Monte `signed_payload = timestamp + "." + corpo_bruto` e calcule
    `HMAC_SHA256(signed_payload, api_secret)` em hexadecimal.
  </Step>

  <Step title="Compare em tempo constante">
    Compare o valor calculado com o recebido em `X-Webhook-Signature` (sem o prefixo
    `sha256=`) usando uma função de comparação **em tempo constante**
    (`timingSafeEqual`, `hmac.compare_digest`, `hash_equals`). Nunca use `==` simples.
  </Step>

  <Step title="Garanta idempotência">
    Use o `X-Webhook-Event-Id` para descartar entregas duplicadas (retentativas reenviam
    o mesmo `Event-Id`). Só então processe o payload.
  </Step>
</Steps>

## Exemplos de implementação

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

  // IMPORTANTE: capture o corpo BRUTO. Ex.:
  // app.use('/webhook', express.raw({ type: 'application/json' }));

  function verificarWebhook(req, apiSecret) {
    const header = req.headers['x-webhook-signature']; // "sha256=<hex>"
    const timestamp = req.headers['x-webhook-timestamp'];
    if (!header || !timestamp) return false;

    // 1. Anti-replay: rejeita timestamps fora de ±5 min
    const agora = Math.floor(Date.now() / 1000);
    if (Math.abs(agora - Number(timestamp)) > 300) return false;

    // 2. Recalcula o HMAC sobre `${timestamp}.${rawBody}`
    const rawBody = req.body.toString('utf8'); // Buffer -> string crua
    const signedPayload = `${timestamp}.${rawBody}`;
    const esperado = crypto
      .createHmac('sha256', apiSecret)
      .update(signedPayload)
      .digest('hex');

    // 3. Comparação em tempo constante
    const recebido = header.replace(/^sha256=/i, '');
    const a = Buffer.from(esperado, 'hex');
    const b = Buffer.from(recebido, 'hex');
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  app.post('/webhook', (req, res) => {
    if (!verificarWebhook(req, process.env.API_SECRET)) {
      return res.status(401).send('assinatura inválida');
    }
    const evento = JSON.parse(req.body.toString('utf8'));
    // ... idempotência via req.headers['x-webhook-event-id'] e processamento
    res.status(200).send('OK');
  });
  ```

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

  def verificar_webhook(raw_body: bytes, headers, api_secret: str) -> bool:
      header = headers.get("X-Webhook-Signature", "")  # "sha256=<hex>"
      timestamp = headers.get("X-Webhook-Timestamp", "")
      if not header or not timestamp:
          return False

      # 1. Anti-replay: rejeita timestamps fora de ±5 min
      if abs(int(time.time()) - int(timestamp)) > 300:
          return False

      # 2. Recalcula o HMAC sobre f"{timestamp}.{raw_body}"
      signed_payload = timestamp.encode() + b"." + raw_body
      esperado = hmac.new(
          api_secret.encode(), signed_payload, hashlib.sha256
      ).hexdigest()

      # 3. Comparação em tempo constante
      recebido = header.replace("sha256=", "", 1)
      return hmac.compare_digest(esperado, recebido)

  @app.post("/webhook")
  def webhook():
      raw = request.get_data()  # corpo bruto, antes do parse
      if not verificar_webhook(raw, request.headers, API_SECRET):
          abort(401)
      evento = request.get_json()
      # ... idempotência via request.headers["X-Webhook-Event-Id"]
      return "OK", 200
  ```

  ```php PHP theme={null}
  <?php
  function verificarWebhook(string $rawBody, array $headers, string $apiSecret): bool {
      $header = $headers['X-Webhook-Signature'] ?? '';   // "sha256=<hex>"
      $timestamp = $headers['X-Webhook-Timestamp'] ?? '';
      if ($header === '' || $timestamp === '') return false;

      // 1. Anti-replay: rejeita timestamps fora de ±5 min
      if (abs(time() - (int) $timestamp) > 300) return false;

      // 2. Recalcula o HMAC sobre "{timestamp}.{rawBody}"
      $signedPayload = $timestamp . '.' . $rawBody;
      $esperado = hash_hmac('sha256', $signedPayload, $apiSecret);

      // 3. Comparação em tempo constante
      $recebido = preg_replace('/^sha256=/i', '', $header);
      return hash_equals($esperado, $recebido);
  }

  $rawBody = file_get_contents('php://input'); // corpo bruto
  $headers = getallheaders();
  if (!verificarWebhook($rawBody, $headers, getenv('API_SECRET'))) {
      http_response_code(401);
      exit('assinatura inválida');
  }
  $evento = json_decode($rawBody, true);
  // ... idempotência via $headers['X-Webhook-Event-Id']
  http_response_code(200);
  echo 'OK';
  ```
</CodeGroup>

## Idempotência e retentativas

* Se a sua aplicação **não responder com HTTP 2xx**, a 3X Pay reenvia o webhook
  automaticamente: até **5 tentativas**, com *backoff* exponencial (intervalo inicial
  de \~5 segundos).
* Todas as tentativas do mesmo evento carregam o **mesmo `X-Webhook-Event-Id`**.
  Persista esse identificador e ignore eventos já processados para evitar
  processamento duplicado (ex.: creditar o mesmo pagamento duas vezes).
* A cada tentativa o `X-Webhook-Timestamp` é renovado e a assinatura é recalculada.
  Por isso, **valide a assinatura sempre com o `X-Webhook-Timestamp` da própria
  requisição recebida**, não com um valor armazenado.

## Rotação do `api_secret`

Ao gerar novas credenciais no painel, a assinatura dos webhooks pode continuar usando
o segredo anterior por um curto período (até \~1 hora, devido a cache interno). Durante
uma rotação planejada, aceite **temporariamente** assinaturas válidas com o segredo
antigo **ou** com o novo até confirmar que apenas o novo está em uso.

## Boas práticas de segurança

<Steps>
  Valide a assinatura HMAC em **todos** os webhooks antes de processar o payload

  Use sempre o **corpo bruto** da requisição no cálculo do HMAC

  Faça a comparação da assinatura em **tempo constante**

  Aplique a janela de tolerância de timestamp (±5 min) para mitigar *replay*

  Garanta **idempotência** pelo `X-Webhook-Event-Id`

  Sirva o endpoint de webhook **somente via HTTPS**

  Nunca registre o `api_secret` em logs; mantenha-o em variável de ambiente / cofre de segredos

  Responda rápido (`200 OK`) e processe o evento de forma **assíncrona**
</Steps>

export default function MDXPage({children}) {
  return <div className="docs-container">{children}</div>;
}
