A simple, clean Laravel package to connect with WhatsApp via Evolution API. Works with Blade, Livewire, Inertia.js — any Laravel stack.
Disclaimer: This package uses unofficial WhatsApp APIs via Evolution API. WhatsApp/Meta may ban numbers that use unofficial APIs. Use at your own risk and consider the official WhatsApp Cloud API for production business use.
- Send messages: text, image, video, audio, document, location, contact, buttons, list
- Receive messages via webhooks with event-driven architecture
- Full instance management (create, delete, restart, connect, logout)
- Database storage for instances, messages, and webhook logs
- Queue support for async message sending and webhook processing
- Laravel Notifications channel integration
- Multi-instance support for multiple WhatsApp accounts
- Cleanup command for old data retention
- Reusable trait for any service class
- Configurable storage (can run lightweight without database)
- Retry logic on API calls
- PHP 8.1+
- Laravel 10, 11, or 12
- A running Evolution API instance
composer require izzuddinmohsin/laravel-whatsappPublish the config:
php artisan vendor:publish --tag=whatsapp-configPublish and run migrations (optional but recommended):
php artisan vendor:publish --tag=whatsapp-migrations
php artisan migrateAdd to your .env:
EVOLUTION_API_URL=http://localhost:8080
EVOLUTION_API_KEY=your-api-key
EVOLUTION_INSTANCE=your-instance-nameNote: If you don't need database storage, set
WHATSAPP_STORE_MESSAGES=falseandWHATSAPP_STORE_WEBHOOKS=falsein your.env. You can skip the migration publish step entirely.
use IzzuddinMohsin\LaravelWhatsApp\Facades\WhatsApp;
// Text message
WhatsApp::to('60123456789')->text('Hello from Laravel!');
// Image with caption
WhatsApp::to('60123456789')->image('https://example.com/photo.jpg', 'Check this out!');
// Document
WhatsApp::to('60123456789')->document('https://example.com/invoice.pdf', 'invoice.pdf', 'Your invoice');
// Audio
WhatsApp::to('60123456789')->audio('https://example.com/audio.mp3');
// Video
WhatsApp::to('60123456789')->video('https://example.com/video.mp4', 'Watch this');
// Location
WhatsApp::to('60123456789')->location(3.1390, 101.6869, 'KL Tower', 'Kuala Lumpur');
// Contact card
WhatsApp::to('60123456789')->contact('Ali Ahmad', '60198765432', 'Syarikat ABC');// Buttons
WhatsApp::to('60123456789')->buttons(
title: 'Choose an option',
description: 'What would you like to do?',
buttons: [
['id' => 'order', 'text' => 'Track Order'],
['id' => 'support', 'text' => 'Get Support'],
['id' => 'info', 'text' => 'More Info'],
]
);
// List message
WhatsApp::to('60123456789')->list(
title: 'Our Menu',
description: 'Choose from our selections',
buttonText: 'View Menu',
sections: [
[
'title' => 'Drinks',
'rows' => [
['title' => 'Teh Tarik', 'description' => 'RM 3.00', 'rowId' => 'teh_tarik'],
['title' => 'Kopi O', 'description' => 'RM 2.50', 'rowId' => 'kopi_o'],
],
],
]
);// Check connection status
$info = WhatsApp::instanceInfo();
echo $info->status; // 'open', 'close', etc.
echo $info->phoneNumber;
echo $info->isConnected(); // true/false
// Get QR code for pairing
$qrCode = WhatsApp::qrCode(); // base64 image
// Check if number exists on WhatsApp
$exists = WhatsApp::isOnWhatsApp('60123456789'); // true/false
// Logout
WhatsApp::logout();// Create a new instance on Evolution API
WhatsApp::createInstance([
'name' => 'marketing',
'reject_call' => true,
'msg_call' => 'Sorry, we cannot take calls.',
'groups_ignore' => true,
'always_online' => true,
'read_messages' => true,
]);
// Delete instance
WhatsApp::instance('marketing')->deleteInstance();
// Restart instance
WhatsApp::instance('marketing')->restartInstance();
// Fetch all instances from Evolution API
$instances = WhatsApp::fetchInstances();
// Get connected instances from database
$connected = WhatsApp::getConnectedInstances();// Set webhook URL for current instance
WhatsApp::setWebhook('https://your-app.com/whatsapp/webhook');
// Set with specific events
WhatsApp::setWebhook('https://your-app.com/whatsapp/webhook', [
'MESSAGES_UPSERT',
'MESSAGES_UPDATE',
'CONNECTION_UPDATE',
]);
// Get current webhook configuration
$config = WhatsApp::getWebhook();// Get profile picture
$url = WhatsApp::getProfilePicture('60123456789');
// Fetch messages from a chat
$messages = WhatsApp::fetchMessages('60123456789@s.whatsapp.net', 50);
// Update instance settings
WhatsApp::updateSettings([
'rejectCall' => true,
'alwaysOnline' => true,
]);// Switch instance at runtime
WhatsApp::instance('marketing')->to('60123456789')->text('Promo!');
WhatsApp::instance('support')->to('60123456789')->text('Ticket resolved');
// Each call is immutable — won't affect other calls
$marketing = WhatsApp::instance('marketing');
$support = WhatsApp::instance('support');
$marketing->to('60123456789')->text('From marketing');
$support->to('60123456789')->text('From support');The package automatically registers a webhook route at /whatsapp/webhook (configurable).
In your Evolution API instance settings, set the webhook URL to:
https://your-app.com/whatsapp/webhook
Or set it programmatically:
WhatsApp::setWebhook('https://your-app.com/whatsapp/webhook');Create a listener in your app:
// app/Listeners/HandleWhatsAppMessage.php
namespace App\Listeners;
use IzzuddinMohsin\LaravelWhatsApp\Events\WhatsAppMessageReceived;
use IzzuddinMohsin\LaravelWhatsApp\Facades\WhatsApp;
class HandleWhatsAppMessage
{
public function handle(WhatsAppMessageReceived $event): void
{
$message = $event->message;
// Access message properties
echo $message->from; // '60123456789'
echo $message->text; // 'Hello!'
echo $message->type; // 'conversation', 'imageMessage', etc.
echo $message->pushName; // Sender's name
echo $message->isGroup; // true/false
echo $message->isText(); // true/false
echo $message->isMedia(); // true/false
// Auto-reply example
if ($message->isText() && str_contains(strtolower($message->text), 'hello')) {
WhatsApp::to($message->from)->text("Hi {$message->pushName}!");
}
}
}// app/Providers/EventServiceProvider.php (Laravel 10)
// or bootstrap/app.php (Laravel 11+)
use IzzuddinMohsin\LaravelWhatsApp\Events\WhatsAppMessageReceived;
use IzzuddinMohsin\LaravelWhatsApp\Events\WhatsAppMessageStatusUpdated;
use IzzuddinMohsin\LaravelWhatsApp\Events\WhatsAppConnectionStatusChanged;
use IzzuddinMohsin\LaravelWhatsApp\Events\InstanceConnected;
use IzzuddinMohsin\LaravelWhatsApp\Events\InstanceDisconnected;
use IzzuddinMohsin\LaravelWhatsApp\Events\QrCodeUpdated;
// Laravel 11+
Event::listen(WhatsAppMessageReceived::class, HandleWhatsAppMessage::class);
Event::listen(WhatsAppMessageStatusUpdated::class, HandleStatusUpdate::class);
Event::listen(WhatsAppConnectionStatusChanged::class, HandleConnectionChange::class);
Event::listen(InstanceConnected::class, HandleInstanceConnected::class);
Event::listen(InstanceDisconnected::class, HandleInstanceDisconnected::class);
Event::listen(QrCodeUpdated::class, HandleQrCodeUpdated::class);| Event | Description | Payload |
|---|---|---|
WhatsAppMessageReceived |
New message received | $message (IncomingMessage), $rawPayload |
WhatsAppMessageStatusUpdated |
Status changed (sent/delivered/read) | $messageId, $status, $from, $instanceName |
WhatsAppConnectionStatusChanged |
Instance connected/disconnected | $status, $instanceName, $rawPayload |
InstanceConnected |
Instance successfully connected | $instance (WhatsappInstance model) |
InstanceDisconnected |
Instance disconnected | $instance, $reason |
QrCodeUpdated |
New QR code generated | $instance, $qrCodeData |
Enable async message sending and webhook processing:
WHATSAPP_QUEUE_ENABLED=true
WHATSAPP_QUEUE_CONNECTION=redis
WHATSAPP_QUEUE_NAME=whatsappWhen enabled:
- Outgoing messages are dispatched via
SendMessageJob(3 retries, 10s backoff) - Incoming webhooks are processed via
ProcessWebhookJob(3 retries, 30s backoff) - Messages return immediately with
status: 'queued'
Run the queue worker:
php artisan queue:work --queue=whatsappWhen migrations are published and run, the package stores:
Tracks all WhatsApp instances with their connection status, settings, and QR codes.
Logs all incoming and outgoing messages with status tracking (sent_at, delivered_at, read_at).
Logs all webhook events with processing status and error tracking.
For lightweight usage without database:
WHATSAPP_STORE_MESSAGES=false
WHATSAPP_STORE_WEBHOOKS=falseAdd WhatsApp messaging to any class:
use IzzuddinMohsin\LaravelWhatsApp\Concerns\CanSendWhatsappMessage;
class OrderService
{
use CanSendWhatsappMessage;
public function notifyCustomer(Order $order): void
{
$this->sendWhatsappText(
to: $order->customer_phone,
message: "Your order #{$order->id} is ready!",
);
$this->sendWhatsappDocument(
to: $order->customer_phone,
url: $order->invoice_url,
filename: "invoice-{$order->id}.pdf",
caption: 'Here is your invoice',
);
}
public function sendPromo(): void
{
// Send via specific instance
$this->sendWhatsappImage(
to: '60123456789',
url: 'https://example.com/promo.jpg',
caption: 'Special offer!',
instanceName: 'marketing',
);
}
}Available trait methods:
sendWhatsappText($to, $message, $instanceName)sendWhatsappImage($to, $url, $caption, $instanceName)sendWhatsappVideo($to, $url, $caption, $instanceName)sendWhatsappAudio($to, $url, $instanceName)sendWhatsappDocument($to, $url, $filename, $caption, $instanceName)sendWhatsappLocation($to, $lat, $lng, $name, $address, $instanceName)sendWhatsappContact($to, $fullName, $phone, $org, $instanceName)hasWhatsappInstance()— check if any instance is connectedgetConnectedWhatsappInstances()— get all connected instances
Use WhatsApp as a Laravel notification channel:
// app/Notifications/OrderShipped.php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use IzzuddinMohsin\LaravelWhatsApp\Notifications\WhatsAppChannel;
use IzzuddinMohsin\LaravelWhatsApp\Notifications\WhatsAppMessage;
class OrderShipped extends Notification
{
public function via($notifiable): array
{
return [WhatsAppChannel::class];
}
public function toWhatsApp($notifiable): WhatsAppMessage
{
return WhatsAppMessage::create()
->text("Hi {$notifiable->name}! Your order #{$this->order->id} has been shipped!");
}
}// Text
WhatsAppMessage::create()->text('Hello!');
// Image
WhatsAppMessage::create()->image('https://example.com/photo.jpg', 'Caption');
// Video
WhatsAppMessage::create()->video('https://example.com/video.mp4', 'Caption');
// Audio
WhatsAppMessage::create()->audio('https://example.com/audio.mp3');
// Document
WhatsAppMessage::create()->document('https://example.com/file.pdf', 'file.pdf', 'Your document');
// Location
WhatsAppMessage::create()->location(3.1390, 101.6869, 'KL Tower', 'Kuala Lumpur');
// Contact
WhatsAppMessage::create()->contact('Ali Ahmad', '60198765432', 'Syarikat ABC');Add the route to your User model (or any notifiable):
// app/Models/User.php
public function routeNotificationForWhatsapp(): ?string
{
return $this->phone_number; // e.g., '60123456789'
}Send it:
$user->notify(new OrderShipped($order));Clean up old webhooks and messages:
# Use config defaults (30 days webhooks, 90 days messages)
php artisan whatsapp:cleanup
# Custom retention
php artisan whatsapp:cleanup --webhooks-days=7 --messages-days=30
# Preview what would be deleted
php artisan whatsapp:cleanup --dry-runSchedule it in your routes/console.php or app/Console/Kernel.php:
// Laravel 11+
Schedule::command('whatsapp:cleanup')->daily();Full configuration reference:
# Driver
WHATSAPP_DRIVER=evolution
# Evolution API Connection
EVOLUTION_API_URL=http://localhost:8080
EVOLUTION_API_KEY=your-api-key
EVOLUTION_INSTANCE=default
EVOLUTION_API_TIMEOUT=30
EVOLUTION_API_RETRY=3
EVOLUTION_API_RETRY_SLEEP=100
# Webhook
WHATSAPP_WEBHOOK_URL= # Auto-detected from APP_URL if empty
WHATSAPP_WEBHOOK_PATH=/whatsapp/webhook
WHATSAPP_WEBHOOK_SECRET= # Optional webhook secret
WHATSAPP_WEBHOOK_BASE64=true
# Instance Defaults
WHATSAPP_DEFAULT_INSTANCE= # Default instance ID
WHATSAPP_INTEGRATION=WHATSAPP-BAILEYS
WHATSAPP_REJECT_CALL=false
WHATSAPP_MSG_CALL=
WHATSAPP_GROUPS_IGNORE=false
WHATSAPP_ALWAYS_ONLINE=false
WHATSAPP_READ_MESSAGES=false
WHATSAPP_READ_STATUS=false
WHATSAPP_SYNC_FULL_HISTORY=false
# Queue
WHATSAPP_QUEUE_ENABLED=false
WHATSAPP_QUEUE_CONNECTION= # null = default connection
WHATSAPP_QUEUE_NAME=whatsapp
# Storage
WHATSAPP_STORE_MESSAGES=true
WHATSAPP_STORE_WEBHOOKS=true
# Cleanup Retention
WHATSAPP_CLEANUP_WEBHOOKS_DAYS=30
WHATSAPP_CLEANUP_MESSAGES_DAYS=90
# Media
WHATSAPP_MEDIA_DISK=public
WHATSAPP_MEDIA_DIRECTORY=whatsapp-media
WHATSAPP_MEDIA_MAX_SIZE=16384 # 16MB in KB
# Cache
WHATSAPP_CACHE_ENABLED=true
WHATSAPP_CACHE_TTL=60The package provides enums for type-safe usage:
use IzzuddinMohsin\LaravelWhatsApp\Enums\MessageTypeEnum;
use IzzuddinMohsin\LaravelWhatsApp\Enums\MessageStatusEnum;
use IzzuddinMohsin\LaravelWhatsApp\Enums\MessageDirectionEnum;
use IzzuddinMohsin\LaravelWhatsApp\Enums\ConnectionStatusEnum;
// Message types
MessageTypeEnum::TEXT; // 'text'
MessageTypeEnum::IMAGE; // 'image'
MessageTypeEnum::VIDEO; // 'video'
MessageTypeEnum::AUDIO; // 'audio'
MessageTypeEnum::DOCUMENT; // 'document'
MessageTypeEnum::LOCATION; // 'location'
MessageTypeEnum::CONTACT; // 'contact'
// Message statuses
MessageStatusEnum::PENDING; // 'pending'
MessageStatusEnum::SENT; // 'sent'
MessageStatusEnum::DELIVERED; // 'delivered'
MessageStatusEnum::READ; // 'read'
MessageStatusEnum::FAILED; // 'failed'
// Directions
MessageDirectionEnum::INCOMING; // 'incoming'
MessageDirectionEnum::OUTGOING; // 'outgoing'
// Connection status
ConnectionStatusEnum::OPEN; // 'open'
ConnectionStatusEnum::CLOSE; // 'close'
ConnectionStatusEnum::CONNECTING; // 'connecting'Access stored data via Eloquent models:
use IzzuddinMohsin\LaravelWhatsApp\Models\WhatsappInstance;
use IzzuddinMohsin\LaravelWhatsApp\Models\WhatsappMessage;
use IzzuddinMohsin\LaravelWhatsApp\Models\WhatsappWebhook;
// Instances
$instance = WhatsappInstance::where('name', 'default')->first();
$instance->isConnected(); // true/false
$instance->messages; // HasMany relationship
$instance->webhooks; // HasMany relationship
// Messages
$messages = WhatsappMessage::where('phone', '60123456789')
->where('direction', 'incoming')
->latest()
->get();
// Update message status
$message->markAsSent();
$message->markAsDelivered();
$message->markAsRead();
// Webhooks
$pending = WhatsappWebhook::pending()->get();
$failed = WhatsappWebhook::failed()->get();
$messageEvents = WhatsappWebhook::byEvent('messages.upsert')->get();use IzzuddinMohsin\LaravelWhatsApp\Exceptions\WhatsAppException;
try {
WhatsApp::to('60123456789')->text('Hello!');
} catch (WhatsAppException $e) {
Log::error('WhatsApp send failed: ' . $e->getMessage());
}Your App
-> WhatsApp Facade
-> WhatsAppManager (fluent API, queue support, storage)
-> EvolutionApiDriver (HTTP client, retry logic)
-> Evolution API Server -> WhatsApp
Evolution API Server
-> POST /whatsapp/webhook
-> WebhookController (verify secret, store webhook)
-> ProcessWebhookJob (queue or sync)
-> Store messages, update statuses
-> Fire Laravel events
-> Your listeners
This package is built with a driver-based architecture, making it easy to swap or add new WhatsApp providers without changing your application code.
| Driver | Status | Description |
|---|---|---|
evolution |
Available | Evolution API (unofficial, self-hosted) |
meta |
Planned | WhatsApp Cloud API (official, Meta-hosted) |
When the Meta driver is available, switching will be as simple as:
# .env
WHATSAPP_DRIVER=meta
META_WHATSAPP_TOKEN=your-token
META_PHONE_NUMBER_ID=your-phone-id
META_BUSINESS_ID=your-business-id
META_API_VERSION=v21.0Your application code stays the same — WhatsApp::to('...')->text('...') works regardless of which driver is active.
- Meta WhatsApp Cloud API driver
- Template message support (for Meta Cloud API)
- Media upload to local storage with base64 conversion
- Rate limiting for outgoing messages
- Message scheduling (send later)
- Conversation/thread tracking
- Bulk messaging with progress tracking
Contributions for any of these are welcome!
composer testContributions are welcome! Please see CONTRIBUTING.md for details.
If you discover a security vulnerability, please email izzuddinmohsin.work@gmail.com instead of opening an issue.
The MIT License (MIT). Please see License File for more information.