Tech Verse Logo
Enable dark mode
Laravel WhatsApp: A New Package That Combines the Cloud API and whatsapp-web.js in One Library

Laravel WhatsApp: A New Package That Combines the Cloud API and whatsapp-web.js in One Library

Tech Verse Daily

Tech Verse Daily

4 min read

WhatsApp integration in Laravel has always forced developers to pick a side.

Meta's WhatsApp Business Cloud API is the official, stable, billed-per-conversation route. It's reliable and policy-safe — but it can only message users who messaged you first inside the last 24 hours. Outside that window, sends are restricted to pre-approved template messages. There's no support for groups, no Status / Stories, no free-form outbound conversations.

whatsapp-web.js fills those gaps. It drives a headless Chromium that speaks the same WebSocket protocol as the WhatsApp Web app, so it can do anything a logged-in user can. But it lives in Node.js, sits outside Meta's blessed API, and the existing PHP bridges tend to be fragile or coupled to abandoned forks.

Most Laravel WhatsApp packages on Packagist pick one of those two worlds. A new release tries to unify them.

Introducing kstmostofa/laravel-whatsapp

kstmostofa/laravel-whatsapp is a Laravel package (supporting Laravel 11, 12, and 13) that ships both backends behind a single facade, plus a Livewire/Flux admin UI that mounts at /whatsapp out of the box.

The three components:

  1. Pure-PHP Cloud API client — Guzzle-based. Covers templates, media, business profile, phone-number management, plus a webhook receiver that verifies Meta's HMAC SHA-256 signature.

  2. Bundled ~300-line Node sidecar wrapping whatsapp-web.js — runs on the same server as Laravel, exposes a small HTTP API, gives access to the features Cloud API doesn't.

  3. Drop-in Livewire + Flux admin UI at /whatsapp — dashboard, QR pairing, compose, conversations (chat-bubble interface), groups, contacts, webhooks log, health page. Light + dark mode included.

The architecture splits responsibilities cleanly:

┌─────────────────────────────────────────────────────┐
│  Laravel app                                        │
│                                                     │
│    WhatsApp::send('+9665XXX', 'Hi')                 │
│         │                                           │
│         ├──→ Cloud API (PHP, Guzzle) → graph.fb.com │
│         │                                           │
│         └──→ WebClient (HTTP) → 127.0.0.1:3000      │
│                                       │             │
└───────────────────────────────────────┼─────────────┘
                                        ▼
                            ┌───────────────────────┐
                            │ Sidecar (Node, ~300)  │
                            │ whatsapp-web.js × N   │
                            │ Puppeteer / Chromium  │
                            └───────────────────────┘
                                        ▲
                                        │ WebSocket
                                        ▼
                                  WhatsApp servers

The facade autoroutes based on the recipient's shape: a phone number goes to the Cloud API, a WhatsApp internal ID (…@c.us for users, …@g.us for groups) goes to the Web sidecar. An explicit backend override is available when needed:

use Kstmostofa\LaravelWhatsApp\Facades\WhatsApp;

// Routes to Cloud API (phone number recipient)
WhatsApp::send('+9665XXXXXXXX', 'Hello');

// Routes to Web sidecar (WhatsApp internal ID)
WhatsApp::send('9665XXXXXXXX@c.us', 'Hello from a personal number');

// Force a specific backend + session
WhatsApp::send('+9665XXXXXXXX', 'Force Web', 'web', 'main');

For anything beyond plain text — templates, media, groups, status — the corresponding resource is exposed directly:

// Cloud API: parameterised template
WhatsApp::messages()->sendTemplate('+9665XXX', 'order_ready', 'en_US', [
    ['type' => 'body', 'parameters' => [['type' => 'text', 'text' => 'Munir']]],
]);

// Web sidecar: create a group and message it
$group = WhatsApp::web('main')->groups()->create('Project X', [
    '9665XXX@c.us', '9665YYY@c.us',
]);

WhatsApp::web('main')
    ->messages()
    ->sendText($group['gid']['_serialized'], 'Welcome aboard');

// Web sidecar: post to Status / Stories (Cloud API can't do this at all)
WhatsApp::web('main')->status()->sendText('New feature live', [
    'backgroundColor' => '#075E54',
    'font' => 2,
]);

