Skip to content

feat: add PIX payment and fix interactive messages (buttons, list, carousel)#40

Open
impa365 wants to merge 1 commit intoEvolutionAPI:mainfrom
impa365:feat/whatsapp-interactive-messages
Open

feat: add PIX payment and fix interactive messages (buttons, list, carousel)#40
impa365 wants to merge 1 commit intoEvolutionAPI:mainfrom
impa365:feat/whatsapp-interactive-messages

Conversation

@impa365
Copy link
Copy Markdown

@impa365 impa365 commented Apr 20, 2026

PIX Payment Support + Button/List/Carousel Protocol Fixes

Description

🇺🇸 English | 🇧🇷 Português abaixo

This PR implements PIX payment message support and fixes the WhatsApp binary protocol for buttons, lists, and carousels to work correctly on both Android and iOS clients.


🆕 New: PIX Payment (SendPix)

  • New PixStruct with all required fields: number, headerTitle, bodyText, merchantName, pixKey, keyType (CPF, CNPJ, EMAIL, PHONE, EVP), optional amount, description, delay, quoted
  • Builds a NativeFlowMessage (review_and_pay) with pixPaymentPayload serialized as JSON inside ButtonParamsJSON
  • Adds required WhatsApp biz/bot AdditionalNodes stanza nodes (biz + bot XML nodes) for business payment flow
  • Sets DeviceListMetadataVersion: 2 for correct end-to-end routing
  • New POST /send/pix route with full input validation in the handler

🔧 Fix: Button messages (SendButton)

  • Wraps ButtonsMessage and InteractiveMessage inside DocumentWithCaptionMessage — required by current WhatsApp protocol to display buttons in chat
  • Generates 32-byte MessageSecret via crypto/rand — required for iOS to render interactive messages
  • Adds biz AdditionalNodes nodes (same pattern as official WhatsApp Business API)
  • Supports all button types: REPLY (quick reply), URL (cta_url), CALL (cta_call), COPY (cta_copy), and REVIEW_AND_PAY
  • Optional ImageUrl / VideoUrl header support

🔧 Fix: List messages (SendList)

  • Wraps ListMessage inside DocumentWithCaptionMessage — required by current WhatsApp protocol
  • Generates MessageSecret (32 bytes)
  • Adds product_list biz AdditionalNodes with v=2 attribute required for list rendering

🔧 Fix: Carousel messages (SendCarousel)

  • Generates MessageSecret (32 bytes) — was missing, causing silent failures on iOS
  • Sets CarouselCardType: HSCROLL_CARDS — required for horizontal scroll carousel layout
  • Adds MessageParamsJSON and MessageVersion on each card's NativeFlowMessage
  • Replaces unsafe fmt.Sprintf JSON construction with json.Marshal for all button params
  • Removes forced empty ContextInfo on direct InteractiveMessage that was breaking iOS carousel rendering
  • Passes Quoted through SendDataStruct correctly
  • Simplifies image upload — removes custom thumbnail generation

🔧 Fix: SendMessage central dispatcher

  • Adds AdditionalNodes passthrough from SendDataStruct to whatsmeow.SendRequestExtra
  • Adds ButtonsMessage case handling for messages wrapped in DocumentWithCaptionMessage
  • Adds ListMessage case handling for messages wrapped in DocumentWithCaptionMessage
  • Fixes InteractiveMessage case: applies ContextInfo only on wrapped messages (buttons/lists); skips empty ContextInfo on direct InteractiveMessage (carousel) to avoid iOS rendering breakage

🧹 Chore

  • Removes unused image/jpeg import (leftover from thumbnail generation code)
  • Adds AdditionalNodes *[]waBinary.Node field to SendRequestExtra in the whatsmeow-lib fork for biz stanza injection

Type of Change

  • Bug fix — fixes button/list/carousel rendering on iOS and Android
  • New feature — adds full PIX payment support via WhatsApp

Testing

  • Manual testing completed with a connected WhatsApp instance
  • Docker build validated (docker compose up -d --build)
  • No breaking changes to existing routes


Português

Este PR implementa suporte a mensagens de pagamento PIX e corrige o protocolo binário do WhatsApp para botões, listas e carrosséis, garantindo funcionamento correto tanto no Android quanto no iOS.


