Skip to content

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/   ← empty

Fix:

bash
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:

bash
unset PUPPETEER_SKIP_DOWNLOAD
php artisan whatsapp:sidecar:install --clean

"I want to use system Chrome instead"

bash
php artisan whatsapp:sidecar:install --skip-chromium
# Then in .env:
PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable

Sidecar

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:

php
'( 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:

bash
pgrep -f 'sidecar/index.js' | xargs kill

Then 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 refused

Check:

  1. php artisan whatsapp:sidecar:status — running?
  2. curl -i http://127.0.0.1:3000/healthz — responds?
  3. WHATSAPP_WEB_HOST matches between sidecar bind and Laravel's connect setting
  4. WHATSAPP_WEB_PORT matches
  5. If WHATSAPP_WEB_TOKEN is set, Laravel uses it as Authorization: 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_SECRET matches Meta App Settings → Basic → App Secret
  • Confirm your reverse proxy isn't rewriting the request body (would break the HMAC)
  • Set WHATSAPP_VERIFY_SIGNATURE=false temporarily 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/livewire installed? Routes only register if Livewire's class_exists check passes
  • php artisan route:list | grep whatsapp — confirm routes exist
  • php 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:

bash
# Switch to standalone (pre-compiled CSS, no build needed)
WHATSAPP_UI_CSS_MODE=standalone

Then re-publish config so the new key is picked up:

bash
php artisan vendor:publish --tag=laravel-whatsapp-config --force

mergeConfigFrom 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:

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:

blade
<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.

<flux:modal> has no show prop. Use wire:model:

blade
<flux:modal wire:model="showQrModal" name="qr-modal">…</flux:modal>
php
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:

bash
tail -f storage/logs/whatsapp-sidecar.err.log

Most 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_index index exists (SHOW INDEXES FROM wa_messages;)
  • You haven't raised WHATSAPP_UI_MESSAGES_INITIAL to 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':

php
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:

bash
tail -f storage/logs/whatsapp-sidecar.log

Common 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:

php
// 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():

php
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

bash
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

bash
# 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 --version

Still stuck? Open an issue with that output + a minimal repro.

Released under the MIT License.