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.
Valide antes de parsear ou agir. Um webhook sem assinatura válida NÃO é da Astron Pay — descarte.
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
- Leia o header
X-Astronpay-Signature (ex.: sha256=abc123...).
- Remova o prefixo
sha256= para obter o hex recebido.
- Calcule
HMAC_SHA256(webhookSecret, rawBody) e encode como hex.
- Compare os dois com comparação em tempo constante (
crypto.timingSafeEqual ou equivalente).
Node.js
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:
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)
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.