From a452ce9e84bcd6ee95e34e8433fc08f2b0c732c5 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:48:16 +0530 Subject: [PATCH] feat(whatsapp): add webhook registration and status endpoints (#13551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Adds webhook configuration management for WhatsApp Cloud API channels, allowing administrators to check webhook status and register webhooks directly from Chatwoot without accessing Meta Business Manager. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## Screenshots Screenshot 2026-03-05 at 7 04 18 PM Screenshot 2026-03-05 at 7 04 29 PM ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth --- .../concerns/whatsapp_health_management.rb | 55 +++++++++++++ .../api/v1/accounts/inboxes_controller.rb | 40 +--------- app/javascript/dashboard/api/inboxHealth.js | 4 + .../dashboard/i18n/locale/en/inboxMgmt.json | 10 +++ .../dashboard/settings/inbox/Settings.vue | 24 +++++- .../inbox/components/AccountHealth.vue | 77 +++++++++++++++++++ app/services/whatsapp/health_service.rb | 13 +++- .../whatsapp/webhook_setup_service.rb | 15 ++-- config/routes.rb | 1 + 9 files changed, 194 insertions(+), 45 deletions(-) create mode 100644 app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb diff --git a/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb b/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb new file mode 100644 index 000000000..795d7f2a9 --- /dev/null +++ b/app/controllers/api/v1/accounts/concerns/whatsapp_health_management.rb @@ -0,0 +1,55 @@ +module Api::V1::Accounts::Concerns::WhatsappHealthManagement + extend ActiveSupport::Concern + + included do + skip_before_action :check_authorization, only: [:health, :register_webhook] + before_action :check_admin_authorization?, only: [:register_webhook] + before_action :validate_whatsapp_cloud_channel, only: [:health, :register_webhook] + end + + def sync_templates + return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel? + + trigger_template_sync + render status: :ok, json: { message: 'Template sync initiated successfully' } + rescue StandardError => e + render status: :internal_server_error, json: { error: e.message } + end + + def health + health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status + render json: health_data + rescue StandardError => e + Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}" + render json: { error: e.message }, status: :unprocessable_entity + end + + def register_webhook + Whatsapp::WebhookSetupService.new(@inbox.channel).register_callback + + render json: { message: 'Webhook registered successfully' }, status: :ok + rescue StandardError => e + Rails.logger.error "[INBOX WEBHOOK] Webhook registration failed: #{e.message}" + render json: { error: e.message }, status: :unprocessable_entity + end + + private + + def validate_whatsapp_cloud_channel + return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud' + + render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request + end + + def whatsapp_channel? + @inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?) + end + + def trigger_template_sync + if @inbox.whatsapp? + Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel) + elsif @inbox.twilio? && @inbox.channel.whatsapp? + Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel) + end + end +end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 322c7c7fe..4ca9a6af8 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -4,8 +4,9 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController before_action :fetch_agent_bot, only: [:set_agent_bot] before_action :validate_limit, only: [:create] # we are already handling the authorization in fetch inbox - before_action :check_authorization, except: [:show, :health] - before_action :validate_whatsapp_cloud_channel, only: [:health] + before_action :check_authorization, except: [:show] + + include Api::V1::Accounts::Concerns::WhatsappHealthManagement def index @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] })) @@ -70,23 +71,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') } end - def sync_templates - return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel? - - trigger_template_sync - render status: :ok, json: { message: 'Template sync initiated successfully' } - rescue StandardError => e - render status: :internal_server_error, json: { error: e.message } - end - - def health - health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status - render json: health_data - rescue StandardError => e - Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}" - render json: { error: e.message }, status: :unprocessable_entity - end - private def fetch_inbox @@ -98,12 +82,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] end - def validate_whatsapp_cloud_channel - return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud' - - render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request - end - def create_channel return unless allowed_channel_types.include?(permitted_params[:channel][:type]) @@ -200,18 +178,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController def get_channel_attributes(channel_type) channel_type.constantize.const_defined?(:EDITABLE_ATTRS) ? channel_type.constantize::EDITABLE_ATTRS.presence : [] end - - def whatsapp_channel? - @inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?) - end - - def trigger_template_sync - if @inbox.whatsapp? - Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel) - elsif @inbox.twilio? && @inbox.channel.whatsapp? - Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel) - end - end end Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController') diff --git a/app/javascript/dashboard/api/inboxHealth.js b/app/javascript/dashboard/api/inboxHealth.js index 181b041ba..b8f69fcfe 100644 --- a/app/javascript/dashboard/api/inboxHealth.js +++ b/app/javascript/dashboard/api/inboxHealth.js @@ -9,6 +9,10 @@ class InboxHealthAPI extends ApiClient { getHealthStatus(inboxId) { return axios.get(`${this.url}/${inboxId}/health`); } + + registerWebhook(inboxId) { + return axios.post(`${this.url}/${inboxId}/register_webhook`); + } } export default new InboxHealthAPI(); diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 8b982dc21..0455bdfa1 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -685,6 +685,16 @@ "SANDBOX": "Sandbox", "LIVE": "Live" } + }, + "WEBHOOK": { + "TITLE": "Webhook Configuration", + "DESCRIPTION": "Webhook URL is required for your WhatsApp Business Account to receive messages from customers", + "ACTION_REQUIRED": "Webhook not configured", + "REGISTER_BUTTON": "Register Webhook", + "REGISTER_SUCCESS": "Webhook registered successfully", + "REGISTER_ERROR": "Failed to register webhook. Please try again.", + "CONFIGURED_SUCCESS": "Webhook configured successfully", + "URL_MISMATCH": "Webhook URL mismatch" } }, "SETTINGS": "Settings", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index a3c0bceed..0ff1ac61e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -99,6 +99,7 @@ export default { healthData: null, isLoadingHealth: false, healthError: null, + isRegisteringWebhook: false, widgetBubblePosition: 'right', widgetBubbleType: 'standard', widgetBubbleLauncherTitle: '', @@ -424,6 +425,23 @@ export default { this.isLoadingHealth = false; } }, + async registerWebhook() { + if (!this.inbox) return; + + try { + this.isRegisteringWebhook = true; + await InboxHealthAPI.registerWebhook(this.inbox.id); + useAlert(this.$t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.REGISTER_SUCCESS')); + await this.fetchHealthData(); + } catch (error) { + useAlert( + error.message || + this.$t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.REGISTER_ERROR') + ); + } finally { + this.isRegisteringWebhook = false; + } + }, handleFeatureFlag(e) { this.selectedFeatureFlags = this.toggleInput( this.selectedFeatureFlags, @@ -1162,7 +1180,11 @@ export default {
- +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue index af32e6ad6..04c54ff0d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/AccountHealth.vue @@ -10,8 +10,14 @@ const props = defineProps({ type: Object, default: null, }, + isRegisteringWebhook: { + type: Boolean, + default: false, + }, }); +const emit = defineEmits(['registerWebhook']); + const { t } = useI18n(); const QUALITY_COLORS = { @@ -133,6 +139,28 @@ const formatModeDisplay = mode => const getModeStatusTextColor = mode => MODE_COLORS[mode] || 'text-n-slate-12'; const getStatusTextColor = status => STATUS_COLORS[status] || 'text-n-slate-12'; + +const showWebhookSection = computed( + () => props.healthData?.webhook_configuration !== undefined +); + +const webhookUrl = computed( + () => + props.healthData?.webhook_configuration?.whatsapp_business_account || + props.healthData?.webhook_configuration?.application +); + +const webhookConfigured = computed(() => !!webhookUrl.value); + +const webhookUrlMismatch = computed( + () => + webhookConfigured.value && + webhookUrl.value !== props.healthData?.expected_webhook_url +); + +const handleRegisterWebhook = () => { + emit('registerWebhook'); +};