Documentação / Correlação entre eventos de webhook

Correlação entre eventos de webhook

Entrar

Correlação entre eventos de webhook

Este documento explica como um evento se relaciona a outro nos webhooks outbound do Pilot Status e qual atributo estabelece essa relação. O foco é o fluxo em que alguém responde a uma mensagem (reply).

Versão para cliente final (sem detalhes internos de implementação): webhooks-correlacao-eventos-guia-cliente.md.

Para a arquitetura geral de webhooks, ver também webhook-outbound.md e webhooks.md.

Identificadores no payload

| Atributo | Significado | Onde costuma aparecer | |----------|-------------|------------------------| | messageId | ID da mensagem no provedor WhatsApp (Evolution: key.id). Identifica aquela mensagem no evento atual. | message.sent, status (delivered / read / failed), message.received, message.reply, message.group | | internalMessageId | ID interno da entidade Message no Pilot (UUID no banco). | message.sent (worker e fluxos correlacionados), message.delivered, message.read, message.failed | | correlationId | ID de correlação de negócio gravado na mensagem outbound (quando existir). | message.sent, message.delivered, message.read, message.failed quando a linha Message tiver o campo; message.reply / message.received quando houver lastOutgoing correlacionado | | quotedMessageId | ID no provedor da mensagem à qual o usuário respondeu (reply / citação). Corresponde ao que o WhatsApp envia como contexto da resposta (ex.: stanzaId). | Somente message.reply | | messageRepliedId | ID interno Pilot da mensagem outbound que está sendo respondida. | Somente message.reply, quando há correlação completa com um envio via API |

Regra prática:

  • Para ligar reply → envio original pelo ID do WhatsApp: use quotedMessageId (reply) = messageId do message.sent da mensagem original.
  • Para ligar reply → envio original pelo ID do Pilot: use messageRepliedId (reply) = internalMessageId do message.sent (ou da mesma linha em logs/API).

quotedMessageId e messageRepliedId não são iguais: um é ID da Evolution, o outro é ID interno do Pilot.


Resposta do POST /v1/messages/send (API pública)

Implementação: apps/fullstack/src/app/api/v1/messages/send/route.ts (resposta 202).

Corpo típico de sucesso:

  • id — ID da linha Message no Pilot → corresponde ao internalMessageId nos webhooks outbound (message.sent, message.delivered, message.read, message.failed) e ao messageRepliedId no message.reply quando há correlação com envio via API.
  • correlationId — mesmo valor persistido na mensagem; pode aparecer nos eventos de status de saída e em message.reply / message.received quando o payload estiver correlacionado ao envio (mesmo valor do 202 quando existir).
  • status, createdAt, origin — metadados; não substituem os IDs de correlação acima.

Não vem no 202: o messageId da Evolution (evolutionKeyId / key.id no WhatsApp). Esse ID só existe após o envio ser aceito pelo provedor e é propagado nos webhooks (e no GET /v1/messages/<id> quando já persistido).

Fluxo recomendado para integradores: persistir id ao enfileirar → associar ao primeiro message.sent pelo par internalMessageId = id e guardar o messageId da Evolution para cruzar com quotedMessageId em replies.

Exemplo ponta a ponta (IDs ilustrativos)

Request POST /v1/messages/send:

{
  "templateId": "consulta-lembrete",
  "destinationNumber": "+5511999999999",
  "variables": { "nome": "Maria" }
}

Response 202:

{
  "id": "cmn0qk33b001kmj01q37f6quv",
  "correlationId": "cid_d815edf55caf4b80a368932cf22cdfa6",
  "status": "QUEUED",
  "createdAt": "2026-04-16T15:00:00.000Z",
  "origin": "Meu WhatsApp"
}

Webhook message.sent (worker ou messages.update; internalMessageId = Message.id = id do 202):

{
  "event": "message.sent",
  "data": {
    "messageId": "A2FHM8YGTQQQH992YRT4R",
    "internalMessageId": "cmn0qk33b001kmj01q37f6quv",
    "correlationId": "cid_d815edf55caf4b80a368932cf22cdfa6",
    "environment": "LIVE",
    "destinationNumber": "+5511999999999",
    "status": "SENT",
    "sentAt": "2026-04-16T15:00:03.000Z"
  }
}

Webhook message.delivered / message.read — mesmo messageId + internalMessageId que no message.sent, com deliveredAt / readAt conforme aplicável.

Webhook message.reply (correlação completa com envio via API key):

{
  "event": "message.reply",
  "data": {
    "messageId": "ACFC4C4DCBEFA43466256BBB207FC3B0",
    "quotedMessageId": "A2FHM8YGTQQQH992YRT4R",
    "messageRepliedId": "cmn0qk33b001kmj01q37f6quv",
    "replyContent": "SIM",
    "content": "",
    "environment": "LIVE",
    "receivedAt": "2026-04-16T15:41:07.445Z"
  }
}

