> ## Documentation Index
> Fetch the complete documentation index at: https://docs.astronpay.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 Astron Pay usando assinatura HMAC-SHA256

# Segurança dos Webhooks

## Visão Geral

Todos os webhooks que a Astron 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 Astron 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`.

## 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 Astron Pay em
**Integrações > Credenciais de API** (veja [Credenciais de API](https://docs.astronpay.co/gateway/authentication.md)).

> [!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.

## 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 Astron 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*.

## Como validar (passo a passo)

**1. Capture o corpo bruto**

Leia o corpo da requisição como string/bytes **antes** de fazer o `parse` do JSON.

**2. 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.

**3. Recalcule o HMAC**

Monte `signed_payload = timestamp + "." + corpo_bruto` e calcule
`HMAC_SHA256(signed_payload, api_secret)` em hexadecimal.

**4. 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.

**5. 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.

## Exemplos de implementação

```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';
```

## Idempotência e retentativas

* Se a sua aplicação **não responder com HTTP 2xx**, a Astron 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

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**


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