API Capivara
Webhooks

Documentação de Webhooks

Notificação em tempo real quando algo acontece na sua conta. Assinatura HMAC-SHA256 + retry exponencial automático.

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

TipoQuando dispara
consultation.completedPDF da consulta foi gerado e signed URL está disponível
consultation.failedFalha em alguma API externa durante processamento. Folhas devolvidas.
payment.confirmedPagamento 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']);
}
Sempre use comparação 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:

TentativaAtraso
1imediato
21 minuto
35 minutos
430 minutos
52 horas
66 horas
724 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 OK em até 15s. Enfileire processamento pesado em background.
  • Idempotência no seu lado. Use o id do 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_at pra 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.