🆕 Novo: Pagamento PIX (SendPix)

  • Nova struct PixStruct com todos os campos necessários: number, headerTitle, bodyText, merchantName, pixKey, keyType (CPF, CNPJ, EMAIL, PHONE, EVP), e opcionais amount, description, delay, quoted
  • Constrói um NativeFlowMessage (review_and_pay) com pixPaymentPayload serializado como JSON dentro de ButtonParamsJSON
  • Adiciona os nós AdditionalNodes biz/bot obrigatórios (nós XML biz + bot) para o fluxo de pagamento empresarial
  • Define DeviceListMetadataVersion: 2 para roteamento correto ponta-a-ponta
  • Nova rota POST /send/pix com validação completa dos campos no handler

🔧 Correção: Mensagens com botões (SendButton)

  • Encapsula ButtonsMessage e InteractiveMessage dentro de DocumentWithCaptionMessage — obrigatório pelo protocolo atual do WhatsApp para exibir botões no chat
  • Gera MessageSecret de 32 bytes via crypto/rand — necessário para o iOS renderizar mensagens interativas
  • Adiciona nós AdditionalNodes biz (mesmo padrão da API oficial do WhatsApp Business)
  • Suporta todos os tipos de botão: REPLY (resposta rápida), URL (cta_url), CALL (cta_call), COPY (cta_copy) e REVIEW_AND_PAY
  • Suporte opcional a cabeçalho com ImageUrl / VideoUrl

🔧 Correção: Mensagens de lista (SendList)

  • Encapsula ListMessage dentro de DocumentWithCaptionMessage — obrigatório pelo protocolo atual
  • Gera MessageSecret (32 bytes)
  • Adiciona nós AdditionalNodes biz product_list com atributo v=2 necessário para renderização da lista

🔧 Correção: Mensagens de carrossel (SendCarousel)

  • Gera MessageSecret (32 bytes) — estava faltando, causando falhas silenciosas no iOS
  • Define CarouselCardType: HSCROLL_CARDS — obrigatório para o layout de carrossel com rolagem horizontal
  • Adiciona MessageParamsJSON e MessageVersion no NativeFlowMessage de cada card
  • Substitui construção de JSON insegura com fmt.Sprintf por json.Marshal para todos os parâmetros de botões
  • Remove ContextInfo vazio forçado no InteractiveMessage direto que quebrava a renderização do carrossel no iOS
  • Passa Quoted corretamente via SendDataStruct
  • Simplifica upload de imagem — remove geração de thumbnail customizado

🔧 Correção: Dispatcher central SendMessage

  • Adiciona passagem de AdditionalNodes de SendDataStruct para whatsmeow.SendRequestExtra
  • Adiciona tratamento do case ButtonsMessage para mensagens encapsuladas em DocumentWithCaptionMessage
  • Adiciona tratamento do case ListMessage para mensagens encapsuladas em DocumentWithCaptionMessage
  • Corrige case InteractiveMessage: aplica ContextInfo apenas em mensagens encapsuladas; omite ContextInfo vazio em InteractiveMessage direto (carrossel) para evitar quebra no iOS

🧹 Limpeza

  • Remove import image/jpeg não utilizado (sobra do código de geração de thumbnail)
  • Adiciona campo AdditionalNodes *[]waBinary.Node em SendRequestExtra no fork do whatsmeow-lib para injeção de nós biz

Tipo de Mudança

  • Bug fix — corrige renderização de botões, listas e carrossel no iOS/Android
  • New feature — adiciona suporte completo a pagamento PIX via WhatsApp

Testes

  • Testado manualmente em ambiente com WhatsApp conectado
  • Build Docker validado (docker compose up -d --build)
  • Sem breaking changes nas rotas existentes

…rousel)

- Add SendPix function with full WhatsApp payment protocol support
  (AdditionalNodes biz/bot nodes, DeviceListMetadataVersion, NativeFlowMessage)
- Add /send/pix route and SendPix handler with input validation
- Rewrite SendButton: DocumentWithCaptionMessage wrapper + MessageSecret +
  AdditionalNodes biz nodes; supports reply, URL, call, copy and review_and_pay
