Webhooks de saída
O webhook de saída avisa o seu sistema quando algo acontece no Papeador. Cada aviso chega por POST, em JSON, no endereço que você cadastrar.
Eventos
Seção intitulada “Eventos”| Evento | Quando |
|---|---|
compra.criada |
compra registrada |
tag.adicionada |
etiqueta adicionada |
projeto.entrou |
o contato entrou num projeto |
estagio.mudou |
o contato mudou de estágio |
conversa_site.criada |
conversa nova no chat do site |
conversa_site.resolvida |
a equipe marcou a conversa do chat do site como resolvida |
Cadastrar
Seção intitulada “Cadastrar”- No Papeador, abra Integrações → Webhooks de saída → Novo destino (só o dono e os admins).
- Cole o endereço do seu sistema e escolha os eventos.
- Guarde o segredo para conferir a assinatura (abaixo).

O aviso
Seção intitulada “O aviso”{ "id": "cm2x9k7d10001", "tipo": "compra.criada", "criado_em": "2026-10-23T14:30:00.000Z", "contato": { "id": "cm2x8a1b20000", "nome": "Ana Souza", "email": "ana@exemplo.com", "telefone": "5551999990000", "tags": ["cliente", "curso-x"] }, "dados": { "compra": { "id": "cm2x9k7d10002", "descricao": "Curso X", "valor": 197, "status": "paid", "data": "2026-10-23T14:30:00.000Z", "produto_id": null } }}Os cabeçalhos trazem X-Papeador-Evento, X-Papeador-Entrega (o mesmo id do corpo: use para descartar repetidos) e X-Papeador-Assinatura. Os corpos de cada evento estão na referência.
No formato do seu sistema (de-para). No destino, escolha “Formato do destino” e monte o JSON que você espera receber, com {{caminho}} do aviso padrão ({{contato.email}}, {{dados.compra.valor}}). Sozinho num texto, {{caminho}} mantém o tipo (número, lista); junto com outro texto, vira texto. Dá para mandar também cabeçalhos extras, como Authorization. A assinatura vale sobre o corpo que sai.
Novas tentativas
Seção intitulada “Novas tentativas”Responda 2xx em até 10 segundos. Senão, o Papeador tenta de novo em 1 min, 5 min, 30 min, 2 h e 6 h, e depois desiste. A tela do destino mostra cada entrega, com a resposta, e o botão para reenviar.
Assinatura
Seção intitulada “Assinatura”Os avisos assinados levam o cabeçalho t=<unix>,v1=<hex>, em que v1 é o HMAC-SHA256 do segredo sobre <t>.<corpo bruto>. Confira com o corpo exatamente como chegou (antes de converter o JSON) e recuse se t estiver a mais de 15 minutos do relógio. O mesmo formato vale para os avisos que o seu sistema manda às conexões de entrada.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";function assinaturaOk(corpoBruto, cabecalho, segredo) { const p = Object.fromEntries(cabecalho.split(",").map((x) => x.split("="))); if (Math.abs(Date.now() / 1000 - Number(p.t)) > 900) return false; const esperado = createHmac("sha256", segredo).update(`${p.t}.`).update(corpoBruto).digest("hex"); return p.v1?.length === esperado.length && timingSafeEqual(Buffer.from(p.v1), Buffer.from(esperado));}Python
import hmac, hashlib, timedef assinatura_ok(corpo_bruto: bytes, cabecalho: str, segredo: str) -> bool: p = dict(x.split("=", 1) for x in cabecalho.split(",")) if abs(time.time() - int(p["t"])) > 900: return False esperado = hmac.new(segredo.encode(), f"{p['t']}.".encode() + corpo_bruto, hashlib.sha256).hexdigest() return hmac.compare_digest(p.get("v1", ""), esperado)PHP
function assinatura_ok(string $corpoBruto, string $cabecalho, string $segredo): bool { parse_str(str_replace(",", "&", $cabecalho), $p); if (abs(time() - (int) $p["t"]) > 900) return false; $esperado = hash_hmac("sha256", $p["t"] . "." . $corpoBruto, $segredo); return hash_equals($esperado, $p["v1"] ?? "");}