vamobotDocs

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

  1. Você cadastra uma URL https:// e escolhe os eventos.
  2. Quando um evento acontece, o Vamo envia um POST com um JSON assinado.
  3. O seu servidor responde com um status 2xx em até 10 segundos.
  4. 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.

Página Webhooks com a lista de endpoints, os eventos inscritos, o status e a taxa de sucesso

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.

EventoDispara quandodata
contact.createdUm 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.updatedPATCH /contacts/:id{ contact }
contact.deletedDELETE /contacts/:id{ contactId }
conversation.createdUm envio pela API abre a primeira conversa do contato{ conversation }
conversation.updatedPATCH /conversations/:id que não fecha a conversa{ conversation }
conversation.closedPATCH /conversations/:id com "status": "closed"{ conversation }
message.createdUma 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.deliveredO WhatsApp confirma a entrega de qualquer mensagem da loja (IA, equipe, automação, campanha ou API){ messageId, conversationId, waMessageId, status, … }
message.failedO WhatsApp avisa que uma mensagem da loja não foi entregueIgual, com errorCode e errorMessage
order.createdUm pedido é criado por POST /orders{ order } com items
order.updatedUm 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.approvedA Meta aprova um modelo{ id, name, language, waTemplateId, status, previousStatus }
template.rejectedA Meta rejeita um modeloIgual, com reason
store.connectedO WhatsApp da loja é conectado ou volta a funcionar{ storeId, phoneNumber, connectionMethod, provider }
store.disconnectedO 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çalhoConteúdo
Content-Typeapplication/json
X-Webhook-EventO evento, por exemplo order.updated
X-Webhook-TimestampHora da assinatura, em segundos Unix
X-Webhook-Signature-V2sha256= + HMAC-SHA256 em hexadecimal de timestamp + "." + corpo
X-Webhook-Signaturesha256= + HMAC-SHA256 em hexadecimal só do corpo (para integrações antigas)
X-Webhook-Attempt1 na primeira tentativa, depois 2, 3…
X-Vamo-Delivery-IdID 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 200 antes de processar.
  • Ordem. Cada entrega é independente: um message.delivered pode chegar antes do message.created da 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:

  • status vira disabled e 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 ping assinado ({ "event": "ping", "deliveryId": "…", "data": {}, "timestamp": "…" }) e, se o seu servidor responder 2xx, o endpoint volta a active. 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 como nip.io e URLs com usuário e senha são recusados, ao cadastrar e a cada entrega.
  • Redirecionamentos não são seguidos: um 301 ou 302 conta 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.

CampoTipoObrigatórioDescrição
urlstringSimA URL https://
eventsstring[]SimOs 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": "…" } }.

StatuscodeQuando
400validation_errorFalta 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
404not_foundWebhook endpoint not found
409webhook_disabled_test_firstO 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.

Nesta página