Todo webhook enviado pela AbacatePay inclui dois mecanismos de segurança: um secret na URL e uma assinatura HMAC no header. Use os dois juntos.
1. Secret na URL
Ao criar o webhook, você define um secret. A AbacatePay inclui esse valor como query parameter em cada requisição:
https://meusite.com/webhook/abacatepay?webhookSecret=SEU_SECRET
Seu backend valida antes de qualquer processamento:
if (req.query.webhookSecret !== process.env.WEBHOOK_SECRET) {
return res.status(401).json({ error: "Unauthorized" });
}
2. Assinatura HMAC
Mesmo que alguém descubra sua URL e seu secret, a assinatura HMAC garante que o corpo da requisição não foi alterado e que o evento realmente veio da AbacatePay.
O header enviado é:
X-Webhook-Signature: <assinatura em base64>
A assinatura é calculada com HMAC-SHA256 sobre o corpo raw da requisição usando a chave pública da AbacatePay.
Validação em Node.js
import crypto from "node:crypto";
const ABACATEPAY_PUBLIC_KEY =
"t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMUgo6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmUCKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1JhNHDX9";
export function verifyAbacateSignature(rawBody: string, signatureFromHeader: string): boolean {
const expectedSig = crypto
.createHmac("sha256", ABACATEPAY_PUBLIC_KEY)
.update(Buffer.from(rawBody, "utf8"))
.digest("base64");
const A = Buffer.from(expectedSig);
const B = Buffer.from(signatureFromHeader);
return A.length === B.length && crypto.timingSafeEqual(A, B);
}
Validação em Python
import hmac
import hashlib
import base64
ABACATEPAY_PUBLIC_KEY = "t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMUgo6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmUCKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1JhNHDX9"
def verify_abacate_signature(raw_body: bytes, signature_from_header: str) -> bool:
expected = hmac.new(
ABACATEPAY_PUBLIC_KEY.encode("utf-8"),
raw_body,
hashlib.sha256
).digest()
expected_b64 = base64.b64encode(expected).decode("utf-8")
return hmac.compare_digest(expected_b64, signature_from_header)
Validação em Go
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)
const abacatePublicKey = "t9dXRhHHo3yDEj5pVDYz0frf7q6bMKyMRmxxCPIPp3RCplBfXRxqlC6ZpiWmOqj4L63qEaeUOtrCI8P0VMUgo6iIga2ri9ogaHFs0WIIywSMg0q7RmBfybe1E5XJcfC4IW3alNqym0tXoAKkzvfEjZxV6bE0oG2zJrNNYmUCKZyV0KZ3JS8Votf9EAWWYdiDkMkpbMdPggfh1EqHlVkMiTady6jOR3hyzGEHrIz2Ret0xHKMbiqkr9HS1JhNHDX9"
func VerifySignature(rawBody []byte, signatureFromHeader string) bool {
mac := hmac.New(sha256.New, []byte(abacatePublicKey))
mac.Write(rawBody)
expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureFromHeader))
}
Use sempre timingSafeEqual (ou equivalente) ao comparar assinaturas — nunca ==. Comparações diretas são vulneráveis a timing attacks.
Retentativas
Se seu endpoint não retornar 2xx dentro do timeout, a AbacatePay tenta reenviar o evento automaticamente com backoff progressivo.
O que pode causar retentativa:
- Timeout na conexão
- Resposta com status
5xx
- Resposta com status
4xx (exceto 200)
Boas práticas para lidar com retentativas:
Idempotência é obrigatória
Armazene o campo id de cada evento recebido. Antes de processar, verifique se esse ID já foi tratado anteriormente. Eventos duplicados são raros mas acontecem.
// Exemplo de controle de idempotência
const eventId = req.body.id; // ex: "log_abc123xyz"
const alreadyProcessed = await db.events.findOne({ id: eventId });
if (alreadyProcessed) {
return res.status(200).json({ ok: true }); // responde 200 mas não processa de novo
}
await processEvent(req.body);
await db.events.insert({ id: eventId, processedAt: new Date() });
return res.status(200).json({ ok: true });
Checklist de segurança
- Use HTTPS — nunca HTTP em produção
- Valide o secret na query string
- Valide a assinatura HMAC do header
- Responda 200 OK somente após concluir o processamento
- Implemente idempotência usando o
id do evento
- Não valide o payload inteiro com schemas rígidos (como Zod) — campos novos podem ser adicionados futuramente