* feat(whatsapp): allow converting inbox between WhatsApp providers
Adds a Convert flow to switch a WhatsApp inbox between the four
supported providers (default/360dialog, whatsapp_cloud, baileys, zapi)
without losing conversations, agents, or history.
- Channel::Whatsapp#convert_provider! runs inside a transaction:
disconnects the old provider, clears provider_connection and
message_templates, assigns the new provider/config, and triggers
webhook setup plus template resync on the new service.
- New POST /api/v1/accounts/:id/inboxes/:id/convert_provider endpoint
guarded by InboxPolicy#convert_provider? (admin only).
- UI adds a Convert button on the inbox Settings page with a
type-to-confirm ConvertInboxModal that lists the effects before
redirecting to a dedicated route reusing the WhatsApp provider
wizard in convert mode (phone number locked, current provider
hidden from the picker).
* chore(whatsapp): polish convert UI colors and expand specs
- Settings: use slate for the Convert trigger and ruby for the modal
confirm to mirror the delete gate instead of the less conventional
amber variant.
- Drop the redundant "current provider is hidden from the list"
sentence from the convert wizard description.
- Add specs for the post-conversion webhook setup path (triggered and
skipped branches) and the sync_templates error-rescue behaviour.
* fix: address CodeRabbit review on convert-provider flow
- Whitelist provider_config keys in the convert endpoint via permit
rather than permit!, and default to an empty hash when omitted so
the request no longer crashes.
- Pre-validate the new provider config before disconnecting the old
session so a bad target config no longer terminates the existing
provider; also keep the disconnect bound to the old provider_url.
- Guard ConvertInboxModal's submit handler so pressing Enter cannot
bypass the type-to-confirm gate, and migrate it to <script setup>.
- Reject invalid ?provider= query values in convert mode so hidden
providers (Twilio, the current provider) cannot be reached via URL.
- Await the inbox fetch in InboxConvert before running the route guard
so directly opening the route for a non-WhatsApp inbox redirects.
- Remove the unreachable second CloudWhatsapp branch in Whatsapp.vue.
* fix: address second CodeRabbit round on convert-provider flow
- Unify provider picker validation so create mode also rejects
unknown ?provider= values, with a single helper that accepts
available providers plus the whatsapp_manual fallback.
- Simplify the pre-validation rollback in convert_provider!: the
errors snapshot/merge dance was redundant because assign_attributes
does not clear errors.
- Follow the repo convention of asserting on error.class.name so the
rollback spec stays stable under reloading/parallel environments.
- Strengthen the controller success spec with provider_connection and
message_templates cleanup invariants, and set Content-Type on the
templates stub so HTTParty parses the empty data array correctly.
* fix: address third CodeRabbit round on convert-provider flow
- Add 360Dialog entry to the Whatsapp provider catalog, keep it hidden
from the create picker (preserving the existing fork behavior) but
expose it in the convert picker where it is a valid target. Restore
URL reachability for ?provider=360dialog in create mode.
- Scope the WHATSAPP_MANUAL allowance to create mode only: the manual
fallback flow is not reachable in convert mode.
- Redirect to the inboxes list in InboxConvert when the inbox is still
absent after the store fetch, so the page no longer stays blank.
- Use an explicit allowlist of WhatsApp providers to gate the Convert
button instead of negating Twilio, so adding a new WhatsApp channel
type will not silently expose the flow.
- Bind the disabled provider display field with :value instead of
v-model, since the underlying computed is getter-only.
- Add Content-Type: application/json to the templates stub in the
model spec so HTTParty parses the empty data array.
* fix: address fourth CodeRabbit round on convert-provider flow
- Reject no-op conversions that target the same provider as the one
already configured, so the endpoint no longer wipes provider
connection and message templates on a request that changes nothing.
- Call the provider service's disconnect directly so failures abort
the conversion instead of being silently swallowed; otherwise the
old external session could remain live while the inbox flips to
the new provider.
- Cover both behaviors with specs.
* fix: address fifth CodeRabbit round on convert-provider flow
- Reset the Vuelidate state when closing ConvertInboxModal so reopening
the gate does not surface stale validation errors.
- Call teardown_webhooks before converting away from whatsapp_cloud so
the Meta webhook subscription is removed for embedded_signup channels,
mirroring the destroy-time cleanup (manual-setup channels keep the
existing no-op behavior). Swallow teardown failures so a flaky Meta
call does not abort the swap.
- Switch the rollback specs to compare message_templates counts instead
of the boolean be_present matcher so they remain meaningful if the
fixture happens to have an empty templates list.
* fix: address sixth CodeRabbit round on convert-provider flow
- Derive the convert header's current-provider label from the shared
PROVIDER_CATALOG so the picker and header stay in sync.
- Assert the full Cloud provider_config payload and the absence of the
Baileys-only provider_url key on both the controller success spec
and the model atomic-swap spec.
- In the sync-error spec, reload and assert that the record was
actually flipped to the new provider before the sync rescue fires,
so the test can't pass on a pre-save failure.
* test: pin 422 error payload on convert_provider negative paths
The unsupported-conversion and invalid-config specs only checked the
status code, so they would have stayed green if the 422 started coming
from a different branch. Pin the response body so each example actually
covers the failure case it names.
* fix(baileys): save custom host as provider_url, not url
The Baileys form was writing the custom endpoint to
provider_config['url'] while the backend reads
provider_config['provider_url']. That silently broke the custom-host
feature for newly created or converted Baileys inboxes: they always
fell back to BAILEYS_PROVIDER_DEFAULT_URL. Align the key on both ends.
* fix(whatsapp): skip second validation pass in convert_provider!
The transaction's save! was re-running validate_provider_config after
the old provider's session had already been disconnected, so a transient
Graph API failure on the second check could roll back the swap while
leaving the external session terminated — the exact inconsistency the
pre-flight valid? was meant to rule out.
Capture the validated provider_config snapshot after valid? (so fields
populated by before_validation callbacks like webhook_verify_token are
preserved) and switch the final persist to save!(validate: false) so the
earlier check stays authoritative.
* fix: normalize provider-conversion failures and pass accountId
- The convert_provider action only rescued ActiveRecord::RecordInvalid,
so disconnect/teardown failures bubbled up as 500 with no stable
payload. Catch StandardError, log the class + message, and return a
422 with a generic user-facing message so the dashboard can surface
the error consistently.
- Nested settings routes live under /accounts/:accountId, so the
router push from Settings.vue must include accountId alongside
inboxId. Mirrors how sibling pages navigate to settings_inbox_show.
* fix: report missing :provider as 400 and sync modal v-model
- The generic rescue StandardError on convert_provider was masking
ActionController::ParameterMissing behind a misleading
provider-conversion error message. Catch it explicitly before the
generic rescue and return 400 with the parameter-missing message.
- ConvertInboxModal's closeModal now drives localShow to false so
parents using v-model:show stay in sync on every close path,
not only when the explicit onClose listener flips the flag.
* fix(whatsapp): serialize concurrent convert_provider calls with_lock
Without a per-record lock, two admin requests against the same inbox
could both pass the pre-flight validation, race the disconnect/save,
and then run setup_webhooks/sync_templates in arbitrary order, leaving
the persisted provider out of sync with the external configuration.
Wrap the whole convert flow in with_lock so the loser blocks until the
winner commits; the subsequent no-op guard then rejects a second
conversion request targeting the provider the first one just set.
* test: harden convert_provider policy + controller failure specs
- Pass accountId explicitly in InboxConvert redirects so the route
navigation mirrors how Settings.vue reaches settings_inbox_convert.
- Add a spec that assigns the agent to the inbox and still expects 401,
so a future regression in InboxPolicy#convert_provider? can no longer
slip past on the show policy alone.
- Add a spec that stubs convert_provider! to raise StandardError and
asserts the controller's generic-failure 422 payload, pinning the
dashboard contract for provider-side failures.
* test: pin convert_provider success response payload
Parse the rendered body and assert provider + provider_config so the
spec catches regressions where the DB is updated correctly but the
serialized response drifts (dashboard store commits response.data).
* fix(whatsapp): reset teardown guard after pre-conversion webhook cleanup
teardown_webhooks memoizes @webhook_teardown_initiated = true to prevent
double execution during destroy. Calling it from convert_provider!
leaves that flag set, so a subsequent destroy! or follow-up conversion
on the same instance would skip webhook removal silently. Reset the
flag in an ensure block so the destroy-time guard stays scoped to
destroy only.
* fix: include accountId in post-conversion redirect params
* test: pin same-provider convert returns 422
* fix(whatsapp): reset template columns when post-conversion sync fails
* fix(convert): enforce provider allowlist in InboxConvert route guard
* test: broaden Cloud templates stub to match account-scoped path
* test(whatsapp): cover cloud to baileys conversion branch
Account webhooks sign outgoing payloads with HMAC-SHA256, but agent bot
and API inbox webhooks were delivered unsigned. This PR adds the same
signing to both.
Each model gets a dedicated `secret` column rather than reusing the
agent bot's `access_token` (for API auth back into Chatwoot) or the API
inbox's `hmac_token` (for inbound contact identity verification). These
serve different trust boundaries and shouldn't be coupled — rotating a
signing secret shouldn't invalidate API access or contact verification.
The existing `Webhooks::Trigger` already signs when a secret is present,
so the backend change is just passing `secret:` through to the jobs.
Shared token logic is extracted into a `WebhookSecretable` concern
included by `Webhook`, `AgentBot`, and `Channel::Api`. The frontend
reuses the existing `AccessToken` component for secret display. Secrets
are admin-only and excluded from enterprise audit logs.
### How to test
Point an agent bot or API inbox webhook URL at a request inspector. Send
a message and verify `X-Chatwoot-Signature` and `X-Chatwoot-Timestamp`
headers are present. Reset the secret from settings and confirm
subsequent deliveries use the new value.
---------
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Fixes
https://linear.app/chatwoot/issue/CW-5692/whatsapp-es-numbers-stuck-in-pending-due-to-premature-registration
### Problem
Multiple customers reported that their WhatsApp numbers remain stuck in
**Pending** in WhatsApp Manager even after successful onboarding.
- Our system triggers a **registration call**
(`/<PHONE_NUMBER_ID>/register`) as soon as the number is OTP verified.
- In many cases, Meta hasn’t finished **display name
review/provisioning**, so the call fails with:
```
code: 100, error_subcode: 2388001
error_user_title: "Cannot Create Certificate"
error_user_msg: "Your display name could not be processed. Please edit
your display name and try again."
```
- This leaves the number stuck in Pending, no messaging can start until
we manually retry registration.
- Some customers have reported being stuck in this state for **7+
days**.
### Root cause
- We only check `code_verification_status = VERIFIED` before attempting
registration.
- We **don’t wait** for display name provisioning (`name_status` /
`platform_type`) to be complete.
- As a result, registration fails prematurely and the number never
transitions out of Pending.
### Solution
#### 1. Health Status Monitoring
- Build a backend service to fetch **real-time health data** from Graph
API:
- `code_verification_status`
- `name_status` / `display_name_status`
- `platform_type`
- `throughput.level`
- `messaging_limit_tier`
- `quality_rating`
- Expose health data via API
(`/api/v1/accounts/:account_id/inboxes/:id/health`).
- Display this in the UI as an **Account Health tab** with clear badges
and direct links to WhatsApp Manager.
#### 2. Smarter Registration Logic
- Update `WebhookSetupService` to include a **dual-condition check**:
- Register if:
1. Phone is **not verified**, OR
2. Phone is **verified but provisioning incomplete** (`platform_type =
NOT_APPLICABLE`, `throughput.level = NOT_APPLICABLE`).
- Skip registration if number is already provisioned.
- Retry registration automatically when stuck.
- Provide a UI banner with complete registration button so customers can
retry without manual support.
### Screenshot
<img width="2292" height="1344" alt="CleanShot 2025-09-30 at 16 01
03@2x"
src="https://github.com/user-attachments/assets/1c417d2a-b11c-475e-b092-3c5671ee59a7"
/>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
* chore: allow non-admin to call `setup_channel_provider` and add localization
* feat: allow agents to setup channel provider for assigned inboxes and handle unauthorized access
* feat: baileys provider and placeholder for link device modal
* chore: drop qrcode.vue in favor of just img tag
* chore: update modal props
* feat: setup channel provider connection
* chore: update .env.example with Baileys API default configuration
* feat: add support for Baileys provider in WhatsApp events processing
* chore: rename Baileys API default host variable to DEFAULT_BAILEYS_URL
* feat: add setup and disconnect methods for Baileys channel provider in inboxes controller that will be implemented
* feat: add CHANNEL_CONNECTION_UPDATE event and include it in broadcast data preparation
* refactor: simplify channel retrieval logic in WhatsappEventsJob
* refactor: revert CHANNEL_UPDATE_EVENTS constant from ActionCableBroadcastJob
* feat: add 'baileys' as a provider option in Whatsapp channel model
* feat: add provider_connection field to Whatsapp channel model and migration
* refactor: remove unnecessary CHANNEL_CONNECTION_UPDATE event type
* feat: implement channel provider connection with baileys API
* feat: add inbox association to Whatsapp channel model and update webhook URL handling
* feat: enhance Baileys service to handle webhook multiple event types
* refactor: simplify webhook verification logic in Baileys service
* feat: add setup channel provider call, and refactor some logic
* chore: adapt logic to new API
* refactor: fix typo
* refactor: fix import
* refactor: fix typo
* chore: add fixme comment about race condition
* fix: remove double disconnect call
* feat: implement message processing for incoming WhatsApp messages
* refactor: streamline message type determination and improve readability
* chore: increase cache key granularity
provider connection info might be updated multiple times within 1 second, so updates might be lost due to cache key not being updated. changing cache key to milliseconds solves this
* feat: add `is-loading` to buttons
* feat: update send_message method to use 'to' parameter and improve error handling
* refactor: simplify test setup and update API key in specs
* chore: add setup and disconnect channel provider specs
* test: fix spec after increase cache key granularity
* feat: handle reconnecting state on modal
* style: centered error text
* feat: advanced options on create inbox
* feat: handle new reconnecting on backend
* refactor: update inbox controller specs and leave a FIXME note
* test: add specs for Whatsapp::IncomingMessageBaileysService
* feat: add baileys configuration page
* feat: link device button when disconnected on conversation
* chore: refactor .env.example
* feat: add TODO for unimplemented methods in IncomingMessageBaileysService
* fix: correct method name and update environment variable references in WhatsappBaileysService
* refactor: simplify channel lookup by removing redundant method and handling phone number check directly
* chore: add TODO for unimplemented event processing methods in IncomingMessageBaileysService
* fix: update environment variable references in WhatsappBaileysService tests
* chore(webhook): add pt-BR translations
* chore: add pt-br translations
* chore: inboxname component margin
* refactor: inboxname computed prop
* feat: enhance WhatsApp provider connection handling and message processing
* test: inbox controller
* chore: improve baileys connection and messages handling
* test: incoming message service baileys
* refactor: update provider config validation and improve test setup for WhatsApp Baileys service
* fix: ensure only text messages are sent and update message source ID
* fix: create message
* fix: only update message on success
* test: fix broken specs
* chore: raise error on unsupported message content type
* feat: hide provider connection data from non-admins
* fix: update advanced options
* chore: move class definition
* fix: issue with send_message not returning id
---------
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
Migration Guide: https://chwt.app/v4/migration
This PR imports all the work related to Captain into the EE codebase. Captain represents the AI-based features in Chatwoot and includes the following key components:
- Assistant: An assistant has a persona, the product it would be trained on. At the moment, the data at which it is trained is from websites. Future integrations on Notion documents, PDF etc. This PR enables connecting an assistant to an inbox. The assistant would run the conversation every time before transferring it to an agent.
- Copilot for Agents: When an agent is supporting a customer, we will be able to offer additional help to lookup some data or fetch information from integrations etc via copilot.
- Conversation FAQ generator: When a conversation is resolved, the Captain integration would identify questions which were not in the knowledge base.
- CRM memory: Learns from the conversations and identifies important information about the contact.
---------
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
- This PR adds a feature to auto-trigger handoff events when an Agent bot toggles a conversation status from Pending to Open
Co-authored-by: Sojan <sojan@pepalo.com>
This commit introduces the ability to associate response sources to an inbox, allowing external webpages to be parsed by Chatwoot. The parsed data is converted into embeddings for use with GPT models when managing customer queries.
The implementation relies on the `pgvector` extension for PostgreSQL. Database migrations related to this feature are handled separately by `Features::ResponseBotService`. A future update will integrate these migrations into the default rails migrations, once compatibility with Postgres extensions across all self-hosted installation options is confirmed.
Additionally, a new GitHub action has been added to the CI pipeline to ensure the execution of specs related to this feature.
* Delete inbox avatar
1) New API endpoint added for deleting inbox avatar.
2) Delete avatar button in the inbox settings page.
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* Refactor: Inbox store, remove inboxes from sidebar
* Add a new page for inbox settings
* Show inboxes on sidebar
* Add inbox_members API
* Disable similar-code check
* Fix codeclimate scss issues
* Add widget_color update API and actions
* Add specs for inbox store
* Fix Facebook auth flow
* Fix agent loading, inbox name