Webhooks

Em vez de você ficar perguntando "aconteceu alguma coisa?", a Wevichat avisa. Cadastre uma URL, escolha os eventos, e cada acontecimento vira um POST assinado no seu sistema, em segundos.

Escopo necessário: webhooks:manage. Limite: 60 requisições por minuto.

Como funciona

  1. Você cadastra uma URL https e marca os eventos que quer receber.
  2. Guardamos um segredo, mostrado uma vez só, que assina cada envio.
  3. Quando algo acontece, mandamos um POST com o corpo do evento.
  4. Se sua URL não responder com 2xx, tentamos de novo: 1 min, 5 min, 30 min, 2 h, 12 h e 24 h depois.
  5. Cada tentativa fica registrada, com o código HTTP e a resposta, e pode ser reenviada por você.

Do fato até a entrega costuma passar cerca de um segundo.

Cadastrar

curl -X POST "https://api.wevi.chat/functions/v1/webhooks" \
  -H "Authorization: Bearer wevi_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://seusistema.com/webhooks/wevichat",
    "description": "integração com o CRM",
    "events": ["message.received", "conversation.handoff_requested", "contact.stage_changed"]
  }'

Resposta (201):

{
  "id": "835eb7a8-efbe-4604-910e-fd1d8cfd1e9c",
  "url": "https://seusistema.com/webhooks/wevichat",
  "description": "integração com o CRM",
  "events": ["message.received", "conversation.handoff_requested"],
  "active": true,
  "health": {
    "failure_count": 0,
    "failing_since": null,
    "last_success_at": null,
    "disabled_at": null,
    "disabled_reason": null
  },
  "created_at": "2026-09-08T04:22:07Z",
  "updated_at": "2026-09-08T04:22:07Z",
  "secret": "whsec_105ad5941ea851312f0d1df16ca79bfc177b7dee8586ed1692585fe8c6065367"
}

O secret só aparece aqui e em POST /webhooks/{id}/rotate-secret.

Assinar um recurso inteiro

"contact.*" assina todos os eventos de contato de uma vez. O que fica gravado é a lista expandida, então você enxerga exatamente o que assinou.

{ "events": ["contact.*", "conversation.resolved"] }

O corpo do evento

{
  "id": "evt_83b33d07095c40dcb2f2aaa7a3949f57",
  "type": "contact.stage_changed",
  "api_version": "v1",
  "created_at": "2026-09-08T04:25:31Z",
  "org_id": "746853d9-c2c9-44b1-8557-56e2129953dc",
  "data": {
    "object": { "id": "cccc...", "name": "Maria Souza", "email": "...", "tags": [], "pipelines": [] },
    "pipeline_id": "c24cde19-...",
    "from_stage_id": "00741eec-...",
    "to_stage_id": "3af8c4a2-...",
    "lost": false,
    "lost_reason": null,
    "source": "api"
  }
}
  • data.object é o recurso no mesmo formato que o GET correspondente devolve. Você aprende a forma do contato uma vez e ela vale na leitura e no webhook.
  • Os demais campos de data são o contexto daquele evento: o que mudou, de onde para onde, por quem.
  • data.object vem null quando o recurso foi apagado entre o evento e a entrega.

Verificar a assinatura

Todo envio traz o header:

Wevi-Signature: t=1788841497,v1=d3f85f1eb6d20cb72e50c006e1e840922987c0c302b0bedf70733fa142ee19f2

v1 é um HMAC SHA-256, com o seu segredo, sobre a string <t>.<corpo cru>. Use o corpo exatamente como chegou, antes de qualquer parse: reserializar o JSON muda a ordem das chaves e a assinatura não bate.

import crypto from "node:crypto";

