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) =messageIddomessage.sentda mensagem original. - Para ligar reply → envio original pelo ID do Pilot: use
messageRepliedId(reply) =internalMessageIddomessage.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 linhaMessageno Pilot → corresponde aointernalMessageIdnos webhooks outbound (message.sent,message.delivered,message.read,message.failed) e aomessageRepliedIdnomessage.replyquando há correlação com envio via API.correlationId— mesmo valor persistido na mensagem; pode aparecer nos eventos de status de saída e emmessage.reply/message.receivedquando o payload estiver correlacionado ao envio (mesmo valor do202quando 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:
message.sent— mensagem aceita pelo provedor / status SENT.- Correlacionar entre si:
messageId+internalMessageIdrepetem-se nos eventos seguintes da mesma mensagem.
- Correlacionar entre si:
message.delivered(quando aplicável)message.read(quando aplicável; depende de recibos de leitura no WhatsApp)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 doquotedMessageno payload da Evolution e/ou do texto resolvido do envio no banco (quotedOrSentContentno handler). Pode ficar""se o provedor não enviar texto citado extraível ou em certos tipos de mensagem; nesse casoreplyContentainda 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.sentapós envio pelo worker:apps/worker/src/send-message.ts