The `[WHATSAPP_MANUAL_TRANSFER] success` log introduced in #14975 to
track embedded signup → manual migrations was firing on any WhatsApp
credential change — including routine `api_key` rotations on inboxes
that were already manually configured — inflating the migration count.
The log now fires only for the actual migration, and uses a new tag so
log searches don't match the older over-counted entries.
## How to reproduce
1. On a manually configured WhatsApp Cloud inbox, update the API key
from inbox settings → Configuration.
2. Before this change, the app log records a `[WHATSAPP_MANUAL_TRANSFER]
success` line even though no migration happened; after this change it
stays silent.
3. Switching an embedded signup inbox to manual setup still logs the
migration (now as `[WHATSAPP_EMBEDDED_TO_MANUAL] success`).
## What changed
- `Channel::Whatsapp#log_credentials_transfer` now keys off the
migration's unique signal — `provider_config['source']` changing from
`embedded_signup` to absent — instead of diffing credential keys.
- Renamed the log tag from `WHATSAPP_MANUAL_TRANSFER` to
`WHATSAPP_EMBEDDED_TO_MANUAL` (success and failure lines) so the
corrected entries are searchable without matching pre-fix false
positives.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
WhatsApp Cloud API template sync currently fails silently — if the Graph
API call errors (expired token, rate limit, permission issue), the
channel simply keeps its stale templates with no trace in the logs. This
adds a warning log when the template fetch fails, so failed syncs are
visible and debuggable.
## What changed
- `Whatsapp::Providers::WhatsappCloudService#fetch_whatsapp_templates`
now logs a warning with the account id, inbox id, HTTP status code, and
Meta's error message when the response is not successful.
- The inbox id uses safe navigation since sync also runs from the
channel's `after_create` callback, before the inbox record exists.
- The request URL is intentionally not logged, as it contains the access
token as a query param.
## How to reproduce
1. Set up a WhatsApp Cloud inbox with an invalid/expired `api_key`.
2. Trigger a template sync (Inbox settings → sync templates, or wait for
the scheduler).
3. Previously nothing was logged; now a `[WHATSAPP] Template sync failed
for account ... inbox ...` warning appears in the Rails logs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Since embedded signup has been disabled on production, WhatsApp inboxes
that were created through it have no way to be managed going forward.
This PR lets admins transfer an embedded signup inbox to a manual Cloud
API setup: the inbox settings Configuration tab now shows the webhook
verification token and a "Switch to Manual Setup" form (pre-populated
with the Phone Number ID, Business Account ID, and API key stored during
the embedded signup journey) instead of the old Reconfigure button.
Saving the form updates the channel's `provider_config` without the
`source: embedded_signup` marker, so the inbox becomes a regular
manually-configured WhatsApp Cloud inbox. The backend re-validates the
submitted credentials against Meta before accepting the change.
## How to test
1. Open the settings page of a WhatsApp inbox created via embedded
signup → Configuration tab.
2. The Reconfigure button is gone; you see the webhook verification
token and a Switch to Manual Setup form pre-filled with the stored
credentials.
3. Configure the webhook in your own Meta app using the verification
token, enter a permanent access token from that app, and click "Switch
to Manual Setup".
4. On success the page switches to the standard manual configuration
view (verify token, API key update), and messaging continues to work
with the new credentials. Invalid credentials are rejected with an
error.
## What changed
- `ConfigurationPage.vue`: replaced the embedded-signup Reconfigure
section (and the hidden reauthorize component) with the manual transfer
form.
- New `WHATSAPP_MANUAL_TRANSFER_*` translation keys.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
> Reopened from #13613, now from a personal fork
(`gabrieljablonski/chatwoot`) so maintainers can push edits —
organization-owned forks don't support "Allow edits from maintainers".
The previous PR is closed in favor of this one; same commits, same diff.
## Description
This PR adds support for sending voice messages (voice notes) through
the WhatsApp Cloud API. When agents record audio in Chatwoot, it is now
transcoded in the browser from WebM/Opus to OGG/Opus and sent with the
`voice: true` flag, so it appears as a native voice note bubble on
WhatsApp — not as a file/document attachment.
Closes#13283
**Key Changes:**
- Added `webmOpusToOgg.js` — a pure JS EBML parser + OGG page builder
that remuxes browser-recorded WebM/Opus audio into OGG/Opus entirely
client-side, with no server-side dependencies.
- Updated `AudioRecorder.vue` to use an explicit `mimeType` hint, proper
resource cleanup, and an `AUDIO_EXTENSION_MAP` for correct file
extensions.
- Renamed `mp3ConversionUtils.js` → `audioConversionUtils.js` and added
OGG conversion support via the new remuxer.
- Updated `ReplyBox.vue` to request OGG format for WhatsApp channels,
pass `isVoiceMessage` per-attachment, and handle recording errors with a
user-facing alert.
- Updated `MessageBuilder` to read the `is_voice_message` param and
persist it in attachment metadata.
- Updated `WhatsappCloudService` to:
- Normalize `audio/opus` → `audio/ogg` content type on ActiveStorage
blobs (works around Marcel gem re-detection).
- Send the `voice: true` flag when the attachment is a voice message
with `audio/ogg` content type.
- Use WhatsApp Cloud API `v24.0` for the attachment endpoint.
- Added `AUDIO_CONVERSION_FAILED` i18n key.
**How it works:**
1. The browser records audio as WebM/Opus (Chrome/Firefox default).
2. `audioConversionUtils.js` remuxes it to OGG/Opus using the pure-JS
`webmOpusToOgg` remuxer — no server transcoding needed.
3. The OGG file is uploaded with `is_voice_message: true` in the form
payload.
4. `MessageBuilder` persists `is_voice_message` in the attachment's
`meta` hash.
5. `WhatsappCloudService` normalizes the blob content type if needed,
then sends the attachment with `voice: true` so WhatsApp renders it as a
voice note.
## Type of change
- [X] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
1. Record a voice message in a WhatsApp Cloud conversation.
2. Verify the audio is transcoded to OGG (check file extension in the
attachment preview).
3. Verify the message arrives on WhatsApp as a voice note bubble (not a
document/file).
4. Send an image or document attachment and verify it still works as
before (no `voice` flag).
5. Send a regular (non-voice) audio file and verify it arrives without
the voice flag.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Meta WhatsApp Cloud API surface needed for browser-based
calling. This is the second slice of the WhatsApp calling feature,
sitting on top of `feat/voice-call-model-wiring` and consumed by later
PRs (incoming-webhook pipeline, call service, frontend).
This PR ships only the provider-level HTTP wrapper and one error class.
It is feature-flag-free and does not change any user-visible behaviour
on its own — without later PRs, no caller invokes these methods.
## Linear
-
https://linear.app/chatwoot/issue/PLA-148/pr-2-meta-cloud-api-provider-methods
## What changed
- Add `Whatsapp::Providers::WhatsappCloudCallMethods`
(`enterprise/app/services/whatsapp/providers/whatsapp_cloud_call_methods.rb`)
wrapping six Meta endpoints:
- `pre_accept_call`, `accept_call`, `reject_call`, `terminate_call` —
`POST /{phone_id}/calls` with the relevant action payload.
- `send_call_permission_request` — `POST /{phone_id}/messages`
interactive `call_permission_request`.
- `initiate_call` — `POST /{phone_id}/calls` with `audio`/`offer`
session.
- Prepend the module into `Whatsapp::Providers::WhatsappCloudService`
only if defined, so OSS continues to work without the enterprise
overlay.
- Add `Voice::CallErrors::NoCallPermission`
(`enterprise/lib/voice/call_errors.rb`) — raised when Meta returns error
code `138006` from `initiate_call`. The remaining call-service errors
(`NotRinging`, `AlreadyAccepted`, `CallFailed`) will land with PR-4.
## How to test
There is no UI in this PR. Smoke-test from a Rails console with a
WhatsApp inbox configured for calling:
```ruby
inbox = Inbox.find(<id>)
svc = inbox.channel.provider_service
svc.respond_to?(:initiate_call) # => true
svc.respond_to?(:send_call_permission_request) # => true
# Optional live calls (require a real phone + Meta call-permission opt-in):
svc.send_call_permission_request('15551234567')
svc.initiate_call('15551234567', '<sdp_offer>')
```
Failure path: `initiate_call` against a contact who has not granted call
permission should raise `Voice::CallErrors::NoCallPermission` with
Meta's user-facing message.
Fixes https://github.com/chatwoot/chatwoot/issues/13317
Fixes an issue where WhatsApp attachment messages (images, audio, video,
documents) were failing to download. Messages were being created but
without attachments.
The `phone_number_id` parameter was being passed to the `GET
/<MEDIA_ID>` endpoint when downloading incoming media. According to
Meta's documentation:
> "Note that `phone_number_id` is optional. If included, the request
will only be processed if the business phone number ID included in the
query matches the ID of the business
phone number **that the media was uploaded on**."
For incoming messages, media is uploaded by the customer, not by the
business phone number. Passing the business's `phone_number_id` causes
validation to fail with error: `Param phone_number_id is not a valid
whatsapp business phone number id ID`
This PR removes the `phone_number_id` parameter from the media URL
request for incoming messages.
## Description
This pull request introduces an optional parameter, `phone_number_id`,
to the WhatsApp API call responsible for retrieving media. The addition
of this parameter allows for greater flexibility when interacting with
the WhatsApp API, as it can now accommodate scenarios where specifying a
particular phone number ID is necessary. This change is backward
compatible and does not affect existing functionality if the parameter
is not provided.
Fixes # (issue)
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
The changes were tested locally by invoking the WhatsApp media retrieval
API with and without the `phone_number_id` parameter. Both scenarios
were verified to ensure that:
- When `phone_number_id` is provided, the API call includes the
parameter and functions as expected.
- When `phone_number_id` is omitted, the API call continues to work as
before, maintaining backward compatibility.
No errors or warnings were observed during testing, and all relevant
unit tests passed successfully.
## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description
This implementation adds support for the `media_name` parameter for
WhatsApp document templates, resolving the issue where documents appear
as "untitled" when sent via templates.
**Problem solved:** Documents sent via WhatsApp templates always
appeared as "untitled" because Chatwoot didn't process the `filename`
field required by the WhatsApp API.
**Solution:** Added support for the `media_name` parameter that maps to
the WhatsApp API's `filename` field.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
## How Has This Been Tested?
Created and executed **7 comprehensive test scenarios**:
1. ✅ Document without `media_name` (backward compatibility)
2. ✅ Document with valid `media_name`
3. ✅ Document with blank `media_name`
4. ✅ Document with null `media_name`
5. ✅ Image with `media_name` (ignored as expected)
6. ✅ Video with `media_name` (ignored as expected)
7. ✅ Blank URL (returns nil appropriately)
**All tests passed** and confirmed **100% backward compatibility**.
## Technical Implementation
**Backend Changes:**
- `PopulateTemplateParametersService`: Added `media_name` parameter
support
- `TemplateProcessorService`: Pass `media_name` to parameter builder
- `WhatsappCloudService`: Updated documentation with `media_name`
example
**Frontend Changes:**
- `WhatsAppTemplateParser.vue`: Added UI field for document filename
input
- `templateHelper.js`: Include `media_name` for document templates
- `whatsappTemplates.json`: Added translation key for document name
placeholder
**Key Features:**
- 🔄 **100% Backward Compatible** - Existing templates continue working
- 📝 **Document Filename Support** - Users can specify custom filenames
- 🎯 **Document-Only Feature** - Only affects document media types
- ✅ **Comprehensive Testing** - All edge cases covered
## Expected Behavior
**Before:**
```ruby
# All documents appear as "untitled"
{
type: 'document',
document: { link: 'https://example.com/document.pdf' }
}
```
**After:**
```ruby
# With media_name - displays custom filename
{
type: 'document',
document: {
link: 'https://example.com/document.pdf',
filename: 'Invoice_2025.pdf'
}
}
# Without media_name - works as before
{
type: 'document',
document: { link: 'https://example.com/document.pdf' }
}
```
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
WhatsApp template message errors were not being properly handled because
the `@message instance` variable was only set in the `send_message`
method but not in `send_template`. When template sending failed, the
`handle_error` method couldn't update the message status due to the
missing @message reference, resulting in silent failures with no user
feedback.
This PR adds missing error handlers for the following channels and cases
1. WhatsApp - Generic Handlers for both Cloud and 360Dialog (Deprecated)
2. Instagram - Handler for a case where there is an HTTP error instead
of an `:error` in the 200 response
3. Facebook - Errors from the two sentry issues
([Net::OpenTimeout](https://chatwoot-p3.sentry.io/issues/6164805227) &
[JSON::ParserError](https://chatwoot-p3.sentry.io/issues/5903200786))
4. SMS: Generic handlers for Bandwidth SMS
#### Checklist
- [x] Bandwidth SMS
- [x] Whatsapp Cloud + 360 Dialog
- [x] Twilio SMS
- [x] Line
- [x] Telegram
- [x] Instagram
- [x] Facebook
- [x] GMail
- [x] 365 Mail
- [x] SMTP Mail
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
- update the templates updated at, even if the API request fails ( to prevent jobs from stacking up in case of API failures upstream )
- sequence the job in batches of 25 requests per minutes schedule ( in case API response time is high, also not to send too many requests in a single batch )
- move the sync job re-rerun to 3 hours ( since we are updating the updated at even in case of failures )(prev 15 minutes )
Fixes: https://linear.app/chatwoot/issue/CW-1590
* chore: update to ruby 3.1.3
* chore: ping docker version to alpine3.16 for nodev16.x
Starting with Node 17, nodejs switched to OpenSSL3. The docker builds
are installing node18.xx with alpine-3.1.3.
From Node.js 17's announcement post:
If you hit an ERR_OSSL_EVP_UNSUPPORTED error in your application
with Node.js 17, it’s likely that your application or a module you’re
using is attempting to use an algorithm or key size which is no longer
allowed by default with OpenSSL 3.0. A new command-line option,
--openssl-legacy-provider, has been added to revert to the legacy
provider as a temporary workaround for these tightened restrictions.
Looks like a webpack issue. This is fixed in webpacl 5+ and we are on
webpack4 at the moment.
Solutions
Upgrade webpack.
Pin nodejs version to be 16.x.x
Use --openssl-legacy-provider as a workaround.
Pin docker version to alpine3.16 branch to have node16.x by default
ref:
https://github.com/chatwoot/chatwoot/pull/5555#issuecomment-1379778532
* chore: update webmock
* chore: fix ruby gem path in dockerfile
* chore: switch to node16 in circleci
* chore: update ruby version in linux installer script
* chore: update ruby version in linux installer script
* chore: fix circleci
* chore: fix circleci
* feat: upgrade node version to 16.x in linux installer
* chore: update systemd files
Co-authored-by: Sojan Jose <sojan@chatwoot.com>