- Rewrite SendList: DocumentWithCaptionMessage wrapper + MessageSecret +
  AdditionalNodes (product_list v=2 biz nodes)
- Fix SendCarousel: add MessageSecret (32 bytes), set HSCROLL_CARDS card type,
  add MessageParamsJSON+MessageVersion on card buttons, use json.Marshal instead
  of fmt.Sprintf, remove forced empty ContextInfo that broke iOS rendering,
  pass Quoted message, simplify image upload (remove thumbnail generation)
- Fix SendMessage central: add AdditionalNodes support in sendExtra, add
  ButtonsMessage and ListMessage wrapped cases in DocumentWithCaptionMessage,
  fix InteractiveMessage handling (skip empty ContextInfo on direct carousel)
- Remove unused image/jpeg import
Copilot AI review requested due to automatic review settings April 20, 2026 22:54
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @impa365, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds PIX payment message support and updates WhatsApp interactive message construction (buttons/lists/carousels) to match newer protocol expectations across Android/iOS, including biz stanza injection and MessageSecret handling.

Changes:

  • Introduces SendPix message builder and POST /send/pix endpoint.
  • Reworks button/list sending to wrap messages in DocumentWithCaptionMessage, generates MessageSecret, and injects biz AdditionalNodes.
  • Updates carousel card native-flow params and refactors button param JSON building to use json.Marshal.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
pkg/sendMessage/service/send_service.go Adds PIX support and reworks interactive message building/routing (buttons/lists/carousels), plus AdditionalNodes passthrough.
pkg/sendMessage/handler/send_handler.go Adds /send/pix handler with request validation.
pkg/routes/routes.go Registers POST /send/pix route.
.gitignore Adds an ignore entry for a local/reference folder.
.dockerignore Ignores reference/analysis artifacts in Docker builds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1951 to +1954
resp, err := http.Get(data.ImageUrl)
if err == nil {
fileData, err := io.ReadAll(resp.Body)
resp.Body.Close()
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http.Get is performed on data.ImageUrl / data.VideoUrl (user-provided) without any allowlist/denylist, scheme checks, size limits, or timeouts. This opens SSRF and resource exhaustion risk (e.g., fetching internal URLs or very large responses). Use an http.Client with timeouts, validate scheme/host, and enforce a maximum download size (e.g., io.LimitReader).

Copilot uses AI. Check for mistakes.
Comment on lines +2243 to 2248
for _, row := range section.Rows {
rows = append(rows, &waE2E.ListMessage_Row{
Title: proto.String(rowTitle),
Description: proto.String(r.Description),
RowID: proto.String(rowId),
Title: proto.String(row.Title),
Description: proto.String(row.Description),
RowID: proto.String(row.RowId),
})
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List rows are now sent with RowID: proto.String(row.RowId) and Title: proto.String(row.Title) without generating fallbacks when these are empty. Previously this code ensured non-empty titles/IDs; sending empty RowIDs can cause WhatsApp to reject the list or render incorrectly. Add validation (reject empty titles/rowIds) or generate deterministic defaults when missing.

Copilot uses AI. Check for mistakes.
Sections: sections,
// Generate MessageSecret (32 random bytes)
listMsgSecret := make([]byte, 32)
crypto_rand.Read(listMsgSecret)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto/rand.Read(listMsgSecret) ignores the returned count/error. If it fails or returns fewer than 32 bytes, MessageSecret may be invalid and interactive list rendering can break. Check n, err and handle failures explicitly.

Suggested change
crypto_rand.Read(listMsgSecret)
if _, err := io.ReadFull(crypto_rand.Reader, listMsgSecret); err != nil {
return nil, fmt.Errorf("failed to generate list message secret: %w", err)
}

Copilot uses AI. Check for mistakes.
interactiveMsg.ContextInfo = contextInfo
// Generate MessageSecret (32 random bytes) required for iOS to render
msgSecret := make([]byte, 32)
crypto_rand.Read(msgSecret)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto/rand.Read(msgSecret) ignores the returned count/error. If the RNG read fails or is partial, MessageSecret may be invalid and carousel rendering can break. Check n, err and handle failures explicitly.

Suggested change
crypto_rand.Read(msgSecret)
n, err := crypto_rand.Read(msgSecret)
if err != nil {
s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error generating carousel message secret: %v", instance.Id, err)
return nil, err
}
if n != len(msgSecret) {
err = fmt.Errorf("incomplete random read for carousel message secret: got %d bytes, want %d", n, len(msgSecret))
s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error generating carousel message secret: %v", instance.Id, err)
return nil, err
}

Copilot uses AI. Check for mistakes.
Comment on lines +2078 to +2094
client, _ := s.ensureClientConnected(instance.Id)
uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage)
if err == nil {
interactive.Header = &waE2E.InteractiveMessage_Header{
Title: proto.String(""),
HasMediaAttachment: proto.Bool(true),
Media: &waE2E.InteractiveMessage_Header_ImageMessage{
ImageMessage: &waE2E.ImageMessage{
URL: proto.String(uploaded.URL),
DirectPath: proto.String(uploaded.DirectPath),
MediaKey: uploaded.MediaKey,
Mimetype: proto.String("image/jpeg"),
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(fileData))),
},
},
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too client, _ := s.ensureClientConnected(...) ignores the error before calling client.Upload(...). If the client is disconnected, this can panic. Reuse the validated client from the beginning of SendButton or return an error if reconnection fails.

Suggested change
client, _ := s.ensureClientConnected(instance.Id)
uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage)
if err == nil {
interactive.Header = &waE2E.InteractiveMessage_Header{
Title: proto.String(""),
HasMediaAttachment: proto.Bool(true),
Media: &waE2E.InteractiveMessage_Header_ImageMessage{
ImageMessage: &waE2E.ImageMessage{
URL: proto.String(uploaded.URL),
DirectPath: proto.String(uploaded.DirectPath),
MediaKey: uploaded.MediaKey,
Mimetype: proto.String("image/jpeg"),
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(fileData))),
},
},
client, err := s.ensureClientConnected(instance.Id)
if err == nil {
uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage)
if err == nil {
interactive.Header = &waE2E.InteractiveMessage_Header{
Title: proto.String(""),
HasMediaAttachment: proto.Bool(true),
Media: &waE2E.InteractiveMessage_Header_ImageMessage{
ImageMessage: &waE2E.ImageMessage{
URL: proto.String(uploaded.URL),
DirectPath: proto.String(uploaded.DirectPath),
MediaKey: uploaded.MediaKey,
Mimetype: proto.String("image/jpeg"),
FileEncSHA256: uploaded.FileEncSHA256,
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(fileData))),
},
},
}

