pushinbr / pam-whatsapp-web
Persistent WhatsApp Web client for PHP on PAM, without Node.js or Puppeteer.
Requires
- php: ^8.5
- ext-mbstring: *
- chillerlan/php-qrcode: ^6.0
- pushinbr/pam-browser: ^1.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
README
PAM WhatsApp Web
The whatsapp-web.js experience, now in persistent, pure PHP.
Terminal QR · persistent sessions · real-time events · media · groups · channels · calls
Start here · Features · Compatibility · Safety
A typed API for controlling WhatsApp Web directly from PHP. PAM keeps the
process alive, pam-browser communicates with Chrome/Chromium through the
Chrome DevTools Protocol, and this library provides the familiar
whatsapp-web.js surface — without Node.js, npm, Puppeteer, or Playwright in
production.
Start here
1. Install PAM
curl --proto '=https' --proto-redir '=https' --tlsv1.2 \ --connect-timeout 15 --max-time 60 --max-filesize 1048576 -fsSL \ https://github.com/push-in/pam/releases/latest/download/install.sh | sh pam doctor
2. Create your project and install the library
mkdir my-whatsapp && cd my-whatsapp pam composer init --no-interaction pam composer require pushinbr/pam-whatsapp-web:^1.0
Chrome or Chromium must be installed on the host. No JavaScript runtime is required.
3. Create listen.php
<?php declare(strict_types=1); require __DIR__.'/vendor/autoload.php'; use Pam\WhatsApp\Auth\LocalAuth; use Pam\WhatsApp\Auth\LocalAuthOptions; use Pam\WhatsApp\Client; use Pam\WhatsApp\ClientOptions; use Pam\WhatsApp\Event\MessageReceived; use Pam\WhatsApp\Event\QrCodeReceived; use Pam\WhatsApp\Event\Ready; use Pam\WhatsApp\TerminalQrCode; $client = new Client(new ClientOptions( authStrategy: new LocalAuth(new LocalAuthOptions( clientId: 'main', dataPath: __DIR__.'/.sessions', )), )); $client->onQrCode(static function (QrCodeReceived $event): void { echo "\n\nOpen WhatsApp > Linked devices > Link a device\n\n"; echo TerminalQrCode::render($event->code), "\n"; }); $client->onReady(static function (Ready $event): void { echo "\n\n✓ WhatsApp connected. Waiting for messages...\n"; }); $client->onMessage(static function (MessageReceived $event): void { $message = $event->message; $body = $message->body !== '' ? $message->body : '['.$message->type->name.']'; printf("[%s] %s: %s\n", date('H:i:s', $message->timestamp), $message->from, $body); }); $client->initialize(); $client->run();
4. Run it
pam listen.php
On the first run, a scannable QR code appears in the terminal. After scanning,
the client connects automatically and starts printing inbound messages.
Authentication is stored in .sessions/; subsequent runs restore the session
without another QR code. Never publish this directory or share its contents.
The complete runnable program is available at
examples/listen.php.
What's included
| Area | Features |
|---|---|
| Session | QR, pairing code, LocalAuth, RemoteAuth, reconnection, and session conflict handling |
| Messages | text, replies, editing, deletion, forwarding, reactions, mentions, and read receipts |
| Media | images, audio, video, documents, stickers, thumbnails, and streaming downloads |
| Conversations | contacts, private chats, groups, participants, invites, and membership requests |
| Content | locations, polls, contacts, contact lists, scheduled events, and legacy buttons/lists |
| presence, connection state, calls, channels, labels, business profiles, products, and orders | |
| API | typed objects, integer-backed enums, immutable events, PHPStan level 9, and an automated parity matrix |
Reply to a message
$client->onMessage(static function (MessageReceived $event): void { if ($event->message->body === '!ping') { $event->message->reply('pong 🟢'); } });
Send messages and manage chats
$client->sendMessage('15551234567@c.us', 'Hello directly from PHP!'); $client->sendPresenceAvailable(); $client->archiveChat($chatId); $client->muteChat($chatId, new DateTimeImmutable('+1 hour')); $contact = $client->getNumberId('5511999999999');
For unsaved contacts, pass any human-readable international number. The client normalizes it, verifies that it is registered, resolves its current WhatsApp ID, and sends the message:
use Pam\WhatsApp\RetryOptions; $message = $client->sendMessageToNumber( '+55 (11) 99999-9999', "Hello!\nThis message has two lines.", retry: new RetryOptions(maxAttempts: 3), );
Retries are opt-in because retrying an ambiguous send failure can produce a duplicate. The default is one attempt.
Send media with one call
$client->sendImageToNumber('+55 11 99999-9999', __DIR__.'/photo.jpg', 'Photo caption'); $client->sendAudioToNumber('+55 11 99999-9999', __DIR__.'/voice.ogg'); $client->sendDocumentToNumber('+55 11 99999-9999', __DIR__.'/invoice.pdf'); $client->sendStickerToNumber('+55 11 99999-9999', __DIR__.'/sticker.webp', 'Pack name', 'Author');
Observe delivery
$client->onMessageSent(fn ($event) => printf("Sent: %s\n", $event->message->id)); $client->onMessageDelivered(fn ($event) => printf("Delivered: %s\n", $event->message->id)); $client->onMessageRead(fn ($event) => printf("Read: %s\n", $event->message->id)); $client->onMessageFailed(fn ($event) => printf("Failed: %s\n", $event->message->id));
These convenience events are derived from the typed upstream acknowledgement
event. on(EventType::MessageAcknowledged, ...) remains available unchanged.
Reconnection, diagnostics, and logs
use Pam\WhatsApp\ClientOptions; use Pam\WhatsApp\LogLevel; $client = new Client(new ClientOptions( autoReconnect: true, reconnectMaxAttempts: 5, reconnectDelayMs: 1_000, logger: static function (LogLevel $level, string $message, array $context): void { fwrite(STDERR, sprintf("[%s] %s %s\n", $level->name, $message, json_encode($context))); }, )); $diagnostics = $client->diagnoseSession(); if (!$diagnostics->healthy()) { var_dump($diagnostics); }
Automatic reconnect is disabled by default to preserve existing production
behavior. Logout and QR retry exhaustion are never reconnected automatically.
Client::reconnect() is available after a closed or failed session.
Upstream supported-feature status
Every feature currently marked ✅ in the upstream whatsapp-web.js README is
represented by the typed PHP API: Multi Device, send/receive messages, all
listed media formats, stickers, contact cards, locations, replies, group
administration, mentions, chat mute, contact block, contact/profile operations,
status, reactions, polls, and channels.
Buttons and list messages are deprecated and marked ❌ upstream. Their PHP types remain for source compatibility, but successful delivery is not guaranteed. Communities are marked 🔜 upstream and are not advertised as a completed feature here.
Complete supported-feature cookbook
The examples below mirror every row in the upstream Supported features table.
They assume an initialized, ready $client; IDs such as $chatId, $groupId,
and $messageId must come from your own account. The machine-readable mapping
is supported-features.json.
Import the classes used by the recipes you copy:
use Pam\WhatsApp\Button; use Pam\WhatsApp\Buttons; use Pam\WhatsApp\Chat; use Pam\WhatsApp\ContactList; use Pam\WhatsApp\CreateChannelOptions; use Pam\WhatsApp\GroupChat; use Pam\WhatsApp\GroupMentionSend; use Pam\WhatsApp\ListMessage; use Pam\WhatsApp\Location; use Pam\WhatsApp\LocationSendOptions; use Pam\WhatsApp\MessageSendOptions; use Pam\WhatsApp\Poll; use Pam\WhatsApp\Event\MessageReceived;
Multi Device ✅
Persist the linked-device profile so subsequent starts reconnect without a new QR:
$client = new Client(new ClientOptions( authStrategy: new LocalAuth(new LocalAuthOptions('main', __DIR__.'/.sessions')), )); $client->initialize(); $client->run();
Send messages ✅
$client->sendMessageToNumber('+55 (11) 99999-9999', "Hello!\nSecond line."); $client->sendMessage($chatId, 'Hello using a resolved chat ID.');
Receive messages ✅
$client->onMessage(static function (MessageReceived $event): void { printf("%s: %s\n", $event->message->from, $event->message->body); });
Send images, audio, and documents ✅
$client->sendImageToNumber($phone, __DIR__.'/photo.jpg', 'A photo'); $client->sendAudioToNumber($phone, __DIR__.'/voice.ogg', asVoiceNote: true); $client->sendDocumentToNumber($phone, __DIR__.'/invoice.pdf', 'Invoice');
Send video ✅
Video sending requires Google Chrome, matching the upstream requirement:
$client->sendVideoToNumber($phone, __DIR__.'/demo.mp4', 'Demo video'); $client->sendVideoToNumber($phone, __DIR__.'/animation.mp4', asGif: true);
Send stickers ✅
$client->sendStickerToNumber($phone, __DIR__.'/sticker.webp', 'My pack', 'My app');
Video-to-sticker conversion uses the ffmpegPath configured in ClientOptions.
Receive images, audio, video, and documents ✅
$client->onMessage(static function (MessageReceived $event): void { $message = $event->message; if (!$message->hasMedia) return; $media = $message->downloadMedia(); if ($media !== null) { $binary = base64_decode($media->data, true); if ($binary !== false) { file_put_contents(__DIR__.'/download.bin', $binary); } } });
Use downloadMediaStream() for large files, as shown later in this README.
Send contact cards ✅
$contact = $client->getContactById('5511999999999@c.us'); $client->sendMessage($chatId, new ContactList([$contact]));
Send location ✅
$location = new Location( -23.5505, -46.6333, new LocationSendOptions(name: 'São Paulo', address: 'SP, Brazil'), ); $client->sendMessage($chatId, $location);
Send buttons ❌ deprecated upstream
WhatsApp deprecated this format. The type remains for source compatibility, but delivery is not guaranteed and new applications should not depend on it:
$legacy = new Buttons('Choose an option', [new Button('Continue', 'continue')]); $client->sendMessage($chatId, $legacy); // May be rejected by current WhatsApp builds.
Send lists ❌ deprecated upstream
$legacy = new ListMessage('Choose a product', 'Open list', [[ 'title' => 'Products', 'rows' => [['id' => 'coffee', 'title' => 'Coffee']], ]]); $client->sendMessage($chatId, $legacy); // May be rejected by current WhatsApp builds.
Receive location ✅
$client->onMessage(static function (MessageReceived $event): void { $location = $event->message->location; if ($location !== null) { printf("Coordinates: %f, %f\n", $location->latitude, $location->longitude); } });
Reply to messages ✅
$client->onMessage(static function (MessageReceived $event): void { $event->message->reply('Thanks for your message!'); });
Join groups by invite ✅
Pass only the invite code, not the complete URL:
$groupId = $client->acceptInvite('AbCdEfGhIjKlMnOpQrStUv');
Get a group invite ✅
$group = $client->getChatById($groupId); if ($group instanceof GroupChat) { $inviteCode = $group->getInviteCode(); }
Modify group subject and description ✅
if ($group instanceof GroupChat) { $group->setSubject('Customer community'); $group->setDescription('Support and product announcements'); }
Modify group settings ✅
if ($group instanceof GroupChat) { $group->setMessagesAdminsOnly(true); $group->setInfoAdminsOnly(true); $group->setAddMembersAdminsOnly(true); }
Add group participants ✅
if ($group instanceof GroupChat) { $result = $group->addParticipants(['5511999999999@c.us']); }
Remove group participants ✅
if ($group instanceof GroupChat) { $result = $group->removeParticipants(['5511999999999@c.us']); }
Promote and demote group participants ✅
if ($group instanceof GroupChat) { $group->promoteParticipants(['5511999999999@c.us']); $group->demoteParticipants(['5511999999999@c.us']); }
Mention users ✅
$client->sendMessage( $chatId, 'Hello @5511999999999', new MessageSendOptions(mentions: ['5511999999999@c.us']), );
Mention groups ✅
$client->sendMessage( $chatId, 'See @Support', new MessageSendOptions(groupMentions: [ new GroupMentionSend('Support', '120363000000000000@g.us'), ]), );
Mute and unmute chats ✅
$chat = $client->getChatById($chatId); if ($chat instanceof Chat) { $chat->mute(new DateTimeImmutable('+1 hour')); $chat->unmute(); }
Block and unblock contacts ✅
$contact = $client->getContactById('5511999999999@c.us'); $contact->block(); $contact->unblock();
Get contact information ✅
$contact = $client->getContactById('5511999999999@c.us'); printf("%s (%s)\n", $contact->name, $contact->id->serialized);
Get profile pictures ✅
$url = $client->getProfilePicUrl('5511999999999@c.us');
Set the user status message ✅
$client->setStatus('Available — powered by PAM');
React to messages ✅
$message = $client->getMessageById($messageId); $message?->react('👍');
Create polls ✅
$poll = new Poll('Where should we have lunch?', ['Pizza', 'Sushi', 'Salad']); $client->sendMessage($chatId, $poll);
Channels ✅
$channels = $client->getChannels(); $created = $client->createChannel('Product news', new CreateChannelOptions( description: 'Release announcements', )); if (isset($channels[0])) { $channels[0]->sendMessage('A new version is available!'); }
Vote in polls ✅
$pollMessage = $client->getMessageById($messageId); $pollMessage?->vote(['Pizza']); // Select one or more options by name.
Communities 🔜 planned upstream
The upstream library does not yet advertise Communities as supported, so this package intentionally does not claim a complete Communities API. You can inspect the experimental WhatsApp Web feature flag without treating it as support:
$availableInThisWebBuild = $client->interface?->checkFeatureStatus('communities') ?? false;
This flag does not provide community creation or administration guarantees.
Download large media without exhausting memory
use Pam\WhatsApp\MediaStreamOptions; $media = $message->downloadMediaStream(new MediaStreamOptions( chunkSize: 1024 * 1024, )); if ($media !== null) { foreach ($media->stream as $chunk) { // Persist or forward each binary chunk. } }
Compatibility
The current release tracks whatsapp-web.js 1.34.7 at reference commit
942d236a11ad68807308b058303ba5256915979c. Coverage is auditable in
api-matrix.json: 81 symbols + 670 members, 751/751 strict
contracts.
pam composer parity:gate
pam composer test
pam composer analyse
Live certification is split between an unauthenticated QR smoke test and an
authenticated suite with explicit mutation guards. See
CERTIFICATION.md.
Architecture
your PHP code
│
▼
PAM WhatsApp Web ── typed events and objects
│
▼
PAM Browser ─────── Chrome DevTools Protocol
│
▼
Chrome / Chromium ─ WhatsApp Web
Responsible use
This is an unofficial, community-maintained library. WhatsApp may change its internal modules without notice and may restrict or block accounts that violate its terms. Avoid spam and abusive automation. For officially supported integrations and critical workloads, use Meta's WhatsApp Business Platform.
License
Open source under the Apache License 2.0. You may use, modify, and distribute it, including commercially.