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

# Validação de assinatura

> Como verificar a autenticidade de um webhook recebido.

Cada webhook vem com um header `X-Astronpay-Signature` no formato `sha256=<hex>`. O valor é o HMAC-SHA256 do **body bruto** (raw bytes, não JSON parseado), usando o `webhookSecret` que você configurou.

<Warning>
  Valide **antes** de parsear ou agir. Um webhook sem assinatura válida NÃO é da Astron Pay — descarte.
</Warning>

## Algoritmo

```
signature = "sha256=" + HMAC_SHA256(key=webhookSecret, data=rawBody).hexdigest()
```

* Chave: o `webhookSecret` configurado via `PATCH /api/v1/webhook/config`.
* Dados: o body bruto da requisição (bytes, antes de qualquer parsing).
* Formato: `sha256=` + hex lowercase.

## Receita

1. Leia o header `X-Astronpay-Signature` (ex.: `sha256=abc123...`).
2. Remova o prefixo `sha256=` para obter o hex recebido.
3. Calcule `HMAC_SHA256(webhookSecret, rawBody)` e encode como hex.
4. Compare os dois com comparação **em tempo constante** (`crypto.timingSafeEqual` ou equivalente).

## Node.js

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(
  signatureHeader: string,
  rawBody: Buffer,
  secret: string,
): boolean {
  // Header format: "sha256=<hex>"
  const hex = signatureHeader.startsWith('sha256=')
    ? signatureHeader.slice(7)
    : signatureHeader;

  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');

  const sigBuf = Buffer.from(hex, 'hex');
  const expBuf = Buffer.from(expected, 'hex');

  if (sigBuf.length !== expBuf.length) return false;
  return timingSafeEqual(sigBuf, expBuf);
}
```

Em um handler Express:

```ts theme={null}
import express from 'express';

app.post(
  '/webhooks/astronpay',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('X-Astronpay-Signature') ?? '';
    if (!verifyWebhook(signature, req.body, process.env.ASTRONPAY_WEBHOOK_SECRET!)) {
      return res.status(401).send('invalid signature');
    }
    const event = JSON.parse(req.body.toString('utf8'));
    // ... processar de forma assíncrona
    res.sendStatus(200);
  },
);
```

## Python (FastAPI)

```python theme={null}
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SECRET = b"meu-webhook-secret"

@app.post("/webhooks/astronpay")
async def handler(request: Request):
    raw = await request.body()
    sig_header = request.headers.get("x-astronpay-signature", "")
    hex_received = sig_header.removeprefix("sha256=")

    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(hex_received, expected):
        raise HTTPException(status_code=401, detail="invalid signature")

    event = await request.json()
    # ... processar
    return {"ok": True}
```

## Idempotência

Use o header `X-Astronpay-Delivery` (ou o campo `deliveryId` no payload) para detectar entregas duplicadas. Armazene os IDs já processados e ignore repetições — a Astron Pay pode reenviar o mesmo evento em caso de falha de rede.

## Armadilhas comuns

* **Body alterado por middleware JSON**: use sempre o body **raw** antes de parsing. Em Express, `express.raw()` é obrigatório — `express.json()` altera os bytes.
* **Comparação `==` simples**: vulnerável a timing attacks. Use `timingSafeEqual` / `hmac.compare_digest`.
* **Secret errado**: se rotacionou o `webhookSecret`, atualize imediatamente — o anterior é invalidado na mesma chamada.
* **Prefixo `sha256=`**: lembre de remover antes de comparar os hashes.
