Skip to main content
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

  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

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.