Webhooks
Receba eventos do Vamo no seu servidor, verifique a assinatura, trate repetições e gerencie endpoints pela API.
Um webhook é uma URL do seu sistema que o Vamo chama com um POST quando algo acontece: uma mensagem foi entregue, um pedido mudou de etapa, a Meta aprovou um modelo. Assim você não precisa ficar consultando a API.
Como funciona
- Você cadastra uma URL
https://e escolhe os eventos. - Quando um evento acontece, o Vamo envia um
POSTcom um JSON assinado. - O seu servidor responde com um status
2xxem até 10 segundos. - Qualquer outra resposta (inclusive
3xx), ou nenhuma, é uma falha, e o Vamo tenta de novo.
Você cadastra o endpoint no Vamo, em Desenvolvedores → Webhooks → Adicionar webhook, ou pela API com POST /webhooks. O Segredo de assinatura aparece uma única vez, ao criar.

Eventos
Assine os eventos um a um, ou "*" para todos. Repare em que situação cada um dispara: vários só refletem o que foi feito pela API.
| Evento | Dispara quando | data |
|---|---|---|
contact.created | Um contato é criado por POST /contacts, ou por POST /templates/send para um número novo. Ao fim de uma importação em massa, um único evento de resumo | { contact } ou { bulk, created, skipped, failed } |
contact.updated | PATCH /contacts/:id | { contact } |
contact.deleted | DELETE /contacts/:id | { contactId } |
conversation.created | Um envio pela API abre a primeira conversa do contato | { conversation } |
conversation.updated | PATCH /conversations/:id que não fecha a conversa | { conversation } |
conversation.closed | PATCH /conversations/:id com "status": "closed" | { conversation } |
message.created | Uma mensagem sai por POST /messages/send ou POST /templates/send. Ao fim de um envio em massa, um único evento de resumo | { message, conversationId, contactId } |
message.delivered | O WhatsApp confirma a entrega de qualquer mensagem da loja (IA, equipe, automação, campanha ou API) | { messageId, conversationId, waMessageId, status, … } |
message.failed | O WhatsApp avisa que uma mensagem da loja não foi entregue | Igual, com errorCode e errorMessage |
order.created | Um pedido é criado por POST /orders | { order } com items |
order.updated | Um pedido muda, venha de onde vier: a API, a equipe no Vamo, a loja conectada, a aprovação ou recusa de um pedido da IA, o fim do prazo de reserva, ou a IA trocando um pedido por outro | { order } sem items |
template.approved | A Meta aprova um modelo | { id, name, language, waTemplateId, status, previousStatus } |
template.rejected | A Meta rejeita um modelo | Igual, com reason |
store.connected | O WhatsApp da loja é conectado ou volta a funcionar | { storeId, phoneNumber, connectionMethod, provider } |
store.disconnected | O WhatsApp da loja é desconectado | { storeId, reason, provider } |
O que não gera evento hoje
Mensagens que o cliente envia e respostas da IA não geram message.created (elas aparecem só em message.delivered e message.failed, quando são da loja). Clientes que chegam pelo WhatsApp, contatos criados no Vamo e pedidos criados pela IA ou pela equipe não geram contact.created nem order.created. message.sent, campaign.created, campaign.sent e campaign.failed aparecem na lista de eventos e podem ser assinados, mas ainda não são enviados. Para conversas e mensagens dos clientes, consulte GET /conversations.
Em template.approved e template.rejected, o ID do modelo pode vir em id ou em templateId, conforme o caminho por onde a Meta avisou: leia os dois.
O que chega no seu servidor
O corpo é sempre { event, data, timestamp }:
{
"event": "message.delivered",
"data": {
"messageId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"conversationId": "5e4d3c2b-1a09-4f8e-8d7c-6b5a49382716",
"waMessageId": "wamid.HBgNNTUxMTk4NzY1NDMyMRUCABEYEjQ1",
"status": "delivered",
"errorCode": null,
"errorMessage": null,
"timestamp": "2026-10-31T13:20:09.000Z"
},
"timestamp": "2026-10-31T13:20:10.412Z"
}Mais um exemplo, um pedido enviado ao cliente:
{
"event": "order.updated",
"data": {
"order": {
"id": "8192a3b4-c5d6-4e7f-9a8b-92a3b4c5d6e7",
"contactId": "2c9d8e7f-6a5b-4c3d-9e2f-1a0b9c8d7e6f",
"status": "shipped",
"total": 462400,
"currency": "BRL",
"source": "api",
"updatedAt": "2026-10-31T16:00:00.000Z"
}
},
"timestamp": "2026-10-31T16:00:00.871Z"
}Os objetos contact, conversation, message e order têm os mesmos campos das respostas da API. Campos novos podem aparecer: ignore os que você não conhece.
Cabeçalhos
| Cabeçalho | Conteúdo |
|---|---|
Content-Type | application/json |
X-Webhook-Event | O evento, por exemplo order.updated |
X-Webhook-Timestamp | Hora da assinatura, em segundos Unix |
X-Webhook-Signature-V2 | sha256= + HMAC-SHA256 em hexadecimal de timestamp + "." + corpo |
X-Webhook-Signature | sha256= + HMAC-SHA256 em hexadecimal só do corpo (para integrações antigas) |
X-Webhook-Attempt | 1 na primeira tentativa, depois 2, 3… |
X-Vamo-Delivery-Id | ID da entrega (UUID), igual em todas as tentativas dela |
Verificar a assinatura
Verifique toda entrega antes de confiar nela. A chave do HMAC é o segredo inteiro, com o prefixo whsec_. Calcule sobre o corpo exatamente como chegou (os bytes crus, antes de qualquer JSON.parse), compare em tempo constante e recuse entregas assinadas há mais de 5 minutos: assim uma requisição capturada não pode ser repetida depois.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.VAMO_WEBHOOK_SECRET; // "whsec_..."
const TOLERANCE_SECONDS = 300;
const app = express();
function verify(rawBody, timestamp, signature) {
if (!timestamp || !signature) return false;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw: o corpo chega como Buffer, sem ser reescrito
app.post("/webhooks/vamo", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const ok = verify(rawBody, req.get("X-Webhook-Timestamp"), req.get("X-Webhook-Signature-V2"));
if (!ok) return res.status(401).send("assinatura inválida");
const { event, data } = JSON.parse(rawBody);
const deliveryId = req.get("X-Vamo-Delivery-Id");
// Responda logo e processe depois (fila); ignore deliveryId já processado
res.sendStatus(200);
enqueue({ deliveryId, event, data });
});Integrações antigas que verificam X-Webhook-Signature (HMAC só do corpo) continuam funcionando, mas essa assinatura não protege contra repetição. Prefira a V2.
Repetições, duplicatas e ordem
- Tentativas. Uma entrega que falha é tentada de novo até 5 vezes, com espera crescente de cerca de 1, 2, 4, 8 e 16 segundos entre elas: 6 tentativas no total, que terminam em um ou dois minutos. Depois disso, aquele evento não é reenviado: ele fica no histórico de entregas como falha.
- Duplicatas. Se o seu servidor processou mas respondeu tarde (mais de 10 segundos) ou com erro, a mesma entrega chega de novo com o mesmo
X-Vamo-Delivery-Id. Guarde os IDs já processados e ignore os repetidos. - Responda rápido. Grave o evento numa fila e responda
200antes de processar. - Ordem. Cada entrega é independente: um
message.deliveredpode chegar antes domessage.createdda mesma mensagem. Use os timestamps.
Desativação depois de 3 dias
Se um endpoint passar 3 dias sem nenhuma entrega bem-sucedida (contando da primeira falha depois do último sucesso), o Vamo o desativa:
statusviradisablede os eventos seguintes não são enviados a ele, nem depois.- O proprietário da organização recebe um e-mail com a URL e o último erro, e a página do webhook no Vamo mostra o motivo.
- Ele só volta pelo botão Testar e reativar, na página do webhook no Vamo: o Vamo envia um evento
pingassinado ({ "event": "ping", "deliveryId": "…", "data": {}, "timestamp": "…" }) e, se o seu servidor responder2xx, o endpoint volta aactive. Pela API não dá para reativar.
Um endpoint que você pausou (inactive) nunca é desativado, mas também não recebe nada: os eventos do período em que ficou pausado se perdem.
Requisitos da URL
https://, num endereço público da internet.localhost, IPs de rede privada, nomes internos, serviços comonip.ioe URLs com usuário e senha são recusados, ao cadastrar e a cada entrega.- Redirecionamentos não são seguidos: um
301ou302conta como falha. - Do corpo da sua resposta, só o primeiro 1 KB fica no histórico.
Gerenciar pela API
Todas estas rotas pedem uma chave secreta. As que alteram pedem acesso Leitura e escrita.
GET /webhooks/events
A lista de eventos que podem ser assinados.
curl https://vamo.ai/api/v1/webhooks/events \
-H "Authorization: Bearer sk_live_SUA_CHAVE"{
"data": [
"contact.created", "contact.updated", "contact.deleted",
"conversation.created", "conversation.updated", "conversation.closed",
"message.created", "message.sent", "message.delivered", "message.failed",
"campaign.created", "campaign.sent", "campaign.failed",
"template.approved", "template.rejected",
"order.created", "order.updated",
"store.connected", "store.disconnected"
]
}POST /webhooks
Cadastra um endpoint.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
url | string | Sim | A URL https:// |
events | string[] | Sim | Os eventos, ou ["*"] para todos |
curl -X POST https://vamo.ai/api/v1/webhooks \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{ "url": "https://erp.lojaaurora.com.br/webhooks/vamo", "events": ["order.created", "order.updated", "message.failed"] }'Resposta 201
{
"data": {
"id": "a3b4c5d6-e7f8-4a9b-8c0d-b4c5d6e7f8a9",
"url": "https://erp.lojaaurora.com.br/webhooks/vamo",
"secret": "whsec_4f1a9c2e7b3d8f6a0c5e9b2d7f4a1c8e3b6d9f2a5c8e1b4d",
"events": ["order.created", "order.updated", "message.failed"],
"status": "active",
"createdAt": "2026-10-31T17:00:00.000Z"
}
}O secret só aparece nesta resposta. Guarde-o agora; se perder, gere outro com rotate-secret.
GET /webhooks e GET /webhooks/:id
Os endpoints da organização (não paginado), ou um só. Sem o segredo.
{
"data": [
{
"id": "a3b4c5d6-e7f8-4a9b-8c0d-b4c5d6e7f8a9",
"url": "https://erp.lojaaurora.com.br/webhooks/vamo",
"events": ["order.created", "order.updated", "message.failed"],
"status": "active",
"failureCount": 0,
"lastDeliveryAt": "2026-10-31T16:00:01.000Z",
"createdAt": "2026-10-31T17:00:00.000Z"
}
]
}status é active, inactive (pausado por você) ou disabled (desativado pelo Vamo). failureCount conta as tentativas que falharam desde o último sucesso.
PATCH /webhooks/:id
Muda url, events ou status (active ou inactive). Mande só o que muda.
curl -X PATCH https://vamo.ai/api/v1/webhooks/a3b4c5d6-e7f8-4a9b-8c0d-b4c5d6e7f8a9 \
-H "Authorization: Bearer sk_live_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{ "events": ["*"] }'Responde 200 com o endpoint. Num endpoint disabled, mudar o status responde 409 com "code": "webhook_disabled_test_first"; url e events continuam editáveis.
DELETE /webhooks/:id
Exclui o endpoint. Responde { "data": { "success": true } }.
POST /webhooks/:id/rotate-secret
Gera um novo segredo e responde { "data": { "id": "…", "secret": "whsec_…" } }. O segredo antigo para de valer na hora, inclusive nas novas tentativas de entregas que estavam falhando. Atualize o seu servidor logo em seguida.
GET /webhooks/:id/deliveries
As 100 tentativas de entrega mais recentes, da mais nova para a mais antiga. Cada tentativa é uma linha.
{
"data": [
{
"id": "d6e7f8a9-b0c1-4d2e-9f3a-e7f8a9b0c1d2",
"deliveryId": "5b0f6c1e-8a39-4d0e-9a4e-2f1d7c3b9e10",
"endpointId": "a3b4c5d6-e7f8-4a9b-8c0d-b4c5d6e7f8a9",
"eventType": "order.updated",
"payload": { "order": { "id": "8192a3b4-c5d6-4e7f-9a8b-92a3b4c5d6e7", "status": "shipped" } },
"responseStatus": 200,
"responseBody": "ok",
"attempts": 1,
"createdAt": "2026-10-31T16:00:01.000Z"
}
]
}payload é o data do evento, attempts o número da tentativa e deliveryId o mesmo valor do cabeçalho X-Vamo-Delivery-Id. Sem resposta do seu servidor, responseStatus é null e responseBody traz o erro de rede.
Erros
Os erros vêm no formato de sempre: { "error": { "code": "…", "message": "…" } }.
| Status | code | Quando |
|---|---|---|
400 | validation_error | Falta url ou events; URL recusada (url: Only HTTPS URLs are allowed, url: Private IP addresses are not allowed…); evento que não existe (events: Invalid event types: …); status que não é active nem inactive; PATCH sem nenhum campo |
404 | not_found | Webhook endpoint not found |
409 | webhook_disabled_test_first | O Vamo desativou o endpoint: reative pelo Vamo |
POST /webhooks aceita Idempotency-Key: repetir com a mesma chave devolve o mesmo endpoint (e o mesmo secret), sem criar outro.