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.

EscopoOperação
fullCriar, listar, pausar ou desativar webhook
read_onlyLer o feed
send_onlyEnviar mensagem (POST /api/v1/messages)

Cadastrar

POST /api/v1/webhooksbash
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"}'
201 Createdjson
{
  "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ó

Guarde o 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"}'
StatusEfeito
activeRecebe entregas. Voltar pra active zera as falhas e tenta na hora.
pausedNão recebe. O cursor fica parado; ao reativar, recebe tudo o que acumulou.
disabledRemoçã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.

POST na sua URLjson
{
  "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
    }
  ]
}
HeaderConteúdo
X-ZMCP-Eventmessages.batch
X-ZMCP-Webhook-Idid da assinatura
X-ZMCP-Delivery-Idid único da tentativa (muda a cada retry)
X-ZMCP-Timestampsegundos Unix da assinatura
X-ZMCP-Signaturesha256=<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.

Node / Workerstypescript
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)
}
Pythonpython
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)

A mesma mensagem pode chegar de novo: retry depois de um 2xx perdido no caminho, ou mensagem regravada pelo WhatsApp (edição, sincronização de histórico). Responda 2xx rápido e processe depois.

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.

GET /api/v1/feedbash
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

O corpo leva o texto integral das conversas, incluindo documentos e endereços enviados pelos contatos. Mascare no seu sistema o que não precisa guardar.