name: chatwoot-whatsapp-webhook-debug
description: Debug and fix WhatsApp webhook verification failures on self-hosted Chatwoot — masked phone numbers in DB and nginx WebSocket config
Chatwoot WhatsApp Cloud Webhook Debug & Fix
Debug and fix WhatsApp webhook verification failures on self-hosted Chatwoot (VPS with Docker + nginx).
Symptoms
- - Meta/Facebook webhook verification returns:
(#100) Before override the current callback uri, your app must be subscribed to receive messages - - Chatwoot returns 401/422 on webhook verification attempt from Meta
- -
Error; wrong verify tokenwhen Meta calls the verify endpoint - - Browser console shows WebSocket failures:
WebSocket connection to 'wss://chat.rochasalesseguros.com.br/cable' failed - - Meta webhook verification fails:
(#N/A:WBxP-1061056854-4223745678) Não foi possível validar a URL de callback - - Token mismatch error even though
webhook_verify_tokenin DB matches - - Rails logs show
Parameters: {"phone_number" => "+551**1508-1701"}— double-masked with hyphen - - Chatwoot installed on VPS with Docker
- - nginx for reverse proxy
- - WhatsApp Cloud API channel configured
- - SSH access to VPS with
dockercommand available
Root Causes Found
1. Nginx missing WebSocket headers
Chatwoot uses ActionCable (WebSocket) for real-time updates. nginx proxy needs upgrade headers.
Fix — update /etc/nginx/sites-enabled/chatwoot:
`
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /cable {
proxy_pass http://127.0.0.1:3000/cable;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
`
Then: nginx -t && nginx -s reload
2. Phone number masked in Chatwoot database
The channel_whatsapp.phone_number column stores masked values like +551**1701, but the Meta API callback URL uses the real number +5511950801701. The valid_token? method does Channel::Whatsapp.find_by(phone_number: params[:phone_number]) which fails to find the channel.
Debug steps:
`bash
1. Test webhook verification manually
curl "https://chat.example.com/webhooks/whatsapp/+5511950801701?hub.mode=subscribe&hub.verify_token=TOKEN&hub.challenge=123456"
2. Check if channel is found (add temporary debug to controller)
File: /app/app/controllers/webhooks/whatsapp_controller.rb
Add inside valid_token? before the return:
Rails.logger.warn "DEBUG WEBHOOK: phone_number=#{params[:phone_number].inspect} token=#{token.inspect}"
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
Rails.logger.warn "DEBUG WEBHOOK: channel found=#{channel.present?}"
3. Get real phone number from Meta API
curl "https://graph.facebook.com/v18.0/PHONE_NUMBER_ID?fields=display_phone_number&access_token=ACCESS_TOKEN"
4. Check raw database value
docker exec root-rails-1 bundle exec rails runner "puts Channel::Whatsapp.first.phone_number"
`
Fix — patch valid_token? to fall back to masked number matching:
`ruby
def valid_token?(token)
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
# Fallback: find by matching last 8 digits (handles masked DB values)
if channel.blank?
channel = Channel::Whatsapp.all.find do |c|
masked_digits = c.phone_number.gsub(/\D/, '')
request_digits = params[:phone_number].to_s.gsub(/\D/, '')
masked_digits[-8..-1] == request_digits[-8..-1]
end
end
return false unless channel.present?
whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token']
token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present?
end
`
Deployment:
`bash
docker cp whatsapp_controller.rb root-rails-1:/app/app/controllers/webhooks/whatsapp_controller.rb
docker restart root-rails-1
`
New Critical Finding (Jun 10 2026)
Phone number hyphen format mismatch.
Meta may send the callback URL with the phone number formatted with a hyphen (e.g., /webhooks/whatsapp/+551191508-1701) instead of the continuous E.164 format (+5511915081701). Chatwoot's Rails router may parse this as +551**1508-1701 (with double masking), causing the valid_token? lookup to fail silently with "Error; wrong verify token" even when the token itself is correct.
Symptoms:
Fix:
`sql
-- Check current format stored in DB
docker exec root-postgres-1 psql -U postgres -d chatwoot_production \
-c "SELECT id, phone_number, provider_config->>'webhook_verify_token' as token FROM channel_whatsapp;"
-- Update to match the format Meta is sending (with hyphen)
UPDATE channel_whatsapp SET phone_number = '+551191508-1701' WHERE id = 2;
`
Then re-test the webhook in Meta's dashboard.
Note: Using hyphenated format is not standard E.164. The proper fix is to ensure Meta sends the E.164 format without hyphen in the callback URL. But if Meta consistently sends hyphenated format, Chatwoot's routing and valid_token? must handle it.
Previous Critical Finding (May 27 2026)
The valid_token? method has already been patched to handle masked phone numbers. BUT a new issue was found: the URL format matters critically.
When Meta calls /webhooks/whatsapp/812251211981254 (phone_number_id format), Chatwoot returns 200 but does NOT create any conversation in the database.
When Meta calls /webhooks/whatsapp/+5511955701701 (real phone number format), Chatwoot creates the conversation correctly.
So the fix requires BOTH:
1. The valid_token? patch (handles masked DB values)
2. The URL in Meta App must use the real phone number (+5511955701701), NOT the phone_number_id (812251211981254)
Nginx log evidence
`
May 22 (working) — uses phone number format:
POST /webhooks/whatsapp/+551**1701 → 200 + conversation created
May 27 (broken) — uses phone_number_id format:
POST /webhooks/whatsapp/812251211981254 → 200 but NO conversation created
`
Meta Webhook Setup Fields
| Campo | Valor |
| ------- | ------- |
| URL de callback | https://chat.example.com/webhooks/whatsapp/+REAL_PHONE_NUMBER |
| Verificar token | webhook_verify_token from channel.provider_config |
Get token: docker exec root-postgres-1 psql -U postgres -d chatwoot_production -c "SELECT provider_config->>'webhook_verify_token' FROM channel_whatsapp;"
Debug: Test if webhook creates conversation
`bash
This creates a conversation in inbox 17:
curl -s --max-time 10 -k -X POST 'https://chat.rochasalesseguros.com.br/webhooks/whatsapp/+5511955701701' \
-H 'Content-Type: application/json' \
-d '{"object":"whatsapp_business_account","entry":[{"id":"1264409148885415","time":1700000000,"changes":[{"value":{"messaging_product":"whatsapp","metadata":{"phone_number_id":"812251211981254","display_phone_number":"+5511955701701"},"contacts":[{"profile":{"name":"Test"},"wa_id":"5511955701701"}],"messages":[{"from":"5511955701701","id":"test123","timestamp":"1700000000","type":"text","text":{"body":"Test message"}}]}},"field":"messages"}]}]}'
Verify:
docker exec root-postgres-1 psql -U postgres -d chatwoot_production \
-c "SELECT c.id, c.inbox_id, m.content FROM conversations c JOIN messages m ON m.conversation_id = c.id WHERE c.inbox_id = 17 AND m.created_at > NOW() - INTERVAL '2 minutes' ORDER BY m.created_at DESC LIMIT 3;"
`