What's new, 21 October 2026: A customer who sends several messages before the bot's first reply no longer gets the opening sequence of a Chat Flow twice
WASync · Developers

Webhooks

Um endpoint assinado por conta. Verificação de assinatura, catálogo de eventos, política de novas tentativas e rotação do segredo.

Aponte o WASync para o seu endpoint e você recebe um segredo de assinatura de volta. Usa whatsapp.events. Não há registro de aplicativo nem etapa de consentimento.

Um único slot de webhook por conta

Uma conta tem exatamente uma URL de webhook. PUT /webhook a define — chamá-lo de novo com outra URL move o endpoint em vez de adicionar um segundo, e não existe exclusão. Isso importa assim que mais de uma coisa quer eventos: um gatilho do n8n e o seu próprio backend não podem ter o slot ao mesmo tempo. Faça a distribuição (fan-out) do seu lado.

Defina o endpoint

PUT /webhook
curl -X PUT https://developers.wasync.app/api/v1/webhook \
  -H "Authorization: Bearer $WASYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/wasync/webhook"}'
200 response
{
  "url": "https://example.com/wasync/webhook",
  "events": [
    "message.received",
    "message.status",
    "connection.disconnected",
    "connection.connected"
  ],
  "secret": "whsec_3f9a…"
}

Guarde o segredo. Ele é emitido aqui porque este é o momento em que você precisa dele para escrever o código de verificação, e GET /webhook não o retornará de novo — esse endpoint informa { url, events, secretSet }, deliberadamente sem o segredo, porque um caminho de leitura é registrado em logs e colado em tickets. Perdeu? Chame POST /webhook/rotate para obter um novo. A URL deve ser HTTPS em um host acessível publicamente; http, localhost e faixas privadas são rejeitados com 400 invalid_webhook.

A rotação não tem janela de sobreposição: o segredo antigo deixa de valer no instante em que o novo é emitido. Implante o novo segredo rapidamente — a política de novas tentativas abaixo cobre uma troca medida em segundos, não em horas.

Catálogo de eventos

EventFires whenDirection
message.receivedUma mensagem do WhatsApp chega em uma conexão autorizada (texto ou mídia).Somente recebidas — as mensagens que você envia não disparam este evento.
message.statusO estado de entrega de uma das suas mensagens enviadas muda.Somente enviadas.
connection.disconnectedUma conexão parou de funcionar e o proprietário precisa pareá-la novamente.Uma vez por queda, não repetidamente enquanto continuar com problema.
connection.connectedUma conexão que tinha caído voltou a funcionar.Uma vez, e somente para uma conexão sobre a qual você foi avisado que tinha caído.

Ignore valores de event desconhecidos em vez de tratá-los como erros — novos tipos são anunciados no changelog antes de entrarem em produção.

Os eventos disparam somente para conexões qr. As conexões cloud_api não emitem webhooks hoje; consulte GET /messages e GET /connections/{id} periodicamente para elas.

Payload

POST your webhook URL
{
  "event": "message.received",
  "connection_id": "conn_8f3a21",
  "message": {
    "id": "cmqj3k2ab0001xyz",
    "wa_id": "[email protected]_3EB0A1B2C3",
    "from": "40700000000",
    "text": "Hello!",
    "media_url": null,
    "media_type": null,
    "timestamp": 1766138640000
  }
}
  • message.id — o id estável do WASync (o mesmo valor que GET /messages retorna como id). Faça a deduplicação e os joins por ele — a entrega é at-least-once.
  • message.wa_id — o id próprio do WhatsApp, null quando desconhecido. Use apenas para suporte e depuração, nunca como chave de deduplicação ou de join.
  • from — somente dígitos internacionais, sem +.
  • text é null em mensagens somente de mídia; media_url / media_type são null em mensagens de texto ou quando o arquivo não pode ser obtido publicamente.
  • timestamp — epoch em milissegundos, como número, não como string ISO.

Um message.status atualiza uma das suas mensagens enviadas:

POST your webhook URL
{
  "event": "message.status",
  "connection_id": "conn_8f3a21",
  "message": {
    "id": "cmqj3k2ab0001xyz",
    "wa_id": "[email protected]_3EB0A1B2C3",
    "status": "read",
    "timestamp": 1766138641000
  }
}

Verifique a assinatura

Toda entrega traz X-WASync-Timestamp (epoch em ms) e X-WASync-Signature: sha256=<hex>, que é HMAC-SHA256(timestamp + "." + rawBody, secret). Calcule-a sobre o corpo bruto (raw) — antes de qualquer parsing de JSON — e compare em tempo constante. Rejeite entregas cujo timestamp tenha mais de ~5 minutos, o que impede replays.

verify.js
import crypto from "node:crypto";

