Eventos
Cada webhook assina uma lista de eventos ou * (todos, inclusive os futuros).
Contatos: contact.created contato criado · contact.updated contato editado · contact.stage_changed contato mudou de etapa
Orçamentos: quote.sent orçamento enviado · quote.viewed orçamento visualizado · quote.accepted orçamento aceito · quote.declined orçamento recusado
Contratos: contract.sent contrato enviado · contract.signed contrato assinado · contract.declined contrato recusado
Cobranças: invoice.created cobrança criada · invoice.paid cobrança paga · invoice.overdue cobrança atrasada · invoice.canceled cobrança cancelada
Agenda: appointment.created compromisso criado · appointment.confirmed compromisso confirmado · appointment.canceled compromisso cancelado · appointment.done serviço concluído
Carteira: payout.paid saque pago
Notas fiscais: nfse.issued nota fiscal emitida
Mais ping, enviado pelo botão Enviar teste.
Envelope
O corpo é sempre o mesmo envelope; data é o recurso no mesmo formato da API (mais a chave object) e, quando o evento tem contexto próprio (etapa anterior, motivo da recusa, meio de pagamento), ele vem em data.event_meta.
{
"id": "evt_1234",
"event": "invoice.paid",
"created_at": "2026-10-19T14:05:33-03:00",
"api_version": "2026-10-19",
"account": { "id": 2, "uuid": "44494753-…", "name": "Bruno Fotografia" },
"data": {
"object": "invoice", "id": 87, "number_label": "COB-0087", "status": "paga",
"amount_cents": 45000, "paid_amount_cents": 45000, "paid_method": "pix_platform",
"paid_at": "2026-10-19T14:05:20-03:00", "contact_id": 12,
"public_url": "https://appdomei.com/pagar/…", "event_meta": { "actor": "system", "method": "pix_platform" }
}
}
Cabeçalhos
Content-Type: application/json; charset=utf-8 e User-Agent: AppDoMEI-Webhooks/1.0 (+https://appdomei.com/desenvolvedores)
X-ADM-Event — nome do evento (invoice.paid)
X-ADM-Delivery — id da entrega (dlv_…). Use como chave de idempotência: após uma falha a mesma entrega chega de novo com o mesmo id.
X-ADM-Webhook — uuid do webhook; X-ADM-Attempt — número da tentativa (1…7)
X-ADM-Signature — t=<unix>,v1=<hmac> (abaixo)
- Cabeçalhos extras que você cadastrar (ex.:
Authorization exigido pelo seu destino)
Verificar a assinatura
Ao criar o webhook você recebe um segredo whsec_… (mostrado uma vez). Para cada entrega, v1 é o HMAC-SHA256 de t + "." + corpo — o corpo são os bytes crus da requisição, antes de decodificar o JSON. Recuse se t estiver a mais de 300 s do seu relógio e compare em tempo constante. O cabeçalho pode trazer mais de um v1 durante uma rotação de segredo; aceite se qualquer um bater.
// PHP
$secret = getenv('ADM_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$parts = [];
foreach (explode(',', $_SERVER['HTTP_X_ADM_SIGNATURE'] ?? '') as $p) { [$k, $v] = explode('=', trim($p), 2); $parts[$k] = $v; }
$ok = abs(time() - (int) $parts['t']) <= 300
&& hash_equals(hash_hmac('sha256', $parts['t'] . '.' . $body, $secret), $parts['v1'] ?? '');
if (!$ok) { http_response_code(401); exit; }
http_response_code(200); // responda antes de processar
$event = json_decode($body, true);
// Node.js (Express com express.raw)
const crypto = require('crypto');
app.post('/webhooks/appdomei', express.raw({ type: '*/*' }), (req, res) => {
const parts = Object.fromEntries((req.get('X-ADM-Signature') || '').split(',').map(p => p.trim().split('=')));
const expected = crypto.createHmac('sha256', process.env.ADM_WEBHOOK_SECRET).update(`${parts.t}.${req.body}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
if (!fresh || expected.length !== (parts.v1 || '').length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) return res.sendStatus(401);
res.sendStatus(200);
const event = JSON.parse(req.body);
});
# Python (Flask)
import hmac, hashlib, time, os
@app.post('/webhooks/appdomei')
def appdomei():
body = request.get_data()
parts = dict(p.strip().split('=', 1) for p in request.headers.get('X-ADM-Signature', '').split(','))
expected = hmac.new(os.environ['ADM_WEBHOOK_SECRET'].encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts['t'])) > 300 or not hmac.compare_digest(expected, parts.get('v1', '')):
abort(401)
return '', 200
Retentativas e desativação
Qualquer resposta 2xx em até 10 s conta como entregue. Outra resposta, timeout ou redirecionamento (nunca seguido) agenda nova tentativa em 1 min, 5 min, 30 min, 2 h, 12 h, 24 h após cada falha (6 retentativas, ~1 dias). Esgotadas, a entrega fica como desistida e pode ser reenviada pela tela. Após 25 falhas seguidas o webhook é desativado e o dono da conta é avisado no app e por e-mail; corrigido o destino, basta ativar de novo.
Boas práticas
- Responda 200 primeiro e processe depois (fila). Trabalho pesado dentro da requisição vira timeout e retentativa.
- Trate entregas repetidas: guarde o
X-ADM-Delivery (ou o id do evento) e ignore o que já processou.
- Não confie só na URL secreta: verifique a assinatura. Se o seu destino não lê cabeçalhos (Google Apps Script), use um token na própria URL.
- A ordem não é garantida entre eventos diferentes; use
created_at e o estado em data.status.
- Só
https em host público (porta 443 ou 8443). Endereços internos, localhost e IPs privados são recusados no cadastro e no envio (o DNS é resolvido a cada entrega).
- Teste com Enviar teste (evento
ping) e acompanhe código, tempo e corpo da resposta na ficha do webhook.
Receitas prontas (Zapier, Make, n8n, Google Sheets) em Ajuda → Integrações. Webhooks também podem ser gerenciados pela API (escopo webhooks:manage).