From f7303e12aa79cf3227a68008ce7b8f4532cb7d93 Mon Sep 17 00:00:00 2001 From: iscarelli Date: Sat, 11 Jul 2026 05:35:04 -0300 Subject: [PATCH 1/2] feat(cloud-api): support CTA URL buttons in sendButtons On the Cloud API (WhatsApp Business) channel, sendButtons hardcoded every button to type "reply", so a button with type "url" failed with "interactive.action.buttons.0.reply is required: id" (code 100). This made it impossible to send a link (CTA URL) button in a free-form session message, even though the Cloud API supports it via the interactive "cta_url" type. This preserves url buttons through buttonMessage and, when a cta_url button is present, builds an interactive cta_url message (action.name = "cta_url", parameters = { display_text, url }) instead of the reply-button payload. Per the Cloud API, cta_url allows a single link button and cannot be combined with reply buttons, so the first url button wins. The reply-button path is unchanged. The internal text echo now falls back to the button displayText so cta_url buttons no longer render as "undefined". Closes #1249 --- .../channel/meta/whatsapp.business.service.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 098654c2d0..92f078c842 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -1158,6 +1158,35 @@ export class BusinessStartupService extends ChannelStartupService { return await this.post(content, 'messages'); } if (message['buttons']) { + const ctaUrlButton = message['buttons'].find((button) => button.type === 'cta_url'); + if (ctaUrlButton) { + // WhatsApp Cloud API CTA URL button: a single tappable link button in a + // free-form (session) message. It cannot be combined with reply buttons. + content = { + messaging_product: 'whatsapp', + recipient_type: 'individual', + to: number.replace(/\D/g, ''), + type: 'interactive', + interactive: { + type: 'cta_url', + body: { + text: message['text'] || 'Select', + }, + action: { + name: 'cta_url', + parameters: { + display_text: ctaUrlButton.displayText, + url: ctaUrlButton.url, + }, + }, + }, + }; + quoted ? (content.context = { message_id: quoted.id }) : content; + message = { + conversation: `${message['text'] || 'Select'}\n▶️ ${ctaUrlButton.displayText}: ${ctaUrlButton.url}\n`, + }; + return await this.post(content, 'messages'); + } content = { messaging_product: 'whatsapp', recipient_type: 'individual', @@ -1176,7 +1205,7 @@ export class BusinessStartupService extends ChannelStartupService { quoted ? (content.context = { message_id: quoted.id }) : content; let formattedText = ''; for (const item of message['buttons']) { - formattedText += `▶️ ${item.reply?.title}\n`; + formattedText += `▶️ ${item.reply?.title ?? item.displayText}\n`; } message = { conversation: `${message['text'] || 'Select'}\n` + formattedText }; return await this.post(content, 'messages'); @@ -1536,6 +1565,13 @@ export class BusinessStartupService extends ChannelStartupService { { text: !embeddedMedia?.mediaKey ? data.title : undefined, buttons: data.buttons.map((button) => { + if (button.type === 'url') { + return { + type: 'cta_url', + displayText: button.displayText, + url: button.url, + }; + } return { type: 'reply', reply: { From dd7d91c2ac5c4faa4112413206f0f36c88a98e66 Mon Sep 17 00:00:00 2001 From: iscarelli Date: Sat, 11 Jul 2026 05:41:24 -0300 Subject: [PATCH 2/2] refactor: guard cta_url fields and drop no-op ternary Address review: use an if for the quoted context instead of the no-op ternary, only treat a url button as cta_url when it actually has a url, and fall back display_text to the url when displayText is absent. --- .../channel/meta/whatsapp.business.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 92f078c842..a755180814 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -1175,15 +1175,17 @@ export class BusinessStartupService extends ChannelStartupService { action: { name: 'cta_url', parameters: { - display_text: ctaUrlButton.displayText, + display_text: ctaUrlButton.displayText ?? ctaUrlButton.url, url: ctaUrlButton.url, }, }, }, }; - quoted ? (content.context = { message_id: quoted.id }) : content; + if (quoted) { + content.context = { message_id: quoted.id }; + } message = { - conversation: `${message['text'] || 'Select'}\n▶️ ${ctaUrlButton.displayText}: ${ctaUrlButton.url}\n`, + conversation: `${message['text'] || 'Select'}\n▶️ ${ctaUrlButton.displayText ?? ctaUrlButton.url}: ${ctaUrlButton.url}\n`, }; return await this.post(content, 'messages'); } @@ -1565,7 +1567,7 @@ export class BusinessStartupService extends ChannelStartupService { { text: !embeddedMedia?.mediaKey ? data.title : undefined, buttons: data.buttons.map((button) => { - if (button.type === 'url') { + if (button.type === 'url' && button.url) { return { type: 'cta_url', displayText: button.displayText,