Webhooks
Receba cada mensagem nova do WhatsApp no seu sistema, em lote assinado, sem ficar consultando a API. Perdeu alguma entrega? Recupere pelo feed com o mesmo cursor.
Chaves
Use uma chave full só pra cadastrar, guardada fora do app. No dia a dia, uma read_only pro feed e uma send_only pro envio.
| Escopo | Operação |
|---|---|
| full | Criar, listar, pausar ou desativar webhook |
| read_only | Ler o feed |
| send_only | Enviar mensagem (POST /api/v1/messages) |
Cadastrar
curl -s -X POST "https://sua-instancia.zmcp.me/api/v1/webhooks" \
-H "X-API-Key: zmcp_chave_full" \
-H "Content-Type: application/json" \
-d '{"url": "https://meu-sistema.com/api/zmcp/webhook"}'{
"id": 1,
"url": "https://meu-sistema.com/api/zmcp/webhook",
"status": "active",
"cursor": 48213,
"consecutive_failures": 0,
"next_attempt_at": null,
"last_delivered_at": null,
"last_error": null,
"created_at": "2026-09-30 12:00:00",
"secret": "9f2c...64 hex"
}O segredo aparece uma vez só
secret no cofre do seu sistema. A listagem nunca devolve o segredo.A URL precisa ser https e apontar pra host público. A assinatura começa no cursor atual: recebe só o que chegar depois do cadastro. O histórico anterior vem pelo feed.
Gerir
curl -s "https://sua-instancia.zmcp.me/api/v1/webhooks" -H "X-API-Key: zmcp_chave_full"
curl -s -X PATCH "https://sua-instancia.zmcp.me/api/v1/webhooks/1" \
-H "X-API-Key: zmcp_chave_full" -H "Content-Type: application/json" \
-d '{"status": "paused"}'| Status | Efeito |
|---|---|
| active | Recebe entregas. Voltar pra active zera as falhas e tenta na hora. |
| paused | Não recebe. O cursor fica parado; ao reativar, recebe tudo o que acumulou. |
| disabled | Remoção. Some da listagem e não volta. |
A listagem mostra consecutive_failures, next_attempt_at e last_error: é por ali que se vê um destino fora do ar.
Entrega
Até 100 mensagens por lote. Com atraso acumulado, a instância manda vários lotes seguidos. Quando a instância conhece o telefone de um @lid, o chat_jid já sai pelo telefone.
{
"type": "messages.batch",
"version": 1,
"webhook_id": 1,
"sent_at": "2026-09-30T12:00:05+00:00",
"next_cursor": 48215,
"has_more": false,
"messages": [
{
"cursor": 48214,
"id": "3EB0A1B2C3D4",
"chat_jid": "5511999999999@s.whatsapp.net",
"chat_phone": "5511999999999",
"chat_name": "Ana",
"sender": "5511999999999",
"from_me": false,
"text": "Oi, tudo bem?",
"timestamp": "2026-09-30 09:00:03-03:00",
"media_type": null,
"is_group": false
}
]
}| Header | Conteúdo |
|---|---|
| X-ZMCP-Event | messages.batch |
| X-ZMCP-Webhook-Id | id da assinatura |
| X-ZMCP-Delivery-Id | id único da tentativa (muda a cada retry) |
| X-ZMCP-Timestamp | segundos Unix da assinatura |
| X-ZMCP-Signature | sha256=<hex> |
Verificar a assinatura
HMAC-SHA256(secret, timestamp + "." + corpo_bruto), em hex, com prefixo sha256=. Compare em tempo constante, sobre o corpo bruto, e recuse timestamp com mais de 5 minutos de diferença.
async function verify(secret: string, ts: string, body: string, signature: string) {
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
)
const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${ts}.${body}`))
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join('')
return timingSafeEqual(`sha256=${hex}`, signature)
}expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
ok = abs(time.time() - int(ts)) <= 300 and hmac.compare_digest(expected, signature)Garantias
Entrega pelo menos uma vez, em ordem, por assinatura. O cursor só avança quando sua URL responde 2xx. Qualquer outra resposta, timeout de 15s ou erro de rede agenda nova tentativa: 30s, 60s, 120s... até 1h.
Deduplique por (id, chat_jid)
Feed
Mesmo formato de mensagem, paginado pelo cursor. limit vai de 1 a 500. Repita com after=next_cursor até has_more ser false. Serve pra carga inicial, reconciliação periódica e pra máquinas sem URL pública.
curl -s "https://sua-instancia.zmcp.me/api/v1/feed?after=0&limit=500" \
-H "X-API-Key: zmcp_chave_read_only"{ "messages": [ ... ], "next_cursor": 500, "has_more": true }Dados sensíveis