Troubleshooting
Real failures hit during development + production, with fixes.
Install
Puppeteer install fails with "Cannot read properties of undefined"
Usually means a previous install was interrupted and left an empty version directory:
~/.cache/puppeteer/chrome/linux-127.0.6533.88/ ← emptyFix:
php artisan whatsapp:sidecar:install --clean--clean wipes node_modules AND ~/.cache/puppeteer before reinstalling. The install command also auto-removes empty version dirs on every run.
"Chromium download skipped" but I want it
The install command auto-detects and skips Chromium download only when --skip-chromium is passed. If you accidentally have PUPPETEER_SKIP_DOWNLOAD=true in your environment, unset it and re-run:
unset PUPPETEER_SKIP_DOWNLOAD
php artisan whatsapp:sidecar:install --clean"I want to use system Chrome instead"
php artisan whatsapp:sidecar:install --skip-chromium
# Then in .env:
PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stableSidecar
whatsapp:sidecar:start hangs forever
Symptom: the command never returns. Fixed in current version — but if you patched the code, make sure the spawn command uses the subshell-wrapper pattern:
'( cd … && nohup node index.js < /dev/null >> log 2>> err & echo $! > pidfile ) > /dev/null 2>&1'The outer > /dev/null 2>&1 is critical — PHP's shell_exec waits on the pipe otherwise.
whatsapp:sidecar:stop kills the wrapper but leaves Node running (macOS)
macOS nohup forks rather than execs, so $! is the wrapper PID, not Node's. The sidecar now writes its own PID via the SIDECAR_PID_FILE env var on boot, and the stop command waits up to 5s for that overwrite before killing.
If you see an orphan node process:
pgrep -f 'sidecar/index.js' | xargs killThen investigate why the PID file wasn't updated — usually a stale node_modules (re-run whatsapp:sidecar:install).
Sidecar starts but Laravel can't reach it
Sidecar transport error: Connection refusedCheck:
php artisan whatsapp:sidecar:status— running?curl -i http://127.0.0.1:3000/healthz— responds?WHATSAPP_WEB_HOSTmatches between sidecar bind and Laravel's connect settingWHATSAPP_WEB_PORTmatches- If
WHATSAPP_WEB_TOKENis set, Laravel uses it asAuthorization: Bearer. Mismatch = 401.
whatsapp:web:listen exits with "Stream open failed"
Old bug — early versions used PHP's fopen('http://…') which buffers infinite responses and times out. Current version uses cURL with CURLOPT_WRITEFUNCTION for true streaming. If you see this on a current install, check that you're not on a fork of the listen command.
Sidecar uses huge RAM
Each paired session keeps a Puppeteer Chromium instance. Budget 1–2 GB RAM per active session. To reduce:
- Stop sessions you're not using:
WhatsApp::web('foo')->stop()(keeps auth) - Run fewer sessions per box
Cloud API
"OAuth access token expired"
You're using a temporary token. Switch to a System User permanent token (Meta Business Settings → Users → System Users → generate). The setup walkthrough in installation covers this.
"(#100) Param phone_number_id required"
WHATSAPP_PHONE_NUMBER_ID is empty or wrong. Confirm it's the Phone Number ID from WhatsApp → API Setup, NOT the phone number itself and NOT the WABA ID.
Webhook verify fails with 403
The challenge handler returned 403 — your WHATSAPP_VERIFY_TOKEN in .env doesn't match the one entered in Meta's webhook config. They must be identical.
Webhook POST returns 401 in production
Signature mismatch:
- Confirm
WHATSAPP_APP_SECRETmatches Meta App Settings → Basic → App Secret - Confirm your reverse proxy isn't rewriting the request body (would break the HMAC)
- Set
WHATSAPP_VERIFY_SIGNATURE=falsetemporarily to confirm — never leave this off in prod
Webhook GET returns the challenge but POST never arrives
- Meta only delivers POSTs to webhooks subscribed to a field. In Meta App Dashboard → WhatsApp → Configuration, subscribe to
messages(and any others you want). - Production webhooks must be HTTPS. Localhost/HTTP webhooks Meta won't deliver to.
Template send returns "Template name does not exist in the translation"
The template language code you passed doesn't match what was approved. Check Meta Business Manager → Message Templates → your template → Language. Common mistakes: passing en when only en_US is approved.
UI
/whatsapp returns 404
WHATSAPP_UI_ENABLED=true?livewire/livewireinstalled? Routes only register if Livewire'sclass_existscheck passesphp artisan route:list | grep whatsapp— confirm routes existphp artisan route:clear— if you have route caching, it captures old state
UI loads but no styles (raw HTML)
You're on WHATSAPP_UI_CSS_MODE=vite but the host app doesn't have Vite set up. Either:
# Switch to standalone (pre-compiled CSS, no build needed)
WHATSAPP_UI_CSS_MODE=standaloneThen re-publish config so the new key is picked up:
php artisan vendor:publish --tag=laravel-whatsapp-config --forcemergeConfigFrom is shallow
Laravel's mergeConfigFrom only merges top-level keys. If your published config predates a new nested key (like ui.css_mode), the new default never reaches your config. Always --force re-publish after package upgrades.
UI loads but layout is broken (sidebar + main stacked)
Flux 2's grid layout depends on flux.css being imported. Tailwind's @source directive scans for class names but does NOT load CSS rule files. Add to resources/css/app.css:
@import 'tailwindcss';
@import '../../vendor/livewire/flux/dist/flux.css'; /* ← required */Adds ~13 KB to your built CSS.
Dark mode toggle doesn't persist
The Alpine pattern that actually works:
<flux:menu.radio.group x-data="{ value: $flux.appearance }"
x-model="value"
x-on:change="$flux.appearance = value">The simpler x-data="$flux.appearance" doesn't write back changes.
Modal won't open
<flux:modal> has no show prop. Use wire:model:
<flux:modal wire:model="showQrModal" name="qr-modal">…</flux:modal>public bool $showQrModal = false;
public function openQr(): void
{
$this->showQrModal = true;
}Conversations page hangs / endless loading
Old bug — avatar route lookups could timeout. Current version wraps avatar fetches with withTimeout() in the sidecar and caches both hits AND misses for 30 min server-side. If you see hangs on a current install, check:
tail -f storage/logs/whatsapp-sidecar.err.logMost common cause: WhatsApp Web hasn't finished booting (status: 'authenticating'). Wait for ready.
Conversations page is slow with many messages
The query is already an indexed range scan: (session_id, chat_id) ORDER BY id DESC LIMIT N. If it's slow, check:
- The
wa_messages_session_chat_id_indexindex exists (SHOW INDEXES FROM wa_messages;) - You haven't raised
WHATSAPP_UI_MESSAGES_INITIALto something silly like 10000
Bubble UI shows "from: 176815280758858@lid"
@lid IDs are WhatsApp Web's "linked device" IDs. The package resolves them via the contacts list — make sure your contact list has loaded (visit /whatsapp/contacts once after pairing).
Web sidecar — sending
"session not ready" (409)
Session is paired but whatsapp-web.js hasn't finished booting. Poll state() until 'ready':
do {
sleep(1);
$state = WhatsApp::web('main')->state();
} while ($state['status'] !== 'ready');"session not found" (404)
Session was never started or was destroyed. Call WhatsApp::web('main')->start() first.
QR never appears
Check sidecar logs:
tail -f storage/logs/whatsapp-sidecar.logCommon causes:
- Chrome/Chromium can't launch (sandbox issues on some Linux distros — sidecar already passes
--no-sandbox) - Port 3000 collision with another service — change
WHATSAPP_WEB_PORT
Messages send to phone numbers but not group IDs
Sidecar normalizes phone-shaped inputs. Group IDs (…@g.us) and broadcast lists must be passed in full WA ID form — don't strip the suffix.
Tests
Tests fail with "encryption key not set"
Livewire 4 encrypts component checksums. Set APP_KEY in your testbench env:
// tests/TestCase.php
protected function defineEnvironment($app): void
{
$app['config']->set('app.key', 'base64:'.base64_encode(random_bytes(32)));
}Tests fail with "Class Flux\FluxServiceProvider not found"
Add it to getPackageProviders():
protected function getPackageProviders($app): array
{
return [
\Livewire\LivewireServiceProvider::class,
\Flux\FluxServiceProvider::class,
\Kstmostofa\LaravelWhatsApp\LaravelWhatsAppServiceProvider::class,
];
}Database
Migrations run on the wrong connection
If you set WHATSAPP_DB_CONNECTION=whatsapp AFTER the initial migrate, the tables are on the default connection. Drop them on the default DB and re-run migrate — the published migrations honor the connection at migration time.
Tables named wa_messages but I want a prefix
WHATSAPP_DB_PREFIX=app_Apply BEFORE running migrations. If you set it after, the migration is now looking for app_wa_messages and won't find the existing wa_messages — rename manually or rollback + re-migrate.
When all else fails
# Full health snapshot
php artisan whatsapp:health --format=json
# Last 100 sidecar log lines
tail -n 100 storage/logs/whatsapp-sidecar.log storage/logs/whatsapp-sidecar.err.log
# All package routes
php artisan route:list | grep whatsapp
# Composer + Node versions
composer show kstmostofa/laravel-whatsapp
node --version && npm --versionStill stuck? Open an issue with that output + a minimal repro.