// rawBody MUST be the exact bytes you received (verify BEFORE JSON.parse).
export function verifyWASync(headers, rawBody, secret) {
  const ts  = headers["x-wasync-timestamp"];
  const sig = headers["x-wasync-signature"];          // "sha256=<hex>"
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret)
          .update(`${ts}.${rawBody}`)
          .digest("hex");
  return Boolean(sig) &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
receiver (express)
import express from "express";
import { verifyWASync } from "./verify.js";

const app = express();

// Capture the RAW body — signature verification needs the exact bytes.
app.post("/webhooks/wasync", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyWASync(req.headers, req.body.toString("utf8"), process.env.WASYNC_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  // Reject stale deliveries (replay window) — 5 minutes is a sane tolerance.
  if (Math.abs(Date.now() - Number(req.headers["x-wasync-timestamp"])) > 5 * 60_000) {
    return res.status(401).end();
  }

  res.status(200).end(); // ACK fast — under 10s — then process async
  const { message } = JSON.parse(req.body.toString("utf8"));
  // ... your logic (dedupe on message.id — retries can deliver duplicates)
});

Entrega e política de novas tentativas

PropertyValue
Success criterionQualquer 2xx. Todo o resto, inclusive redirecionamentos, é falha.
Attempts3 por evento — imediata, depois após 500 ms, depois após 1 s.
Per-attempt timeout10 segundos. Confirme (ACK) rápido e processe de forma assíncrona.
After the last failureO evento é descartado. Não há fila longa de reentrega.
DuplicatesPossíveis. Faça a deduplicação por message.id.
OrderingNão garantida. Ordene por timestamp se for importante.
Signature on retriesO mesmo corpo e a mesma assinatura são reenviados — verifique cada tentativa da mesma forma.

Escada de status

Os valores de message.status formam uma escada de mão única: sent (1) → delivered (2) → read (3), com failed como resultado terminal que não vem depois de read.

Aplique as atualizações de forma monotônica. Só aplique um novo status se o seu nível for maior que o do status armazenado. Uma mensagem pode chegar com read antes de delivered (uma entrega tardia), e eventos duplicados são possíveis. Nunca faça uma mensagem retroceder na sua interface.

Reconciliação — consulte periodicamente como rede de segurança

As entregas são tentadas três vezes e depois descartadas, o que faz do seu handler de webhook o caminho rápido, não a garantia. Execute um loop em segundo plano que consulta GET /messages?connectionId=… a cada 30–60 segundos e faz upsert de tudo que o handler não viu, usando o cursor para buscas incrementais. O mesmo loop recupera as atualizações de status que chegaram enquanto o seu endpoint estava fora do ar.

Webhooks = atualizações da interface em tempo real. Consulta periódica = correção. Use os dois.

Ciclo de vida da conexão

POST your webhook URL
{
  "event": "connection.disconnected",
  "connection_id": "conn_8f2a…",
  "connection": {
    "id": "conn_8f2a…",
    "phone_number": "393331234567",
    "label": "Studio Rossi",
    "status": "disconnected",
    "needs_reconnect": true,
    "license_status": "active",
    "license_expires": "2027-08-08T09:00:00.000Z",
    "reason": "AUTH_LOST"
  },
  "timestamp": 1786000000000
}

Uma conexão qr pode parar de funcionar por motivos fora do controle de qualquer pessoa — o telefone fica desligado por um dia, o WhatsApp é aberto em outro dispositivo, a sessão expira. O número então fica mudo nos dois sentidos e, sem um evento, a primeira pessoa a perceber é o seu cliente.

O QR code deliberadamente não vai no payload. Um QR do WhatsApp é uma credencial de login por leitura: quem o lê pode assumir a sessão. O evento avisa que é preciso parear de novo — busque o QR em si pelo seu GET /connections/{id} autenticado e mostre-o ao proprietário do número, exatamente como na configuração inicial. Conduza a sua interface por needs_reconnect; trate reason como uma dica informativa para logs, pois novos valores podem aparecer.

Latência de detecção: uma desconexão é encontrada por uma sondagem de status a cada 5 minutos (~5 min). Quando a sessão parece recuperável, uma recuperação automática é tentada primeiro, então esse caminho leva ~15–20 minutos.

Não está recebendo eventos?

  • A URL de entrega é HTTPS em um host público? Localhost, IPs privados e http:// são rejeitados. Use um túnel durante o desenvolvimento e confira o que está de fato armazenado com GET /webhook.
  • A credencial tem whatsapp.events e cobre a conexão em que a mensagem chegou?
  • A licença da conexão ainda está ativa? Uma licença expirada significa nenhum evento e 402 nos envios.
  • O seu endpoint responde 2xx em até 10 segundos? Handlers lentos parecem falhas e consomem as três tentativas.
  • É uma conexão qr? As conexões cloud_api não emitem webhooks.
  • Somente mensagens recebidas disparam message.received — os seus próprios envios nunca disparam.
Esta página foi útil?

On this page