| Campo | Ligação | |-------|---------| | data.internalMessageId (sent) = id (202) | Mesmo registro Message no Prisma. | | data.messageId (sent) = data.quotedMessageId (reply) | Evolution key.id da outbound respondida. | | data.messageRepliedId (reply) = id (202) = internalMessageId (sent) | ID interno Pilot da outbound. | | data.messageId (reply) | ID Evolution da mensagem inbound de resposta (novo). |


Cadeia do envio outbound (sem reply)

Ordem típica de eventos para uma mensagem enviada pela API:

  1. message.sent — mensagem aceita pelo provedor / status SENT.
    • Correlacionar entre si: messageId + internalMessageId repetem-se nos eventos seguintes da mesma mensagem.
  2. message.delivered (quando aplicável)
  3. message.read (quando aplicável; depende de recibos de leitura no WhatsApp)
  4. message.failed (se falhar depois)

Atributo que amarra toda a cadeia: o mesmo messageId (Evolution) e o mesmo internalMessageId (Pilot) em todos esses eventos, desde que a mensagem tenha sido encontrada no banco e o webhook da API key esteja ativo.

Se o Pilot não achar a mensagem no banco (messages.update), pode haver disparo “só tenant” com internalMessageId: null — aí só messageId + destino permitem cruzar informação.


Quando alguém responde a uma mensagem (message.reply)

O que o evento representa

message.reply é emitido quando a mensagem recebida é tratada como resposta a um envio anterior (há contexto de reply: stanzaId e/ou conteúdo citado, conforme regras do handler). Caso contrário pode ser emitido message.received.

Implementação principal: apps/fullstack/src/app/api/internal/webhook/handlers/messages-upsert.ts.

Como relacionar com o envio original

| Objetivo | Use | |----------|-----| | Saber qual mensagem no WhatsApp foi respondida | quotedMessageId — deve coincidir com o messageId do message.sent da mensagem original. | | Saber qual registro Pilot foi respondido | messageRepliedId — deve coincidir com internalMessageId no message.sent (e com o id da mensagem na API/logs). | | Saber qual é a mensagem de resposta (a nova, inbound) | No message.reply, o messageId do payload é o ID da Evolution da mensagem de resposta (a inbound), não da original. |

content vs replyContent no message.reply

  • replyContent — texto que o usuário enviou na resposta.
  • content — texto da mensagem original citada (lado outbound), preenchido a partir do quotedMessage no payload da Evolution e/ou do texto resolvido do envio no banco (quotedOrSentContent no handler). Pode ficar "" se o provedor não enviar texto citado extraível ou em certos tipos de mensagem; nesse caso replyContent ainda pode ter valor.

Documentação voltada ao cliente: webhooks-correlacao-eventos-guia-cliente.md.

Outros campos: receivedAt, environment, opcionalmente buttonId.

Quando messageRepliedId pode não vir

messageRepliedId só é preenchido quando o sistema resolve a mensagem outbound correlata (lastOutgoing) e o fluxo usa o webhook ligado à API key com correlação completa.

Se não houver essa correlação (por exemplo, reply sem stanzaId alinhado a um evolutionKeyId armazenado, ou envio feito fora do vínculo esperado com a key/webhook), o cliente pode receber message.reply sem messageRepliedId ou outro evento (ex.: message.received) — dependendo do ramo do handler.


Outros eventos (relação breve)

| Evento | Principal atributo de correlação | |--------|-----------------------------------| | message.received | messageId da mensagem recebida (Evolution). Não substitui o papel de message.reply para “resposta a um envio”. | | message.group | groupId (JID do grupo), messageId da mensagem no grupo. Para enviar ao grupo de volta, use o mesmo groupId na API. | | optin.created | projectId, destinationHash; não amarra a uma mensagem outbound por messageId. |


Ambiente TEST vs LIVE

Os nomes dos eventos e a forma do JSON são os mesmos. O campo data.environment indica TEST ou LIVE conforme o webhook (ou a mensagem correlacionada).

Webhooks TEST e LIVE são cadastros separados (URL e lista de eventos). Quem só configurou TEST não recebe entregas LIVE.


Referências de código

  • Envio público e resposta 202: apps/fullstack/src/app/api/v1/messages/send/route.ts
  • Lista de eventos: apps/fullstack/src/spa/pages/webhooks/webhook-events.ts
  • Reply e payloads correlacionados: apps/fullstack/src/app/api/internal/webhook/handlers/messages-upsert.ts
  • Status (sent / delivered / read / failed): apps/fullstack/src/app/api/internal/webhook/handlers/messages-update.ts
  • message.sent após envio pelo worker: apps/worker/src/send-message.ts