Como funciona
Você cadastra um endpoint em /empresa/webhooks e escolhe quais eventos receber. Quando o evento acontece, mandamos um POST com o payload JSON pra essa URL, assinado com HMAC-SHA256.
Você valida a assinatura do seu lado pra garantir que veio da gente, processa, e responde com 2xx. Se der ruim, a gente tenta de novo com backoff.
Eventos disponíveis
| Tipo | Quando dispara |
|---|---|
| consultation.completed | PDF da consulta foi gerado e signed URL está disponível |
| consultation.failed | Falha em alguma API externa durante processamento. Folhas devolvidas. |
| payment.confirmed | Pagamento via Asaas (PIX/boleto/cartão) confirmado pra essa consulta |
Formato do payload
Todos os eventos seguem o mesmo envelope. O data varia por evento.
{
"id": "evt_a1b2c3d4-...", // ID único do evento
"type": "consultation.completed",
"created_at": "2026-05-22T15:43:00.000Z",
"data": {
"consultation_id": "...",
"external_reference": "ticket-42",
"plan_id": "cpf-investigacao",
"category": "cpf",
"target": "12345678900",
"status": "completed",
"pdf_url": "https://...supabase.co/storage/...",
"completed_at": "2026-05-22T15:43:08.000Z"
}
}Assinatura HMAC
Todo POST traz o header x-capivara-signature no formato:
x-capivara-signature: t=1714994580,v1=ab12cd34ef56...
Onde t é o timestamp Unix do envio e v1 é o HMAC-SHA256 hex de {t}.{rawBody} usando o secret do endpoint.
Validação em Node.js
import crypto from "crypto";
function verifyCapivara(rawBody, sigHeader, secret) {
const parts = sigHeader.split(",").reduce((acc, p) => {
const [k, v] = p.split("=");
acc[k] = v;
return acc;
}, {});
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1)
);
}
// Express
app.post("/webhooks/capivara", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["x-capivara-signature"];
if (!verifyCapivara(req.body.toString(), sig, process.env.WHSEC)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString());
// processa...
res.status(200).send("ok");
});Validação em Python
import hmac, hashlib
def verify_capivara(raw_body: bytes, sig_header: str, secret: str) -> bool:
parts = dict(p.split("=") for p in sig_header.split(","))
expected = hmac.new(
secret.encode(),
f"{parts['t']}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Validação em PHP
function verifyCapivara($rawBody, $sigHeader, $secret) {
$parts = [];
foreach (explode(",", $sigHeader) as $kv) {
[$k, $v] = explode("=", $kv);
$parts[$k] = $v;
}
$expected = hash_hmac("sha256", $parts['t'] . "." . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}timing-safe ( timingSafeEqual /compare_digest /hash_equals). Não use === ou ==.Retry policy
Se sua URL não retornar 2xx em até 15 segundos, retentamos com backoff exponencial:
| Tentativa | Atraso |
|---|---|
| 1 | imediato |
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| 6 | 6 horas |
| 7 | 24 horas |
Após 6 tentativas sem sucesso, marcamos como exhausted. Você pode reenviar manualmente em /empresa/webhooks.
Boas práticas
- Responda rápido. Responda
200 OKem até 15s. Enfileire processamento pesado em background. - Idempotência no seu lado. Use o
iddo evento pra deduplicar caso retentemos um evento já processado. - Tolere chegada fora de ordem. Eventos podem chegar fora de ordem por causa de retries. Use
created_atpra ordenar. - Valide o timestamp. Rejeite eventos com
tmuito antigo (ex: > 5 min) pra mitigar replay attack. - HTTPS obrigatório. Não aceitamos URLs
http://em produção.