The dual-backend design lets developers use the right tool for each job — Cloud API for high-volume transactional sends (OTPs, order updates, broadcasts to consenting recipients), and the Web sidecar for personal-number scenarios that no other PHP package handles cleanly.

Inbound messages — two paths, one event API

Cloud API delivers events via webhooks. The package registers POST /webhooks/whatsapp automatically, verifies Meta's X-Hub-Signature-256 HMAC, and fires typed Laravel events:

use Kstmostofa\LaravelWhatsApp\Events\MessageReceived;

Event::listen(MessageReceived::class, function ($event) {
    Log::info('Inbound from '.$event->from().': '.$event->text());
});

The Web sidecar emits a Server-Sent Events stream per session. An artisan command consumes the stream and dispatches matching events on the PHP side:

php artisan whatsapp:web:listen main

The event class shapes mirror each other (Events\Web\MessageReceived, MessageAck, QrGenerated, Disconnected, SessionReady), so listeners look nearly identical regardless of source.

Set WHATSAPP_PERSIST_INCOMING=true and a built-in listener writes everything to a wa_messages table — including delivery acknowledgements, which the UI uses to render proper single, double, and blue-tick badges.

The bundled admin UI

Most WhatsApp packages stop at the SDK layer. This one ships the admin UI too.

/whatsapp auto-registers (gateable via your own auth middleware in production) and includes:

  • Dashboard — sidecar health, message volume by direction, recent activity table

  • Sessions — start new sessions, scan QRs via a modal that auto-updates through qr → authenticating → ready, stop or destroy sessions

  • Compose — send text, image, document, or template; backend selectable or auto-routed

  • Conversations — WhatsApp-style chat-bubble interface with lazy-loaded avatars, inline image previews, audio player, paperclip attachments, edit, delete-for-everyone, ack ticks, sound notifications, smart auto-scroll. The message query is an indexed (session_id, chat_id) ORDER BY id DESC LIMIT N scan, so it stays fast even with millions of rows

  • Groups, Contacts, Webhooks log, Health — the standard admin views

Built on Livewire and Flux UI with three CSS install paths so it works in any host application:

Headless mode is fully supported — uninstalling livewire/livewire makes the UI disappear, while the facade, events, jobs, and webhook receiver keep working unchanged.

Notable implementation details

A few decisions that aren't obvious from the README:

A ~300 LOC sidecar instead of an open-wa/openwa dependency. The entire surface area needed is a thin HTTP wrapper around whatsapp-web.js. Keeping the sidecar small enough to read in one sitting makes it auditable, easy to patch when WhatsApp Web's internals shift, and free of transitive dependencies on abandoned forks.

Detached process spawn that doesn't leak file descriptors. Spawning a long-running Node process from PHP is its own discipline. The sidecar uses a nohup-inside-subshell wrapper with stdout/stderr redirected to log files and stdin closed, so shell_exec returns immediately rather than blocking on shared pipes. On macOS, nohup forks rather than execs — meaning the shell's $! is the wrapper PID, not Node's. The sidecar writes its real PID to a file via SIDECAR_PID_FILE on boot, and PHP polls for it before declaring success.

SSE consumed via cURL streaming, not fopen. PHP's fopen('http://…') buffers indefinite responses, which means it can't consume an SSE stream. The listener uses curl_setopt($ch, CURLOPT_WRITEFUNCTION, ...) to process chunks as they arrive, with exponential backoff on dropped streams.

Separate database connection is a real, fully-wired option. Set WHATSAPP_DB_CONNECTION=whatsapp and WaSession, WaMessage, WaContact resolve the connection at runtime via a UsesWhatsAppConnection trait. Different driver too — your main app on MySQL, WhatsApp data on Postgres. Migrations honor the same configuration, calling Schema::connection(...) at migration time.

Webhook fails closed. If signature verification is enabled (the default) but WHATSAPP_APP_SECRET is unset, the middleware returns 503 service misconfigured and logs an error — never silently accepts unverified payloads.

5-minute install

composer require kstmostofa/laravel-whatsapp

php artisan vendor:publish --tag=laravel-whatsapp-config
php artisan vendor:publish --tag=laravel-whatsapp-migrations
php artisan migrate

Then take one of the two paths:

