Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Whatsapp Laravel Package

kstmostofa/laravel-whatsapp

Dual-backend WhatsApp for Laravel: a pure-PHP Meta Cloud API client plus a whatsapp-web.js sidecar for personal-number access, unified under one WhatsApp:: facade, with a Livewire admin UI, Eloquent models, webhooks, and health checks.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and configure for one backend first — trying to wire Cloud API and the sidecar in the same afternoon is the fastest way to a confusing debugging session.

composer require kstmostofa/laravel-whatsapp
php artisan vendor:publish --tag=laravel-whatsapp-config
php artisan vendor:publish --tag=laravel-whatsapp-migrations
php artisan migrate

Where to look first, in order:

  1. config/laravel-whatsapp.php after publishing — this is the single source of truth for which backend is active, the ui.middleware array, the DB connection, and broadcasting. Read it before writing any code.
  2. php artisan whatsapp:health --json — run it immediately after install. If it doesn't return sane output, nothing else will work.
  3. The WhatsApp:: facade — everything you'll write day-to-day goes through it.

Cloud API path (transactional, official, no Node on the host):

WHATSAPP_ACCESS_TOKEN=EAAG...
WHATSAPP_PHONE_NUMBER_ID=123456789012345
WHATSAPP_BUSINESS_ACCOUNT_ID=987654321098765
WHATSAPP_APP_SECRET=your-meta-app-secret
WHATSAPP_VERIFY_TOKEN=any-string-you-make-up

Sidecar path (personal number, groups, stories):

composer require livewire/livewire livewire/flux
php artisan whatsapp:sidecar:install     # one-time, downloads Chromium (~600 MB)
php artisan whatsapp:sidecar:start
php artisan whatsapp:web:listen main &

Then open /whatsapp/sessions, click Start, and scan the QR with the phone.

First use case — the one line that earns the package its keep:

use Kstmostofa\LaravelWhatsApp\Facades\WhatsApp;

WhatsApp::send('+966512345678', 'Hello from Laravel');        // → Cloud API
WhatsApp::send('[email protected]', 'Hello via personal');    // → Web sidecar

The routing is by recipient shape, not by config. E.164 (+…) goes Cloud; …@c.us goes sidecar. Internalise that before anything else, because it drives most of the design decisions below.


Implementation Patterns

Wrap the facade — don't sprinkle it

The WhatsApp::send() convenience method silently picks a backend from the phone-number format. In application code that's a hidden dependency. Put one thin service in front of it so the choice is explicit and testable:

final class Notifier
{
    public function transactional(string $e164, string $body): void
    {
        WhatsApp::messages()->sendText($e164, $body);   // always Cloud
    }

    public function personal(string $chatId, string $body): void
    {
        WhatsApp::web('main')->messages()->sendText($chatId, $body);
    }
}

Queue everything, and lean on the retry semantics

SendMessage is the job to reach for. It already distinguishes permanent Meta errors (recipient not on WhatsApp, invalid template) from transient ones and skips retries on the former — which is exactly the behaviour you'd otherwise hand-roll in failed().

use Kstmostofa\LaravelWhatsApp\Jobs\SendMessage;

SendMessage::dispatch('+966512345678', 'Queued hello')
    ->onQueue('whatsapp');

Give WhatsApp its own queue. Message sends are latency-bound on a third-party API and will happily starve your default queue during an outage.

Cloud API: think in templates, not strings

The 24-hour customer-service window is the constraint that reshapes your whole notification design. Outside it, only approved templates go through. So model your transactional messages as named templates with typed parameters from day one:

WhatsApp::messages()->sendTemplate('+966512345678', 'order_ready', 'en_US', [
    ['type' => 'body', 'parameters' => [['type' => 'text', 'text' => $order->customer_name]]],
]);

Keep template names and locale codes in config, not literals in jobs — Meta renames and re-approves templates, and you don't want that to be a deploy.

Sidecar: one listener per session, under Supervisor

whatsapp:web:listen [session] is a long-running daemon that bridges sidecar SSE into Laravel events. It is not request-scoped and it does not restart itself.

[program:wa-listen-main]
command=php /var/www/artisan whatsapp:web:listen main
numprocs=1
autostart=true
autorestart=true

Run exactly one process per session. Two listeners on the same session means duplicated events and duplicated PersistIncomingMessage writes.

Inbound: bind to events, not to controllers

Inbound traffic arrives as Laravel events, which means your handlers are ordinary listeners and your webhook controller stays empty:

Event::listen(\Kstmostofa\LaravelWhatsApp\Events\Web\MessageReceived::class, function ($event) {
    if ($event->from() === config('services.ops.number')) {
        app(CommandRouter::class)->handle($event->body());
    }
});

