externalAdReply (Click-to-WhatsApp ads) no longer reaches the Chatwoot integration on 2.4

Author: jpsaldanha4Created Sep 5, 2026Updated Sep 5, 2026

Summary

Since 2.4, messages coming from Instagram/Facebook click-to-WhatsApp ads arrive in Chatwoot as plain text. The ad card — thumbnail, title, body, source URL — is silently dropped. On 2.3.7 the same messages produced an attachment with the resized thumbnail plus the ad title and link.

There is no error in the logs. The message itself is delivered; only the ad context disappears.

Root cause

prepareMessage() in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts changed which field feeds messageRaw.contextInfo.

main (and 2.3.7):

typescript
const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

develop (and 2.4.0-rc2):

typescript
contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo),

messageContextInfo is the WhatsApp device-metadata block (deviceListMetadata, E2E key info). It never carries externalAdReply. The ad context lives in the content message's own contextInfo, e.g. message.message.extendedTextMessage.contextInfo.externalAdReply.

Why the Chatwoot side then fails

getAdsMessage() in src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts is called as this.getAdsMessage(body), where body is the message wrapper:

typescript
title: msg.extendedTextMessage?.contextInfo?.externalAdReply?.title
    || msg.contextInfo?.externalAdReply?.title,

The first branch never matches, because on the wrapper the content lives under msg.message.extendedTextMessage, not msg.extendedTextMessage. So the whole feature depended on the hoisted msg.contextInfo — precisely what the change above repointed. getAdsMessage() now returns an object whose fields are all undefined, the if (adsMessage && adsMessage.title || ...) branch is skipped, and no attachment is created.

Side effect beyond ads

messageRaw?.contextInfo?.quotedMessage (same file) reads the same field, so quote/reply context on media messages is lost as well.

Steps to reproduce

  1. Run 2.4.0-rc2 with the Chatwoot integration enabled.
  2. Publish an Instagram ad with a click-to-WhatsApp button.
  3. Click the ad and send any text.
  4. Chatwoot receives the text only. On 2.3.7 the same flow produced an attachment with the 320x180 thumbnail, the ad title and the source URL.

Suggested fix

Restore the previous source in prepareMessage():

typescript
const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

Independently, getAdsMessage() would be more robust reading msg.message?.extendedTextMessage?.contextInfo?.externalAdReply as its first branch, so it no longer depends on the hoisted field at all.

Environment

  • Evolution API 2.4.0-rc2 (Docker, evoapicloud/evolution-api:2.4.0-rc2)
  • Chatwoot 4.17.1, self-hosted
  • Baileys channel, four instances
  • Regression confirmed by source diff between tags 2.3.7 and 2.4.0-rc2, and between branches main and develop.

Workaround in use

While this is open, we patch the compiled bundles (dist/api/integrations/channel/whatsapp/whatsapp.baileys.service.{js,mjs}) in a derived image, replacing the expression with one that does not depend on minified symbol names:

javascript
contextInfo: this.deserializeMessageBuffers(
  (e.message && Object.values(e.message).find(v => v && typeof v === "object" && v.contextInfo)?.contextInfo)
  ?? e.message?.messageContextInfo
)

Checked against simulated payloads: ad leads recover title / body / thumbnailUrl / sourceUrl, plain text messages are unaffected, and quoted media regains its stanzaId. It is running in our production instance; we have not yet observed a live ad lead through it.


Versão em português (pt-BR) — clique para expandir

Resumo

Desde a 2.4, mensagens vindas de anúncios click-to-WhatsApp do Instagram e do Facebook chegam ao Chatwoot como texto puro. O cartão do anúncio — miniatura, título, corpo e URL de origem — é descartado silenciosamente. Na 2.3.7 as mesmas mensagens produziam um anexo com a miniatura redimensionada, mais o título e o link do anúncio.

Não há erro no log. A mensagem em si é entregue; só o contexto do anúncio some.

Causa

O método prepareMessage(), em src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts, passou a preencher messageRaw.contextInfo a partir de outro campo.

main (e 2.3.7):

typescript
const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

develop (e 2.4.0-rc2):

typescript
contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo),

messageContextInfo é o bloco de metadados de dispositivo do WhatsApp (deviceListMetadata, informação de chaves E2E). Ele nunca carrega externalAdReply. O contexto do anúncio vive na contextInfo da própria mensagem de conteúdo, por exemplo message.message.extendedTextMessage.contextInfo.externalAdReply.

Por que o lado do Chatwoot falha em seguida

getAdsMessage(), em src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts, é chamado como this.getAdsMessage(body), onde body é o invólucro da mensagem:

typescript
title: msg.extendedTextMessage?.contextInfo?.externalAdReply?.title
    || msg.contextInfo?.externalAdReply?.title,

O primeiro ramo nunca casa, porque no invólucro o conteúdo fica em msg.message.extendedTextMessage, e não em msg.extendedTextMessage. Ou seja, o recurso inteiro dependia do msg.contextInfo hoisted — exatamente o que a mudança acima repontou. getAdsMessage() agora devolve um objeto com todos os campos undefined, o ramo if (adsMessage && adsMessage.title || ...) é pulado, e nenhum anexo é criado.

Efeito colateral além dos anúncios

messageRaw?.contextInfo?.quotedMessage (mesmo arquivo) lê o mesmo campo, então o contexto de citação/resposta em mensagens de mídia também se perde.

Como reproduzir

  1. Rodar a 2.4.0-rc2 com a integração do Chatwoot habilitada.
  2. Publicar um anúncio no Instagram com botão click-to-WhatsApp.
  3. Clicar no anúncio e enviar qualquer texto.
  4. O Chatwoot recebe apenas o texto. Na 2.3.7 o mesmo fluxo produzia um anexo com a miniatura 320x180, o título do anúncio e a URL de origem.

Correção sugerida

Restaurar a origem anterior em prepareMessage():

typescript
const contentType = getContentType(message.message);
const contentMsg = message?.message[contentType] as any;
// ...
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),

Independentemente disso, getAdsMessage() ficaria mais robusto lendo msg.message?.extendedTextMessage?.contextInfo?.externalAdReply como primeiro ramo, deixando de depender do campo hoisted.

Ambiente

  • Evolution API 2.4.0-rc2 (Docker, evoapicloud/evolution-api:2.4.0-rc2)
  • Chatwoot 4.17.1, self-hosted
  • Canal Baileys, quatro instâncias
  • Regressão confirmada por diff de fonte entre as tags 2.3.7 e 2.4.0-rc2, e entre os branches main e develop.

Contorno em uso

Enquanto isso não é corrigido, aplicamos um patch nos bundles compilados (dist/api/integrations/channel/whatsapp/whatsapp.baileys.service.{js,mjs}) numa imagem derivada, trocando a expressão por uma que não depende de nomes de símbolos minificados:

javascript
contextInfo: this.deserializeMessageBuffers(
  (e.message && Object.values(e.message).find(v => v && typeof v === "object" && v.contextInfo)?.contextInfo)
  ?? e.message?.messageContextInfo
)

Verificado com payloads simulados: leads de anúncio recuperam title / body / thumbnailUrl / sourceUrl, mensagens de texto comum ficam inalteradas, e mídia citando outra mensagem recupera o stanzaId. Está rodando na nossa instância de produção; ainda não observamos um lead de anúncio real passando por ele.

Source: evolution-foundation/evolution-api