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
- Você cadastra uma URL https e marca os eventos que quer receber.
- Guardamos um segredo, mostrado uma vez só, que assina cada envio.
- Quando algo acontece, mandamos um POST com o corpo do evento.
- 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.
- 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"]
}'
const res = await fetch("https://api.wevi.chat/functions/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer wevi_SUA_CHAVE",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://seusistema.com/webhooks/wevichat",
events: ["message.received", "conversation.handoff_requested"],
}),
});
const { id, secret } = await res.json();
// Guarde o secret agora: ele não aparece de novo.
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 oGETcorrespondente devolve. Você aprende a forma do contato uma vez e ela vale na leitura e no webhook.- Os demais campos de
datasão o contexto daquele evento: o que mudou, de onde para onde, por quem. data.objectvemnullquando 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.