Because Web events implement ShouldBroadcast, the same event can drive both your domain logic and a live Blade/Alpine panel — no extra plumbing. Enable WHATSAPP_BROADCAST=true and run Reverb if you want the bundled UI to update without polling.

Persistence: use the package models, isolate the connection

WaSession / WaMessage / WaContact give you queryable chat history for free. If your app DB is already large or you're multi-tenant, set WHATSAPP_DB_CONNECTION=whatsapp and WHATSAPP_DB_PREFIX before running migrations — the migration files honour both at migration time, so changing your mind later means a manual data move.

Auth on the admin UI — do it on day one

/whatsapp is a full read/write surface over your WhatsApp account. Point ui.middleware at your existing auth stack in the config before the first deploy:

'ui' => [
    'middleware' => ['web', 'auth', 'can:admin'],
],

In production without this, the service provider logs a warning at boot — treat that warning as a blocker, not a note.

Health checks belong in your monitoring, not your eyeballs

php artisan whatsapp:health --json --exit-code

--exit-code makes it a proper probe. Wire it into your uptime monitor alongside a queue-depth check; a dead sidecar is silent otherwise.


Gotchas and Tips

The recipient-shape routing is invisible until it bites. WhatsApp::send('+966…') and WhatsApp::send('966…@c.us') hit completely different backends with different failure modes, rate limits and delivery semantics. A typo in the format silently changes the transport. Validate recipient format at the boundary of your own code and prefer the explicit messages() / web('main')->messages() paths in anything that matters.

The sidecar is browser automation against a moving target — budget for it. The v1.0.3 release notes are a case study: WhatsApp Web minified its own internals and broke message IDs (arriving as id: null, with the serialized key gone) and getChats() began returning {"error": "r"}, taking /chats and /groups down. Both are patched, but the pattern recurs. Pin the package version, watch the releases feed, and don't build your only revenue path on the sidecar. Keep Cloud API for anything you'd be paged about at 3am.

Account-ban risk is real and not a bug you can fix. The README is honest that the sidecar sits in a ToS grey area. Use a number you can afford to lose. Never pair a company's primary business number.

The webhook fails closed with a 503 when WHATSAPP_APP_SECRET is unset. Correct behaviour, but it looks like a broken integration in staging. Set the secret everywhere, or set it explicitly and note the behaviour in your runbook — otherwise someone will spend an hour on "why is Meta retrying".

WHATSAPP_WEB_TOKEN is the whole security boundary on the sidecar. It's a shared bearer secret between PHP and the Node process. Generate something long. The sidecar binds 127.0.0.1 by default — leave it that way unless you've genuinely thought about exposing a Chromium instance that's logged into a WhatsApp account.

PHP version is coupled to the Laravel version. Laravel 11/12 → PHP 8.2+; Laravel 13 pulls Symfony 8 transitively and needs PHP 8.4+. Check this before you plan an upgrade; it's a hard floor, not a suggestion.

CSS mode is a one-line decision with big blast radius. WHATSAPP_UI_CSS_MODE=vite (default) expects Tailwind v4 and three @source lines in your app.css. If you're on Tailwind v3, Bootstrap, or nothing, switch to standalone and stop thinking about it — the pre-compiled bundle (~32 KB gz) loads only on /whatsapp/* and leaves your main app untouched. Don't fight the default if your build isn't Tailwind v4.

Windows works, but was patched into working. v1.0.1 replaced which, rm -rf, nohup and posix_kill with cross-platform equivalents, and seeded the PID file from proc_open so sidecar:start doesn't fail while Node is still loading. If you're on Windows, be on ≥1.0.1 and use WHATSAPP_WEB_CHROME_PATH (or PUPPETEER_EXECUTABLE_PATH) with whatsapp:sidecar:install --skip-chromium to reuse a system Chrome rather than pulling 600 MB per machine.

Debugging order that actually works: whatsapp:health --json → whatsapp:sidecar:status → whatsapp:web:listen output. If the health command is clean but messages vanish, the problem is almost always the listener process not running or running twice. If the sidecar is up but /chats 500s, you're looking at the minified-internals class of bug — check for a newer patch release before debugging your own code.

Extension points worth knowing: the broadcastable events (hook your own listeners, or swap in your Echo channels), the Eloquent models (add scopes, relationships, a Chat aggregate), and the Livewire components (publishable and overridable if you need to change the inbox layout rather than rebuild it). The SendMessage job is also a clean seam for adding rate limiting — the README lists a bulk-send job with rate limiting as still-on-the-roadmap, so if you need it now, that's where it goes.

Headless is a legitimate target. You don't have to install Livewire or Flux at all. The facade, events, jobs, webhook receiver and CLI commands all work without the UI — worth remembering if you're embedding WhatsApp into an existing back-office rather than standing up a second admin panel.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr