ziming / laravel-crisp-whatsapp
This is my package laravel-crisp-whatsapp
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/contracts: ^11.0||^12.0||^13.0
- spatie/laravel-data: ^4.15
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^9.0.0||^10.0.0||^11.0.0
- pestphp/pest: ^3.0||^4.0
- pestphp/pest-plugin-arch: ^3.0||^4.0
- pestphp/pest-plugin-laravel: ^3.0||^4.0
- phpstan/extension-installer: ^1.3||^2.0
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
- spatie/laravel-ray: ^1.35
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Send WhatsApp Messages or Notifications with Crisp Chat!
Support me
I highly recommend Crisp Chat if you are looking for a chat support SaaS for your website. As it charges a flat monthly fee instead of charging by per seat. It has a very nice UI, powerful bot builder & healthy plugins ecosystem as well.
Side Note: Looking to integrate with Crisp in non WhatsApp ways? Check out my Laravel Crisp package too!
Installation
You can install the package via composer:
composer require ziming/laravel-crisp-whatsapp
You may publish the config file with:
php artisan vendor:publish --tag="crisp-whatsapp-config"
You may also publish the migration file if you want to log your whatsapp requests with
php artisan vendor:publish --tag="crisp-whatsapp-migrations"```
And set CRISP_WHATSAPP_LOG_REQUESTS in your .env file to true
This is the contents of the published config file:
declare(strict_types=1); return [ 'website_id' => env('CRISP_WEBSITE_ID'), 'base_url' => env('CRISP_BASE_URL', 'https://plugins.crisp.chat/urn:crisp.im:whatsapp:0/wa/api/website/'), 'access_key_id' => env('CRISP_WHATSAPP_ACCESS_KEY_ID'), 'secret_access_key' => env('CRISP_WHATSAPP_SECRET_ACCESS_KEY'), 'from_phone' => env('CRISP_WHATSAPP_FROM_PHONE'), // change it to false when you are ready for production 'test_mode' => env('CRISP_WHATSAPP_TEST_MODE', true), // when test_mode is true, all whatsapp notifications will go to this number 'to_test_phone' => env('CRISP_WHATSAPP_TO_TEST_PHONE'), 'enable_caching' => env('CRISP_WHATSAPP_ENABLE_CACHE', true), // if you want to log whatsapp requests, you will need to publish migration if this is true 'log_requests' => env('CRISP_WHATSAPP_LOG_REQUESTS', false), ];
Quick Usage (More documentation in the future!)
Here are some examples on how you can use it in a laravel notification class.
Quick Example (Recommended as it greatly simplify things as most of the whatsapp template is auto generated for you after fetching)
declare(strict_types=1); use \Illuminate\Http\Client\ConnectionException; use Ziming\LaravelCrispWhatsApp\Enums\ParameterTypeEnum; use Ziming\LaravelCrispWhatsApp\CrispWhatsAppChannel; use Ziming\LaravelCrispWhatsApp\Enums\ParameterTypeEnum; use Ziming\LaravelCrispWhatsApp\CrispWhatsAppMessage; use Ziming\LaravelCrispWhatsApp\Interfaces\CrispWhatsAppNotification; use Ziming\LaravelCrispWhatsApp\CanReceiveCrispWhatsAppNotification; use Ziming\LaravelCrispWhatsApp\Factories\ComponentParameterFactory; use Ziming\LaravelCrispWhatsApp\Factories\ComponentFactory; use Ziming\LaravelCrispWhatsApp\Enums\ButtonSubTypeEnum; use Ziming\LaravelCrispWhatsApp\LaravelCrispWhatsApp; use Ziming\LaravelCrispWhatsApp\Facades\LaravelCrispWhatsApp as LaravelCrispWhatsAppFacade; class OrderShippedNotification extends Notification implements CrispWhatsAppNotification { use Queueable; public function via(CanReceiveCrispWhatsAppNotification $notifiable): array { return [ CrispWhatsAppChannel::class; ]; } /** * @throws ConnectionException */ public function toCrispWhatsApp(CanReceiveCrispWhatsAppNotification $notifiable): CrispWhatsAppMessage { $templateArray = LaravelCrispWhatsAppFacade::getMessageTemplateArray('hello_world'); return CrispWhatsAppMessageFactory::createFromTemplateArray( $templateArray, [ ComponentParameterFactory::text('Crispy Fries'), ], ); } }
Detailed Example (If you want a lot more control over your whatsapp message)
declare(strict_types=1); use \Illuminate\Http\Client\ConnectionException; use Ziming\LaravelCrispWhatsApp\Enums\ParameterTypeEnum; use Ziming\LaravelCrispWhatsApp\CrispWhatsAppChannel; use Ziming\LaravelCrispWhatsApp\Enums\ParameterTypeEnum; use Ziming\LaravelCrispWhatsApp\CrispWhatsAppMessage; use Ziming\LaravelCrispWhatsApp\Interfaces\CrispWhatsAppNotification; use Ziming\LaravelCrispWhatsApp\CanReceiveCrispWhatsAppNotification; use Ziming\LaravelCrispWhatsApp\Factories\ComponentParameterFactory; use Ziming\LaravelCrispWhatsApp\Factories\ComponentFactory; use Ziming\LaravelCrispWhatsApp\Enums\ButtonSubTypeEnum; use Ziming\LaravelCrispWhatsApp\LaravelCrispWhatsApp; use Ziming\LaravelCrispWhatsApp\Facades\LaravelCrispWhatsApp as LaravelCrispWhatsAppFacade; class OrderShippedNotification extends Notification implements CrispWhatsAppNotification { use Queueable; public function via(CanReceiveCrispWhatsAppNotification $notifiable): array { return [ CrispWhatsAppChannel::class; ]; } /** * @throws ConnectionException */ public function toCrispWhatsApp(CanReceiveCrispWhatsAppNotification $notifiable): CrispWhatsAppMessage { // See the source code for more methods on CrispWhatsAppMessage! $crispMessages = [] // Example 1 $crispMessages[] = CrispWhatsAppMessage::make() ->templateLanguage('en') ->toNumber($notifiable->mobile_phone) ->templateName('template-name') ->addTemplateBodyComponent( LaravelCrispWhatsApp::make()->getMessageTemplateBodyText('template-name'), [ ComponentParameterFactory::text('Crisp'), ] ) ->addTemplateFooterComponent( // you may use the facade as well! LaravelCrispWhatsAppFacade::getMessageTemplateFooterText('template-name') ) ->addTemplateButtonComponent('CTA', ButtonSubTypeEnum::Url) ->addTemplateButtonComponent('Not interested anymore'); // Example 2 $crispMessages[] = CrispWhatsAppMessage::make() ->rawMessageTemplate([ 'language' => 'en_US', 'name' => 'hello_world', 'components' => [ [ 'type' => 'HEADER', 'FORMAT' => 'TEXT', 'text' => 'Hello World', ], [ 'type' => 'BODY', 'text' => 'This is a body text', ], [ 'type' => 'FOOTER', 'text' => 'This is a footer text', ], ], ]); // Example 3 $crispMessages[] => CrispWhatsAppMessage::make() ->templateLanguage('en') ->toNumber($notifiable->mobile_phone) ->rawTemplateComponents([ ComponentFactory::headerText('Order #12345'), ComponentFactory::body('{{1}} the Builder', ComponentParameterFactory::text('Crisp')), ComponentFactory::button('Call To Action', ButtonSubTypeEnum::Url) ComponentFactory::button('No', ButtonSubTypeEnum::QuickReply) ComponentFactory::footer('This is the footer of your whatsapp template'), ]); return $crispMessages[random_int(0, 2)]; } }
Then in your model classes that can receive WhatsApp notifications, you can use the CanReceiveWhatsAppNotification trait:
Below is an example:
use Illuminate\Database\Eloquent\Model; use Ziming\LaravelCrispWhatsApp\Interfaces\CanReceiveCrispWhatsAppNotification; class User extends Model implements CanReceiveCrispWhatsAppNotification { public function routeNotificationForCrispWhatsApp(): string { return $this->attributes['mobile_phone']; } }
WhatsApp API callbacks
Publish and run the migrations before enabling the callback handler:
php artisan vendor:publish --tag=crisp-whatsapp-migrations php artisan migrate
The upgrade uses separate migrations for lifecycle columns and callback history. Existing
callback_response_* columns remain populated for compatibility. If you manually applied
the earlier unreleased combined migration, reconcile your migration history before running
these split migrations; they are intended for the original schema.
Your application's callback controller can delegate persistence to the package:
use Illuminate\Http\Request; use Illuminate\Http\Response; use Ziming\LaravelCrispWhatsApp\Actions\RecordCrispWhatsAppCallback; public function __invoke(Request $request, RecordCrispWhatsAppCallback $recordCallback): Response { if ($request->input('request_id') === null) { return response()->noContent(); } $recordCallback($request->json()->all()); return response()->noContent(); }
The action validates the payload, stores its full contents, deduplicates retries with canonical JSON hashing, and reconciles the parent log under a transaction and row lock. Successful reply/session callbacks take precedence over pending callbacks, regardless of delivery order. Failed and unknown callbacks are preserved without advancing lifecycle state. The legacy fields represent the most advanced successful lifecycle callback when available; inspect event history for subsequent errors and other events.
Callbacks arriving before a send log are stored without a parent. Normal Eloquent creation
of the send log reconciles them automatically; duplicate deliveries also retry reconciliation.
For logs inserted outside Eloquent, call $recordCallback->reconcile($requestId) after insertion.
The action does not authenticate requests; retain the application's configured webhook
authentication. It does not create routes or change the Crisp callback URL.
Schedule pruning explicitly for both package models (Laravel's default app model discovery does not include these vendor models):
use Illuminate\Support\Facades\Schedule; use Ziming\LaravelCrispWhatsApp\Models\CrispWhatsAppCallbackEvent; use Ziming\LaravelCrispWhatsApp\Models\CrispWhatsAppMessageLog; Schedule::command('model:prune', ['--model' => [ CrispWhatsAppMessageLog::class, CrispWhatsAppCallbackEvent::class, ]])->daily();
Ordinary logs retain the existing eight-day default. Pending logs and callback events default
to 90 days, configurable using CRISP_WHATSAPP_PENDING_RETENTION_DAYS and
CRISP_WHATSAPP_CALLBACK_RETENTION_DAYS. Existing published config files should add the
corresponding pending_retention_days and callback_retention_days entries to use these
environment variables. Once history is pruned, deduplication against that history is no longer
possible. Pruning a parent keeps its events with a null parent link until their own expiry.
Caching
Caching is enabled by default. You can disable it by setting the CRISP_WHATSAPP_ENABLE_CACHING environment variable to false.
When caching is enabled, the package will cache the message template for an hour. This is to reduce the number of API calls made to Crisp.
This package also uses the spatie/laravel-data package to build the data objects. You may wish to refer its structure caching documentation too.
Why are the classes final?
This is selfish on my part, my hope is that it would incentivise you to make a pull request so that everyone would benefit. I will also get to know more about where my package is lacking especially in the early days of this library.
Testing
Sending whatsapp messages costs money. Hence there are no tests. So if there are issues, just create an issue to let me know or make a PR fix for it :)
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.