Cloud API — set Meta credentials in .env:

WHATSAPP_PHONE_NUMBER_ID=...
WHATSAPP_BUSINESS_ACCOUNT_ID=...
WHATSAPP_ACCESS_TOKEN=...     # permanent System User token
WHATSAPP_APP_SECRET=...
WHATSAPP_VERIFY_TOKEN=... 

Point Meta's webhook UI at https://yourapp.com/webhooks/whatsapp using the same verify token.

Web sidecar — install and start the Node service:

WHATSAPP_WEB_ENABLED=true
WHATSAPP_WEB_TOKEN=$(openssl rand -hex 24)

php artisan whatsapp:sidecar:install
php artisan whatsapp:sidecar:start

Visit /whatsapp/sessions, click Start, scan the QR with a phone. Pairing typically completes within 10 seconds.

The full reference lives at https://kstmostofa.github.io/laravel-whatsapp/.

Compatibility

The package's composer.json declares support for:

  • Laravel 11.x, 12.x, 13.x

  • Livewire 3.x, 4.x

  • PHP 8.2+ (Laravel 11/12) or 8.4+ (Laravel 13, due to a transitive Symfony 8 dependency)

The CI matrix runs phpunit across the cartesian product on each push. 46 tests / 133 assertions cover the service provider, message router, recipient parsing, Cloud client + error handling, Web client, webhook flow, models, and Livewire smoke tests.

Roadmap

Planned in upcoming releases:

  • Template builder UI (currently API-only)

  • Bulk-send wizard with rate-limit visualization

  • Multi-tenant gates baked into the UI middleware

  • A Laravel Echo channel helper for the live-update events

Caveats worth knowing

  • The Web sidecar drives WhatsApp via browser automation. Meta's Terms of Service technically prohibit this. Account bans happen if volume is pushed or automated patterns get aggressive. For production-grade marketing or transactional sends, the Cloud API is the safer route. The sidecar shines for human-driven, personal-number scenarios.

  • Each Web session keeps a Puppeteer Chromium instance running. Budget 1–2 GB RAM per active session in production. Running 50 sessions on a 1 GB VPS will OOM.

  • The bundled UI defaults to web middleware only. That's fine for local development, but the service provider logs a warning at boot when APP_ENV=production and no auth middleware is configured. Wrap the routes in ['web', 'auth', 'can:manage-whatsapp'] or similar before going live.

Resources

Installation:

composer require kstmostofa/laravel-whatsapp

kstmostofa/laravel-whatsapp is maintained by Md Mostafijur Rahman and released under the MIT license. Contributions and bug reports are welcome via the GitHub repository.

    Latest Posts

    View All

    Ensuring Secure URLs in Laravel Applications

    Ensuring Secure URLs in Laravel Applications

    Simple Feature Flags in Laravel with Laravel Toggle

    Simple Feature Flags in Laravel with Laravel Toggle

    Laravel WhatsApp: A New Package That Combines the Cloud API and whatsapp-web.js in One Library

    Laravel WhatsApp: A New Package That Combines the Cloud API and whatsapp-web.js in One Library

    Laravel diffForHumans() Guide: Display Human-Readable Time Like a Pro

    Laravel diffForHumans() Guide: Display Human-Readable Time Like a Pro

    Handling Large Datasets with Pagination and Cursors in Laravel MongoDB: Offset vs Cursor Pagination

    Handling Large Datasets with Pagination and Cursors in Laravel MongoDB: Offset vs Cursor Pagination

    A Complete Guide: Detecting and Fixing Race Conditions in Laravel Applications

    A Complete Guide: Detecting and Fixing Race Conditions in Laravel Applications

    PestPHP Intellisense in Laravel VS Code Extension v1.7.0

    PestPHP Intellisense in Laravel VS Code Extension v1.7.0

    Laravel Starter Kits Now Come with Built-in Toast Notifications

    Laravel Starter Kits Now Come with Built-in Toast Notifications

    Implement Laravel Search in a Right Way

    Implement Laravel Search in a Right Way

    Installing FreeSWITCH 1.10.X on Ubuntu 18.04 | 20.04 | 22.04 LTS

    Installing FreeSWITCH 1.10.X on Ubuntu 18.04 | 20.04 | 22.04 LTS