whatsapp/chat-api
whatsapp/chat-api is a Laravel/PHP package for integrating WhatsApp chat messaging into your application. It provides a convenient API for connecting your app to WhatsApp chat services and handling common messaging workflows.
What you're actually installing. mgp25/Chat-API is a hand-rolled implementation of the WhatsApp Web (Noise + Curve25519 + protobuf) socket protocol. It is not a Laravel package: there is no ServiceProvider, no facade, no config file, no published migrations. You get a set of WhatsApp\* classes you instantiate yourself. It is also effectively dormant — the repo has not tracked WhatsApp's move to multi-device sessions, and WhatsApp actively bans numbers using unofficial clients. Treat this as a legacy/internal-tooling dependency, not something you ship to a customer's phone number. Before writing a line of code, run composer require in a throwaway directory and confirm it resolves against your PHP version; if it doesn't, stop here and use the official WhatsApp Business Cloud API instead.
Environment checklist. The install fails quietly without the right extensions. Verify with php -m:
sockets, openssl, curl, mbstring — always requiredgmp or bcmath — required for the crypto handshakemcrypt on old PHP 5.x/7.0 tags (removed in PHP 7.2 — pin a tag that uses openssl/phpseclib instead)protobuf-php or the extension), depending on tagWhere to look first. In this order: README.md (it doubles as the API reference), examples/ (example.php, registration.php, group.php — these are the closest thing to documentation), composer.json for the real autoload namespace and extension requirements, then src/Registration.php and src/Auth.php (or src/WhatsProt.php on older tags) for the connect/login surface. src/Events/ tells you which hooks exist, and src/Constants.php holds every endpoint and node name you'll need when debugging raw traffic.
First use case — register one test number, then read one message. Do the registration as a one-off artisan tinker or a throwaway command, then persist the password:
use WhatsApp\Registration;
use WhatsApp\Auth;
// 1. One-time registration (interactive, never in a loop)
$reg = new Registration();
$reg->requestCode($phone); // SMS arrives
$reg->registerCode($phone, $smsCode, $password); // store $password in .env
// 2. Connect and receive
$auth = new Auth($phone, config('whatsapp.nickname'));
$auth->setPassword(config('whatsapp.password'));
$auth->loginWithPassword();
$auth->eventManager()->addEventListener('onGetMessage', function ($node) {
// The payload is a ProtocolNode; attributes vary by tag — dump it once and read from there.
logger()->info('inbound', ['from' => $node->getAttribute('from')]);
});
while (true) {
$auth->pollMessage();
}
Getting that loop to print one inbound message is the whole integration. Everything after that is plumbing.
Bind it as a singleton, never per-request. The socket and session keys are stateful and per-number; only one process may hold them. Register a wrapper in a service provider and inject the wrapper, not the library:
$this->app->singleton(WhatsAppGateway::class, fn ($app) => new WhatsAppGateway(
$app['config']->get('whatsapp')
));
Run the socket as a supervised daemon, not a controller action. The canonical shape is php artisan whatsapp:listen managed by Supervisor (autorestart=true, numprocs=1) with a Redis or database inbox for outbound work. PHP-FPM requests must never call connect()/pollMessage() — the request dies, the session is orphaned, and you'll see conflict/not-authorized handshakes on reconnect.
Bridge library events to Laravel events, then get out of the callback fast. The event listener runs inside the poll loop; anything slow (HTTP, mail, DB writes across a slow connection) stalls heartbeats and causes disconnects. Pattern:
$auth->eventManager()->addEventListener('onGetMessage', function ($node) {
InboundWhatsAppMessage::dispatch(
jid: $node->getAttribute('from'),
body: $node->getAttribute('body'),
);
});
Then a queued listener handles the real work and can reply by pushing onto the outbound inbox.
Outbound sending goes through the owning process. Because only the daemon holds the socket, sendMessage() cannot be called from a queue worker. The daemon polls its inbox (Redis list / jobs-style table) between pollMessage() calls, sends, and records the message ID. Model this as OutboundMessage rows with a status column so retries and observability are free.
Store media outside the library. Inbound media arrives as descriptors pointing at mmg URLs that expire; download in the event handler or a job immediately and push to Storage::disk('s3'), keeping only the path, mime type, and size in your DB. Never treat the library's on-disk message store as your system of record.
Heartbeat and reconnect explicitly. Wrap the loop: catch socket exceptions, sleep() with exponential backoff, re-loginWithPassword(), and send a keepalive/ping on a timer. Log every reconnect with the reason — repeated 401/handshake failures are the signal that the protocol moved and the library is dead, not something you can retry through.
Wrap the library behind your own interface. Define WhatsAppGateway with send(), onMessage(), status(). Your domain code then has zero references to WhatsApp\*, which makes it (a) unit-testable with a fake, and (b) trivially swappable to the Cloud API when this breaks. This is the single highest-value decision in the whole integration.
PHP/Laravel version friction is the first wall. Old tags trigger deprecations (each(), dynamic properties, mcrypt) that are fatal on PHP 8+, while modern Laravel requires PHP 8+. Pin the newest tag you can and check composer.json before assuming it installs.
Credentials are the account. The password and generated keys let anyone impersonate the number. Keep them out of git, chmod the data directory to 600, back it up deliberately, and never let a deploy wipe it — losing it means re-registration, which invalidates the old session and can trigger a ban.
Re-registering is destructive. Calling requestCode() again for the same number invalidates the prior session. There is no "just reconnect with the old keys" path once you've done it. Test on a sacrificial SIM, SMS not voice where possible.
Ban risk scales with volume. New numbers, bulk sends, identical messages, and group blasts get the number banned within hours. If you must use this, warm the number slowly, add jittered delays, and keep a human-approval gate for anything outbound.
The poll loop leaks. Long-running PHP accumulates memory in the library's message store and caches. Call gc_collect_cycles(), DB::disconnect(), and Log::close() periodically; restart the daemon nightly. setDebug(true) dumps full XML per node — enable it only while diagnosing, and point the logger at a rotating channel.
Normalise identifiers yourself. Phone numbers need country code without +; users are <number>@c.us, groups <id>@g.us, and some tags also expose @s.whatsapp.net. Write one toJid() helper and use it everywhere — mixing formats is the most common silent failure (message sent, nothing delivered).
Time skew breaks the handshake. Set the server's timezone/clock correctly (setTimeZone() where available); a drifting container clock produces handshake failures that look like protocol changes.
Debugging order: (1) setDebug(true) and read the raw node stream, (2) check composer show resolved tag against the repo's issue tracker for "401"/"handshake failed", (3) confirm extensions and PHP version, (4) confirm no second process holds the session. If the failure is a 401 right after the web-socket upgrade, the protocol has changed and no amount of retrying will fix it.
Extension points are narrow but real. eventManager()->addEventListener() for additional node types, subclassing WhatsProt/Auth to override send/receive behaviour, and src/Constants.php for endpoint tweaks. Don't fork the protocol code; you will not be able to keep up with WhatsApp's changes.
Have the exit plan agreed before you build. Abstract the gateway (above), keep inbound/outbound records in your schema, and confirm the official Cloud API (or Twilio) can carry the same payloads. That way "the library stopped working" is a one-day adapter swap instead of a rewrite.
How can I help you explore Laravel packages today?