function assinaturaConfere(corpoCru, header, segredo, toleranciaSegundos = 300) {
  const partes = Object.fromEntries(
    header.split(",").map(p => p.split("=", 2)),
  );
  const t = Number(partes.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranciaSegundos) return false;

  const esperado = crypto
    .createHmac("sha256", segredo)
    .update(`${t}.${corpoCru}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(esperado),
    Buffer.from(partes.v1),
  );
}

// Express: use express.raw para receber o corpo sem parse.
app.post("/webhooks/wevichat", express.raw({ type: "application/json" }), (req, res) => {
  const cru = req.body.toString("utf8");
  if (!assinaturaConfere(cru, req.header("Wevi-Signature"), process.env.WEVI_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const evento = JSON.parse(cru);
  // ... trate o evento e responda rápido
  res.sendStatus(200);
});
import hmac, hashlib, time

def assinatura_confere(corpo_cru: bytes, header: str, segredo: str, tolerancia=300) -> bool:
    partes = dict(p.split("=", 1) for p in header.split(","))
    t = int(partes["t"])
    if abs(time.time() - t) > tolerancia:
        return False
    esperado = hmac.new(
        segredo.encode(),
        f"{t}.".encode() + corpo_cru,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(esperado, partes["v1"])

A checagem do t protege contra alguém regravar um envio antigo. Cinco minutos de tolerância é o suficiente.

Outros headers

Header Conteúdo
Wevi-Event Tipo do evento, ex: message.received
Wevi-Event-Id Id do evento. Use para não processar duas vezes
Wevi-Delivery-Id Id desta tentativa de entrega
Wevi-Attempt Número da tentativa, começando em 1

Um mesmo evento pode chegar mais de uma vez se a sua resposta demorar ou se você reenviar. Guarde o Wevi-Event-Id que você já processou e ignore repetido.

O que esperamos da sua URL

  • Responder 2xx para qualquer coisa que você recebeu, mesmo que vá processar depois.
  • Responder em até 10 segundos. Passou disso, tratamos como falha e tentamos de novo.
  • Aceitar https. URL http, endereço de rede privada e nome local são recusados no cadastro e revalidados a cada tentativa.

Guarde o evento numa fila do seu lado e responda na hora. Processar antes de responder é o caminho mais rápido para tomar retentativa à toa.

Quando desistimos

Depois de 6 tentativas sem sucesso, aquela entrega vira exhausted e para. Um endpoint que passa 72 horas sem nenhuma entrega bem sucedida é desativado automaticamente, e o motivo aparece em health.disabled_reason. Reativar é um PATCH com {"active": true}, que zera o histórico de falhas.

Endpoints

Método Rota Faz
GET /webhooks Lista os endpoints cadastrados
POST /webhooks Cadastra (devolve o segredo uma vez)
GET /webhooks/event-types Todos os tipos de evento disponíveis
GET /webhooks/{id} Um endpoint
PATCH /webhooks/{id} Muda url, eventos, descrição ou ativo
DELETE /webhooks/{id} Remove o endpoint e o histórico dele
POST /webhooks/{id}/test Dispara um evento webhook.test
POST /webhooks/{id}/rotate-secret Gera um segredo novo
GET /webhooks/{id}/deliveries Tentativas recentes, com resposta e tempo
POST /webhooks/{id}/deliveries/{delivery_id}/retry Reenvia uma entrega

Testar antes de depender

curl -X POST "https://api.wevi.chat/functions/v1/webhooks/{id}/test" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

Depois consulte o resultado:

curl "https://api.wevi.chat/functions/v1/webhooks/{id}/deliveries?limit=5" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"
{
  "data": [
    {
      "id": "c5a1bbc7-...",
      "event_id": "9e361d90-...",
      "event_type": "webhook.test",
      "status": "succeeded",
      "attempt": 1,
      "response_status": 200,
      "response_body": "ok",
      "error": null,
      "duration_ms": 159,
      "delivered_at": "2026-09-08T04:25:33Z",
      "created_at": "2026-09-08T04:25:31Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

status vale pending (na fila), delivering, succeeded, failed (vai tentar de novo) ou exhausted (desistimos).

Catálogo de eventos

Evento Quando
message.received Cliente mandou mensagem
message.sent Mensagem saiu do agente, da equipe ou da API
message.delivered Chegou no aparelho
message.read Foi lida
message.failed Falhou no envio, com o motivo da Meta
conversation.created Conversa nova
conversation.handoff_requested Pediram atendente humano, com o motivo
conversation.taken_over Humano assumiu
conversation.released Devolvida para a IA
conversation.resolved Encerrada, com quem encerrou
conversation.reopened Reaberta
conversation.rated Cliente avaliou, com nota e comentário
conversation.event Evento de conversão detectado pelo agente
contact.created Contato criado
contact.updated Nome, email, telefone, campos ou notas mudaram
contact.merged Fundido em outro contato
contact.tag_added Etiqueta aplicada
contact.tag_removed Etiqueta removida
contact.stage_changed Mudou de etapa no funil, ou foi marcado como perdido
contact.assigned Ganhou dono de carteira
contact.unassigned Ficou sem dono
contact.opted_out Pediu para não receber mais
contact.opted_in Voltou a aceitar
automation.run_started Automação começou para um contato
automation.run_completed Automação terminou ou saiu por condição
automation.run_failed Automação falhou
campaign.started Campanha começou a disparar
campaign.completed Campanha terminou, com as contagens
template.status_changed Template aprovado, rejeitado ou pausado pela Meta
connection.config_error Número de WhatsApp com erro de configuração

Além desses, webhook.test é enviado só quando você usa o endpoint de teste.

Sem URL pública? Use /events

Se o seu ambiente não recebe requisição de fora (n8n numa rede fechada, script que roda de hora em hora), consulte os mesmos eventos:

curl "https://api.wevi.chat/functions/v1/events?since=2026-09-08T00:00:00Z&limit=100" \
  -H "Authorization: Bearer wevi_SUA_CHAVE"

Aceita type para filtrar um evento específico, mais limit e cursor de paginação. O corpo de cada item é idêntico ao que o webhook entrega.

Só existem eventos gravados para tipos que a organização assinou em algum endpoint. Se você quer só consultar, cadastre um endpoint com os eventos que interessam e deixe ele pausado: os eventos continuam sendo gravados e aparecem aqui.

Pela interface

Em Conta → Desenvolvedores → Webhooks dá para fazer tudo isso sem escrever código: cadastrar, escolher os eventos por grupo, disparar um teste, ver o histórico de entregas com a resposta de cada uma, reenviar e trocar o segredo.

Onde o segredo fica guardado

O segredo que assina cada entrega é gravado cifrado, com a mesma criptografia das chaves de IA da organização. Ele só volta a existir em texto puro dentro do processo que faz a entrega, no instante de assinar.

Isso não muda nada do seu lado. Muda o que acontece se alguém puser as mãos numa cópia do banco: sem a chave de aplicação, o segredo não serve para forjar evento nenhum.

Vale lembrar do que continua sendo com você: o segredo aparece uma vez só, na criação e na rotação. Não há como consultá-lo depois.

Quando algo para de chegar

Um webhook falhando há mais de uma hora aparece com aviso no painel, em Minha conta → Desenvolvedores, e em alerts no `GET /org`. Antes, o único aviso era o email que sai depois de 3 dias, tarde demais para quem depende do evento.

Os eventos não se perdem nesse meio tempo: ficam na fila e são reenviados por até 24 horas.