vibrantcoder / whatsapp-notifier
Reusable Laravel package for sending WhatsApp notifications via multiple drivers (Twilio, Meta Cloud API).
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.5
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/events: ^10.0|^11.0|^12.0|^13.0
- illuminate/queue: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
README
Reusable Laravel package for sending WhatsApp messages through multiple drivers (Twilio, Meta Cloud API) with dynamic templates, queues, events, and DB logging. Works with Laravel 10, 11, 12, 13.
Features
- Driver pattern —
meta,twilio, or your own custom driver. - Fluent template builder —
metaTemplate()for any Meta template (text body, image/video/document headers, URL/quick-reply/copy-code buttons, location, named placeholders). - Template registry — declare each template once in config, reference by alias from app code.
- Queue support — fire-and-forget async delivery via Laravel queues.
- Events —
WhatsAppMessageSending,WhatsAppMessageSent,WhatsAppMessageFailed. - DB logging — every send attempt persisted to
whatsapp_messages(success or failure, with full Meta response). - Service provider + facade — auto-discovered, no manual registration.
Requirements
- PHP 8.1+
- Laravel 10, 11, 12, or 13
- Composer 2.x
Installation
There are three ways to install — pick the one that matches where the package lives.
Option 1 — Local path repository (during development)
Use this when the package source lives inside or next to your Laravel app
(e.g. packages/vibrantcoders/whatsapp-notifier).
In your Laravel app's composer.json:
{
"require": {
"vibrantcoder/whatsapp-notifier": "@dev"
},
"repositories": [
{
"type": "path",
"url": "../packages/vibrantcoders/whatsapp-notifier",
"options": { "symlink": true }
}
]
}
The
@devconstraint is required because path repos publish asdev-main.symlink: truemeans edits inside the package are picked up immediately.
composer update vibrantcoder/whatsapp-notifier
Option 2 — Private Git repository
{
"require": {
"vibrantcoder/whatsapp-notifier": "^1.0"
},
"repositories": [
{
"type": "vcs",
"url": "git@github.com:vibrantcoder/whatsapp-notifier.git"
}
]
}
composer require vibrantcoder/whatsapp-notifier
Option 3 — Public Packagist (once published)
composer require vibrantcoder/whatsapp-notifier
Setup (after install)
One command does everything — publishes config, migrations, env keys, and runs migrate:
php artisan whatsapp:install
Service provider and facade are auto-discovered — no manual registration in
config/app.php is required.
For the manual breakdown, see INSTALL.md.
Environment variables
Add the relevant block to your .env:
# Default driver: meta | twilio | <custom> WHATSAPP_DRIVER=meta # Meta Cloud API META_WHATSAPP_PHONE_ID=1001922139681184 META_WHATSAPP_TOKEN=EAAxxx... META_WHATSAPP_VERSION=v25.0 # Twilio (only if using the twilio driver) # TWILIO_SID=ACxxxx # TWILIO_AUTH_TOKEN=xxxx # TWILIO_WHATSAPP_FROM=whatsapp:+14155238886 # Queue WHATSAPP_QUEUE=true WHATSAPP_QUEUE_NAME=whatsapp # Logging WHATSAPP_LOG=true WHATSAPP_LOG_DB=true
⚠️ The Meta temporary access token expires every 24 hours. For production, generate a System User token from Meta Business Settings (60-day or never-expiring).
Usage
1. Inline free-form message (Meta or Twilio)
Free-form messages are only deliverable in the 24-hour customer-care window on Meta. Use templates outside that window.
use VibrantCoders\WhatsAppNotifier\Facades\WhatsApp; WhatsApp::message('Hello {{name}}, this is a test.') ->with(['name' => 'Ali']) ->send('919727927400');
2. Meta template via the registry (recommended)
Declare each template once in config/whatsapp.php:
'meta_templates' => [ 'order_confirmed' => [ 'name' => 'jaspers_market_order_confirmation_v1', 'language' => 'en_US', 'body' => ['customer_name', 'order_id', 'expected_delivery'], ], ],
Then in your code:
WhatsApp::metaTemplate('order_confirmed', [ 'customer_name' => $order->customer_name, 'order_id' => (string) $order->id, 'expected_delivery' => $order->expectedDeliveryFormatted(), ])->send($order->customer_phone);
3. Meta template via the fluent builder
Use this when the template has dynamic header/buttons that vary per call, or when you don't want to maintain the registry.
WhatsApp::metaTemplate('jaspers_market_order_confirmation_v1', 'en_US') ->body('Ali', '1042', '4 May 2026') ->send('919727927400'); // Image header + URL button WhatsApp::metaTemplate('order_shipped_v2') ->headerImage('https://cdn.example.com/box.jpg') ->body('Ali', '1042') ->urlButton(0, '1042') ->send($phone); // PDF document header WhatsApp::metaTemplate('invoice_ready') ->headerDocument('https://cdn.example.com/inv-1042.pdf', 'invoice-1042.pdf') ->body('Ali', '1042', 'INR 1,499.00') ->send($phone); // Quick-reply buttons WhatsApp::metaTemplate('feedback_request') ->body('Ali') ->quickReplyPayload(0, 'YES') ->quickReplyPayload(1, 'NO') ->send($phone); // Named placeholders ({{customer_name}} style) WhatsApp::metaTemplate('order_confirmed_named') ->bodyNamed(['customer_name' => 'Ali', 'order_id' => '1042']) ->send($phone); // Escape hatch — supply the full components array WhatsApp::metaTemplate('any_template')->withRawComponents([...])->send($phone);
4. Override driver per call
WhatsApp::via('twilio')->message('Hi!')->send('+923001234567'); WhatsApp::via('meta')->metaTemplate('order_confirmed', [...])->send($phone);
5. Force sync or queue
WhatsApp::message('OTP: 123456')->queue(false)->send($phone); // sync WhatsApp::message('Receipt')->queue(true)->send($phone); // async
6. Event-driven delivery (recommended for app code)
// app/Events/OrderPlaced.php class OrderPlaced { use Dispatchable, SerializesModels; public function __construct(public Order $order) {} } // app/Listeners/SendOrderConfirmationWhatsApp.php class SendOrderConfirmationWhatsApp { public function handle(OrderPlaced $event): void { WhatsApp::metaTemplate('order_confirmed', [ 'customer_name' => $event->order->customer_name, 'order_id' => (string) $event->order->id, 'expected_delivery' => $event->order->expectedDeliveryFormatted(), ])->send($event->order->customer_phone); } } // Anywhere in your code: event(new OrderPlaced($order));
Laravel auto-discovers the listener from the typehint — no EventServiceProvider
mapping required (Laravel 11+).
Supported template features
| Feature | Builder method | Registry key |
|---|---|---|
Body {{1}} {{2}} |
body(...$values) |
'body' => [...] |
Named body {{name}} |
bodyNamed(['k' => 'v']) |
(use builder) |
| Header text | headerText('...') |
'header' => 'text' |
| Header image | headerImage($url) |
'header' => 'image' |
| Header video | headerVideo($url) |
'header' => 'video' |
| Header document | headerDocument($url, $filename) |
'header' => 'document' |
| Header location | headerLocation($lat, $lng, $name, $addr) |
(use builder) |
| URL button | urlButton($index, $value) |
'buttons' => [['type' => 'url']] |
| Quick-reply button | quickReplyPayload($index, $payload) |
'buttons' => [['type' => 'quick_reply']] |
| Copy-code button | copyCodeButton($index, $code) |
'buttons' => [['type' => 'copy_code']] |
| Raw passthrough | withRawComponents([...]) |
— |
Custom drivers
Register in any service provider's boot():
use VibrantCoders\WhatsAppNotifier\Contracts\WhatsAppDriver; use VibrantCoders\WhatsAppNotifier\Services\WhatsAppManager; app(WhatsAppManager::class)->extend('myprovider', function ($app, $config) { return new class implements WhatsAppDriver { public function name(): string { return 'myprovider'; } public function send(string $to, string $message, array $options = []): array { // your provider call here... return ['success' => true, 'id' => '...', 'response' => [...], 'error' => null]; } }; });
Then WhatsApp::via('myprovider')->message(...)->send(...).
Logged messages
Every attempt — sync or queued, success or failure — is persisted:
use VibrantCoders\WhatsAppNotifier\Models\WhatsAppMessage; WhatsAppMessage::orderByDesc('id')->limit(50)->get();
Columns:
| Column | Type |
|---|---|
id |
bigint |
driver |
varchar (32) |
to |
varchar (32) |
body |
text |
status |
pending|sent|failed |
provider_id |
varchar |
error |
text (nullable) |
response |
json (nullable) |
sent_at |
timestamp |
created_at |
timestamp |
updated_at |
timestamp |
Quick test (5 minutes, no real templates needed)
# 1. Install + setup composer update vibrantcoder/whatsapp-notifier php artisan vendor:publish --tag=whatsapp-config php artisan vendor:publish --tag=whatsapp-migrations php artisan migrate # 2. Add a tinker test php artisan tinker >>> use VibrantCoders\WhatsAppNotifier\Facades\WhatsApp; >>> WhatsApp::metaTemplate('order_confirmed', [ ... 'customer_name' => 'Ali', ... 'order_id' => '999', ... 'expected_delivery' => '4 May 2026', ... ])->send('919727927400');
You should receive a real WhatsApp message and see a row in the
whatsapp_messages table with status = sent and a provider_id returned
by Meta.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
(#132001) Template name does not exist in the translation |
Template not approved, wrong name, or wrong language code. Check WhatsApp Manager. |
(#131030) Recipient phone number not in allowed list |
You're using Meta's free test number — add the recipient under "Send messages to" in API Setup. |
Invalid OAuth access token |
Temp token expired (24h). Generate a new one in API Setup, paste into .env, run php artisan config:clear. |
Meta template alias [...] is not registered |
Add the alias to meta_templates in config/whatsapp.php, or use builder mode. |
| Sends silently fail outside 24h window | Meta only allows template messages outside the customer-care window. Use metaTemplate(), not message(). |
| Queue jobs not running | Make sure a worker is running: php artisan queue:work --queue=whatsapp |
Roadmap
- Webhook receiver to update
whatsapp_messagesstatus from Meta callbacks (delivered/read/failed) - Inbound message handling (conversations + agents)
- Carousel + authentication (OTP) template helpers
- Rate limiting per Meta tier (1k / 10k / 100k / unlimited messages-per-day)
License
MIT © VibrantCoders