Copilot uses AI. Check for mistakes.
Comment on lines +1673 to +1677
if data.KeyType == "" {
return nil, errors.New("keyType is required (CPF, CNPJ, EMAIL, PHONE, EVP)")
}
data.KeyType = strings.ToUpper(data.KeyType)

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendPix normalizes KeyType with strings.ToUpper, but the codebase already has mapKeyType() that maps values like random -> EVP. As-is, random becomes RANDOM and will be sent to WhatsApp unchanged. Consider validating against the supported set and mapping via mapKeyType (or equivalent) before building the payload.

Copilot uses AI. Check for mistakes.
Comment on lines +1678 to +1689
// Build internal payment payload
defaultAmount := pixAmount{Value: 100, Offset: 100}
payload := pixPaymentPayload{
Currency: "BRL",
ReferenceID: utils.GenerateRandomString(11),
Type: "physical-goods",
TotalAmount: defaultAmount,
Order: pixOrder{
Status: "pending",
OrderType: "ORDER",
Subtotal: defaultAmount,
Items: []pixItem{
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendPix currently hardcodes defaultAmount := pixAmount{Value: 100, Offset: 100} and always sends that as the total/subtotal. This makes the PIX API effectively fixed-value and doesn’t match the PR description/API expectation of an optional amount/description; it will also be surprising to clients that omit an amount. Add fields (e.g., amount, description) to PixStruct and compute TotalAmount/Subtotal/Items from request data (or omit amounts when not provided if the protocol supports it).

Copilot uses AI. Check for mistakes.
Comment on lines +1956 to +1958
client, _ := s.ensureClientConnected(instance.Id)
uploaded, err := client.Upload(context.Background(), fileData, whatsmeow.MediaImage)
if err == nil {
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the media-header upload path you call client, _ := s.ensureClientConnected(...) and ignore the error. If the client isn’t connected, client can be nil and client.Upload(...) will panic. Reuse the already-validated client from the start of SendButton, or handle the error and return it before uploading.

Copilot uses AI. Check for mistakes.
Comment on lines +1741 to +1747
msg := &waE2E.Message{
InteractiveMessage: interactive,
MessageContextInfo: &waE2E.MessageContextInfo{
DeviceListMetadataVersion: proto.Int32(2),
DeviceListMetadata: &waE2E.DeviceListMetadata{},
},
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendPix doesn’t set MessageContextInfo.MessageSecret while other interactive-message senders (buttons/lists/carousel) now do. If iOS rendering requires MessageSecret for interactive messages, PIX messages may still fail to render. Consider generating a 32-byte secret and populating MessageContextInfo.MessageSecret here as well (and handle crypto/rand errors).

Copilot uses AI. Check for mistakes.

// Generate MessageSecret (32 random bytes) required for iOS to render interactive messages
btnMsgSecret := make([]byte, 32)
crypto_rand.Read(btnMsgSecret)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crypto/rand.Read return values are ignored when generating MessageSecret. If the read fails or is partial, you may send an all-zero/short secret which can break rendering. Capture and check the returned n, err and fail the request (or retry) if err != nil || n != 32.

Suggested change
crypto_rand.Read(btnMsgSecret)
n, err := crypto_rand.Read(btnMsgSecret)
if err != nil {
return nil, fmt.Errorf("failed to generate message secret: %w", err)
}
if n != len(btnMsgSecret) {
return nil, fmt.Errorf("failed to generate message secret: read %d bytes, expected %d", n, len(btnMsgSecret))
}

Copilot uses AI. Check for mistakes.
@rafacpti23
Copy link
Copy Markdown

Top..foi reativo isso.. bora subir

Copy link
Copy Markdown

@paluan-batista paluan-batista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bom dia amigo tudo bem?

Na intenção de ajudá-lo com um code review nesse projeto, deixo aqui algumas sugestões de melhorias que encontrei. De modo geral o projeto esta ficando muito bom.

sendExtra.AdditionalNodes = data.AdditionalNodes
}

response, err := s.clientPointer[instance.Id].SendMessage(context.Background(), recipient, msg, sendExtra)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bom dia tudo bem?

Usar context.Background() impede o cancelamento da operação se o cliente HTTP (o usuário da API) fechar a conexão ou se houver um timeout no nível do handler.

Sugestão: Alterar a assinatura do SendPix e SendMessage para aceitar ctx context.Context e propagar o contexto vindo do Gin.


type PaymentItem struct {
Name string `json:"name"`
Amount int `json:"amount"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifiquei uma inconsistência entre as structs de pagamento:

PaymentItem.Amount é int.
pixAmount.Value é int64.
Sugestão: Padronizar todos os valores monetários e quantidades para int64 para evitar overflows silenciosos em sistemas de 32 bits e manter a compatibilidade com os tipos do proto.

return
}

if data.MerchantName == "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As validações de MerchantName, PixKey e KeyType estão presentes tanto no send_handler.go quanto no send_service.go.

Sugestão: Mover a lógica de validação para um método de domínio ou usar tags de validação do Gin (binding:"required") na struct PixStruct. Isso simplifica o handler e garante que o service seja testável isoladamente.

data.KeyType = strings.ToUpper(data.KeyType)

// Build internal payment payload
defaultAmount := pixAmount{Value: 100, Offset: 100}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atualmente, defaultAmount está fixado em Value: 100, Offset: 100 no SendPix.

Sugestão: Estes valores deveriam ser parametrizados no PixStruct para permitir cobranças de valores reais, ou documentados como um "placeholder" para pagamentos de valor aberto (se for o caso do protocolo).

AdditionalNodes: &pixNodes,
})
if err != nil {
s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending pix message: %v", instance.Id, err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O log em SendPix utiliza LogError mas retorna o erro original:

Sugestão: Considere usar fmt.Errorf("failed to send pix: %w", err) para fornecer contexto sem perder o erro original (error wrapping).

Copy link
Copy Markdown

@paluan-batista paluan-batista left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afim de ajudar com um code review, deixei algumas sugestões.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants