42dx / whatsapp-laravel-sdk
A laravel package that abstracts all the whatsapp api integration into an easy-to-use services and facades
Requires (Dev)
- laravel/pint: ^1.27
- orchestra/testbench: ^9.1
- phpunit/phpunit: ^11.2
This package is auto-updated.
Last update: 2026-06-18 01:24:03 UTC
README
This Laravel package's goal is to abstract and simplify integration with Facebook's Whatsapp Business API through services, models, traits, and message builders, while documenting how to work with it correctly.
Check our package on Packagist.
Project Meta Data
Project Status (v1.0, main branch)
Table of Contents
- Composer Scripts
- Installation
- Configure Whatsapp Webhook
- Using the Package
- Run and Test
- Tooling
- Contributors
- Changelog
- Roadmap
Composer Scripts
We tried to automate as much boring tasks and CLI commands as we could on short composer scripts. Below you will find a general description of what each one does:
composer commit: Runs thecomitzenbinary which opens an interactive CLI to keep commits in a standard that our pipeline can use to generate the package changelog automatically.composer coverage: Run the tests and generates code coverage. If you are contributing, please make sure you do not lower the current coverage. ;)composer start: Starts the sample application through Laravel Sail with the package already loaded.composer connect: Connects to Pinggy service and exposes local port80to the web. It assumes the sample app is running through Sail.composer setup: Prepares the sample application: installs dependencies, creates.envwhen missing, generates the app key, starts Sail, waits for MySQL, and runs migrations.composer stop: Stops the sample application containers.composer test: Run the tests (do not generate coverage report)composer lint: Checks PHP code style with Laravel Pint.
Installation
Requirements
- A Laravel application with Composer package auto-discovery enabled, or manual service provider registration.
- A configured WhatsApp Business Account, phone number ID, business account ID, and API token from Meta.
- The examples use the Laravel 11/12
bootstrap/providers.phpprovider registration style.
Install the package through Composer:
composer require 42dx/whatsapp-laravel-sdk
The package supports Laravel package auto-discovery and registers both service providers through composer.json. If auto-discovery is disabled in your application, register the providers manually in bootstrap/providers.php:
return [ // [...] The42dx\Whatsapp\RouteServiceProvider::class, The42dx\Whatsapp\WhatsappServiceProvider::class, ];
Publish the configuration file before running migrations so you can adjust table names, messageable columns, and the user model:
php artisan vendor:publish --tag=whatsapp-business-api-config
Configure Whatsapp Webhook
The package automatically registers two routes to handle Meta's webhook interactions:
| Method | URI | Controller Method | Purpose |
|---|---|---|---|
| GET | {webhook_route} |
WebhookController@check |
Webhook verification handshake |
| POST | {webhook_route} |
WebhookController@handle |
Receive webhook events |
The default URI is webhook/whatsapp. You can customize it by setting WHATSAPP_WEBHOOK_ROUTE in your .env file:
WHATSAPP_WEBHOOK_ROUTE=custom/webhook/path
Meta Dashboard Setup
- Go to your app on the Meta Developer Dashboard.
- Navigate to WhatsApp → Configuration → Webhook.
- Click Edit and set the Callback URL to
https://your-domain.com/{webhook_route}. - Set the Verify Token to a string of your choice — this must match the
WHATSAPP_WEBHOOK_VERIFYvalue in your.envfile. - Subscribe to the webhook fields you need (at minimum, messages).
Environment Variables
Add the following to your .env file:
WHATSAPP_API_VERSION=v20.0 WHATSAPP_BUSINESS_ID= WHATSAPP_BUSINESS_PHONE_ID= WHATSAPP_BUSINESS_PHONE_NUMBER= WHATSAPP_SERVER_URL=https://graph.facebook.com WHATSAPP_TOKEN= WHATSAPP_WEBHOOK_VERIFY= WHATSAPP_WEBHOOK_ROUTE=webhook/whatsapp WHATSAPP_DEFAULT_TEMPLATE_LANGUAGE=en_US
| Variable | Required | Default | Description |
|---|---|---|---|
WHATSAPP_API_VERSION |
No | v20.0 |
Meta Graph API version to use |
WHATSAPP_BUSINESS_ID |
Yes | — | Your WhatsApp Business Account ID |
WHATSAPP_BUSINESS_PHONE_ID |
Yes | — | The phone number ID from your WhatsApp Business dashboard |
WHATSAPP_BUSINESS_PHONE_NUMBER |
Yes | — | The phone number associated with your Business account |
WHATSAPP_SERVER_URL |
No | https://graph.facebook.com |
Base URL for the WhatsApp API |
WHATSAPP_TOKEN |
Yes | — | Permanent or temporary access token for the API |
WHATSAPP_WEBHOOK_VERIFY |
Yes | — | Verify token for webhook validation (must match Meta dashboard) |
WHATSAPP_WEBHOOK_ROUTE |
No | webhook/whatsapp |
URI path where Meta will send webhook events |
WHATSAPP_DEFAULT_TEMPLATE_LANGUAGE |
No | en_US |
Default language code for message templates |
Important: The
WHATSAPP_WEBHOOK_VERIFYvalue must exactly match the verify token you entered on the Meta dashboard. The package uses it to validate the webhook verification request from Meta.
Using the Package
After installing, adjust the package configuration according to your Meta application, then run the migrations to create the tables and columns the package needs.
Migrations
The package will create a whatsapp_messages table to store the messages (both inbound and outbound) handled by the package. It will also create a unique messageable phone column on your messageable entity (default: phone on the users table).
If you need to customize this, adjust the database.* keys in your /config/whatsapp.php file before running the migrations.
If your application owns its migrations manually, set database.skip_migrations to true in config/whatsapp.php and create equivalent application migrations.
To keep everything nice and tidy in your project, it is recommended (but not necessary) to publish the default migrations. You can do so by running the following command:
php artisan vendor:publish --tag=whatsapp-business-api-migrations
Then run your application migrations:
php artisan migrate
whatsapp_messages Table
This table stores persisted WhatsApp message records — both inbound messages received from contacts and outbound messages sent by your application. Reactions are stored as payload metadata on the referenced message instead of being stored as independent message rows. Below is the schema:
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
bigint (auto-increment) | No | — | Primary key |
whatsapp_message_id |
varchar | No | — | WhatsApp message ID (unique) |
contact_phone_number |
varchar | No | — | Phone number of the contact |
{messageable_id_column} |
foreignId | Yes | — | FK to your messageable table (default: user_id → users.id), with cascade delete |
way |
enum (inbound, outbound) |
No | — | Message direction |
status |
enum (pending, sent, delivered, read, failed, deleted, warning) |
No | pending |
Delivery status |
type |
enum (audio, button, contacts, document, image, interactive, location, reaction, sticker, template, text, unsupported, video) |
No | — | Message type |
text |
text | Yes | — | Text body (for text/button messages) |
payload |
json | Yes | — | Full message data including context, reactions, and template components |
whatsapp_deleted_at |
timestamp | Yes | — | When Meta reported the message as deleted |
created_at |
timestamp | Yes | — | Laravel managed |
updated_at |
timestamp | Yes | — | Laravel managed |
deleted_at |
timestamp | Yes | — | Laravel soft delete timestamp |
delivered_at |
timestamp | Yes | — | When Meta confirmed delivery |
read_at |
timestamp | Yes | — | When the contact read the message |
sent_at |
timestamp | Yes | — | When Meta confirmed the message was sent |
Indexes: whatsapp_message_id (unique), contact_phone_number, way, status, type.
The table name defaults to whatsapp_messages but can be changed via config('whatsapp.database.table_name'). The payload column is automatically cast to a PHP array of payload entries on the WhatsappMessage model:
$message = WhatsappMessage::first(); $firstPayloadItem = $message->payload[0] ?? null;
You can also filter the payload by type using the model's helper methods:
// Get only context payload items for reply messages $replyContext = $message->getPayloadType(ContextType::REPLY); // Get all payload items except reaction data $withoutReactions = $message->getPayloadWithoutType(MessageType::REACTION);
Messageable Migration
The package also adds the configured messageable phone column to your messageable table (default: phone on users) so you can associate WhatsApp messages with your application's users. The migration adds:
| Column | Type | Nullable | Unique | Description |
|---|---|---|---|---|
{messageable_phone_column} |
varchar | Yes | Yes | Contact's WhatsApp phone number (default: phone) |
{messageable_phone_column}_verified_at |
timestamp | Yes | No | Phone verification timestamp (default: phone_verified_at) |
The phone column name is configurable via config('whatsapp.database.messageable_phone_column') before running the migrations. The verification timestamp column is derived from it by appending _verified_at:
// config/whatsapp.php 'database' => [ 'messageable_phone_column' => 'phone', // column name for the phone number 'skip_migrations' => false, // set true to disable package-loaded migrations 'table_name' => 'whatsapp_messages', 'users_table' => 'users', // table to alter 'users_table_pk' => 'id', // primary key of the users table 'messageable_id_column' => 'user_id', // FK column on whatsapp_messages 'user_model' => App\Models\User::class, // model used for lookups ],
Note: If your application already has a
phonecolumn on theuserstable, changemessageable_phone_columnto a different name before running the migrations to avoid conflicts.
Note: If you publish and customize the messageable migration, keep the
down()method aligned with both columns. Only{messageable_phone_column}has a unique index by default.
General Concepts
The package follows the same data structure as the WhatsApp Business API. Understanding a few key concepts will help you work with it effectively:
Message Direction
Every message stored in the whatsapp_messages table has a way column that indicates its direction:
- Inbound — messages received from a WhatsApp contact
- Outbound — messages sent by your application
Message Lifecycle
When Meta processes a message, it sends status updates via the webhook. The schema supports all statuses below, but the webhook handler currently updates timestamps only for sent, delivered, read, and deleted.
Unhandled statuses, such as failed and warning, are valid column values but are logged instead of timestamped by the current handler.
| Status | Timestamp Column | Meaning |
|---|---|---|
pending |
— | Message created but not yet confirmed by Meta |
sent |
sent_at |
Meta confirmed the message was sent |
delivered |
delivered_at |
Meta confirmed delivery to the contact's device |
read |
read_at |
The contact read the message |
deleted |
whatsapp_deleted_at |
Meta reported the message as deleted |
failed |
Not handled | Meta could not deliver the message |
warning |
Not handled | A non-critical issue was flagged |
The payload Column
The payload JSON column stores structured data that varies by message type — context (replies, forwards), reaction data, template components, and button payloads. This allows the package to preserve the full message structure without requiring separate tables for each type.
Sending Messages
To send messages, add the CanSendWhatsappMsg trait to any Eloquent model (typically your User model). The model must expose the configured messageable_phone_column attribute, which defaults to phone, matching the contact's WhatsApp number:
use The42dx\Whatsapp\Enums\{MessageComponent, MessageType}; use The42dx\Whatsapp\Models\Traits\CanSendWhatsappMsg; class User extends Authenticatable { use CanSendWhatsappMsg; }
Then you can send messages through the model:
$phoneColumn = config('whatsapp.database.messageable_phone_column', 'phone'); $user = User::where($phoneColumn, '+1234567890')->first(); // Send a text message $user->sendWhatsappMsg(MessageType::TEXT, 'Hello from the SDK!'); // Send a template message $user->sendWhatsappMsg(MessageType::TEMPLATE, [ 'name' => 'hello_world', 'lang' => 'en_US', ]); // React to a message $user->sendWhatsappMsg(MessageType::REACTION, '👍', $originalMessage);
For template messages, name is required and must match the template name approved in Meta. lang is optional and falls back to WHATSAPP_DEFAULT_TEMPLATE_LANGUAGE.
You can also use the WhatsappService directly via dependency injection:
use The42dx\Whatsapp\Factories\WhatsappApiMessage; use The42dx\Whatsapp\Services\WhatsappService; public function __construct( protected WhatsappService $whatsapp, ) {} public function send(): void { $message = WhatsappApiMessage::compose('+1234567890') ->with(text: 'Hello from the SDK!'); $response = $this->whatsapp->send($message); }
Or resolve it from the container:
$whatsapp = app(WhatsappService::class); $message = WhatsappApiMessage::make('+1234567890') ->withText('Hello from the SDK!'); $response = $whatsapp->send($message, $user);
Message Templates
Template components are passed in the same structure used by WhatsappApiMessage::withComponent(). The CanSendWhatsappMsg trait expects each component to include a type and a parameters array. Use at most one component for each MessageComponent type, matching WhatsApp API expectations.
$user->sendWhatsappMsg(MessageType::TEMPLATE, [ 'name' => 'order_update', 'lang' => 'en_US', 'components' => [ [ 'type' => MessageComponent::BODY, 'parameters' => [ ['name' => 'customer_name', 'text' => 'Rafael'], ['name' => 'order_code', 'text' => 'DX-123'], ], ], [ 'type' => MessageComponent::BUTTON, 'subType' => MessageComponent::COPY_CODE, 'index' => 0, 'parameters' => [ [ 'type' => MessageComponent::COUPON_CODE, 'couponCode' => 'SAVE10', ], ], ], ], ]);
When using the builder directly, call usingTemplate() and add components with withComponent():
$message = WhatsappApiMessage::compose('+1234567890') ->usingTemplate('order_update', 'en_US') ->withComponent(MessageComponent::BODY, [ ['name' => 'customer_name', 'text' => 'Rafael'], ['name' => 'order_code', 'text' => 'DX-123'], ]); $response = app(WhatsappService::class)->send($message, $user);
Available Templates
Use WhatsappService::getMessageTemplates() to retrieve templates available for the configured WhatsApp Business Account:
use The42dx\Whatsapp\Services\WhatsappService; $templates = app(WhatsappService::class)->getMessageTemplates();
The method returns the decoded API response as an array. If Meta returns an error, the package logs it and returns an empty array.
Message Support
Outbound support through CanSendWhatsappMsg:
MessageType |
Supported | Behavior |
|---|---|---|
TEXT |
Yes | Sends text and stores an outbound WhatsappMessage |
TEMPLATE |
Yes | Sends template data and stores the template payload |
REACTION |
Yes | Adds or removes reaction payload on an existing stored message |
AUDIO, BUTTON, CONTACTS, DOCUMENT, IMAGE, INTERACTIVE, LOCATION, STICKER, VIDEO, UNSUPPORTED |
No | Logs a warning and does not call the API |
Inbound support through the webhook:
| Inbound data | Supported | Behavior |
|---|---|---|
| Text messages | Yes | Stores text and base message data |
| Button replies | Yes | Stores button text and payload |
| Reactions | Yes | Adds or removes reaction payload on the referenced stored message without creating a separate row |
| Context and status updates | Yes | Stores reply/forward metadata and delivery timestamps |
| Audio, contacts, document, image, interactive, location, sticker, video | Partial | Stores the generic message row, logs unsupported type, and does not persist type-specific payload fields |
Whatsapp API Entities
When Meta sends a webhook event, the payload follows a nested structure. The package maps this structure to a hierarchy of entity objects, each representing a level of the API response:
EventEntity
└── EntryEntity[]
└── ChangesEntity[]
├── ContactsEntity[] (when field = MESSAGES)
├── MessageEntity[] (when field = MESSAGES)
└── StatusEntity[] (when field = MESSAGES)
Top-level entities:
| Entity | Key Properties | Description |
|---|---|---|
EventEntity |
$object (ObjectType), $entries (Collection) |
Root of every webhook event |
EntryEntity |
$id (string), $changes (Collection) |
Represents a WhatsApp Business Account entry |
ChangesEntity |
$field (ApiEvent), $value (Entity) |
A single change — the $field determines the value type |
Message-related entities (under ChangesEntity when $field is MSGS):
| Entity | Key Properties | Description |
|---|---|---|
MessagesEntity |
$contacts, $messages, $statuses, $waId, $phone |
Container for all message data in a single change |
ContactsEntity |
$name, $waId |
Contact profile info from an inbound message |
MessageEntity |
$id, $from, $type, $text, $timestamp, $context, + type-specific props |
A single inbound or outbound message |
StatusEntity |
$id, $recipientNumber, $status, $timestamp |
A delivery status update |
Message sub-entities (accessed via MessageEntity properties):
| Entity | Accessed Via | Key Properties |
|---|---|---|
ContextEntity |
$message->context |
$id, $from, $type (REPLY, FWD, F_FWD) |
ReactionEntity |
$message->reaction |
$emoji, $messageId |
ButtonEntity |
$message->button |
$text, $payload |
LocationEntity |
$message->location |
$latitude, $longitude, $address, $name |
ContactEntity |
$message->contacts |
$name, $phones, $emails, $addresses, $org |
ImageEntity |
$message->image |
$id, $caption, $mimeType |
VideoEntity |
$message->video |
$id, $caption, $mimeType |
DocumentEntity |
$message->document |
$id, $caption, $filename, $mimeType |
AudioEntity |
$message->audio |
$id, $mimeType |
StickerEntity |
$message->sticker |
$id, $type (STATIC, ANIMATED) |
All entities implement the Entity contract, providing toArray() and toJson() methods for serialization, and dynamic property access via __get().
Meta Webhook Validation
When you configure a webhook in the Meta Dashboard, Meta sends a GET request to verify your endpoint. The package handles this automatically via the WebhookController@check route.
How it works:
- Meta sends a
GETrequest withhub_mode,hub_verify_token, andhub_challengequery parameters - The
WebhookCheckRequestform request validates all three:hub_mode— must besubscribe(enforced by theHubModerule)hub_verify_token— must match yourconfig('whatsapp.webhook_verify')value (enforced by theVerifyTokenrule)hub_challenge— must be a present string
- If validation passes, the controller responds with the
hub_challengevalue — this confirms to Meta that your endpoint is valid
Setup in Meta Dashboard:
- Set
WHATSAPP_WEBHOOK_VERIFYin your.envto a secret string of your choice - In the Meta Dashboard → WhatsApp → Configuration → Webhook, enter your callback URL and the same verify token
- Meta will send the verification request — if the token matches, the webhook is activated
If verification fails, check that the WHATSAPP_WEBHOOK_VERIFY value in .env exactly matches what you entered in the Meta Dashboard.
Receiving Messages
When Meta forwards a webhook event, the WebhookController@handle endpoint processes it automatically. The incoming POST request is validated by ApiEventRequest (requires object to be a valid ObjectType and entry to be a non-empty array), then the payload is parsed into the entity hierarchy.
Event routing:
The controller's hookRouter dispatches each ChangesEntity based on its $field:
ApiEvent field |
Handler | Behavior |
|---|---|---|
MSGS |
handleMessages() |
Processes inbound messages and status updates |
| All other fields | handleDefault() |
Logs a warning and ignores the event |
Message processing (MSGS field):
-
Inbound messages — Each
MessageEntityis processed byhandleMessage():- Creates or updates a
WhatsappMessagemodel withway = INBOUND, the contact's phone number, type, and WhatsApp message ID - Attempts to associate the message with a user by matching
fromagainst the configurablemessageable_phone_column - Dispatches to a type-specific handler:
MessageTypeHandler Behavior TEXThandleText()Stores the text body REACTIONhandleReaction()Adds or removes reaction payload on the referenced stored message without creating a separate row BUTTONhandleButton()Stores the button text/payload AUDIO,CONTACTS,DOCUMENT,IMAGE,INTERACTIVE,LOCATION,STICKER,VIDEO— Stores generic message data and logs an unsupported-type warning - Processes context (reply/forward) via
handleContext(), storing it in thepayloadcolumn - Persists via
updateOrCreateusingwhatsapp_message_id+contact_phone_numberas the unique key
- Creates or updates a
-
Status updates — Each
StatusEntityis processed byhandleStatus():- Looks up the existing
WhatsappMessagebywhatsapp_message_id - Updates the status and sets the corresponding timestamp (
sent_at,delivered_at,read_at,whatsapp_deleted_at) - Logs a warning if the message is not found in the database
- Looks up the existing
Run and Test
We included a sample fresh Laravel app to help those who want to contribute to the package. To start it just go through the following steps:
- Clone this repository.
- Run
composer setupfrom the repository root. It enters the sample app, installs dependencies, creates.envwhen missing, generates the app key, starts Sail, waits for MySQL, runs migrations, and returns to the root folder. - Run
composer startfrom the repository root. It will run the sample app throughlaravel sail. - With the local server running, from another terminal run
composer connectfrom the repository. It will connect to pinggy service and expose your local application to the web. - Adjust your webhook configuration on the Meta/Facebook dashboard with the generated pinggy URI.
Tooling
There are a few tools we provide alongside with the source code to ease a little bit the burden of following a bunch of patterns and standards we set up, as well as automate some boring processes.
Comitizen
This CLI helps with writting commit messages in a meaningful and standardized way, so that our automation process can use them to properly write our software changelog.
If you like the tool, kudo the devs ;)
How to use Comitzen
Just run composer commit from the repository's root folder instead of the traditional git commit and follow the CLI interactive steps :)
Pinggy
This service allows routing external requests to your local environment. You can use it to test your Watsapp webooks locally while developing.
If you like the tool, kudos the devs ;)
How to use Pinggy
Just run composer connect from the repository's root folder. You will need to have our sample application running so you can receive webhook requests locally.
After running the composer command, update your Meta Developer Dashboard. Follow the process on Meta Dashboard Setup section of this README.
Contributors
Kudos to all our dear contributors. Without them, nothing would have been possible ❤️
Rafael Eduardo Paulin 💻 🎨 📖 🤔 🚇 🚧 📆 👀 ⚠️ 🔧 ✅ |
Would you like to see your profile here? Take a look on our Code of Conduct and our Contributing docs, and start coding! We would be thrilled to review a PR of yours! 💯
Changelog
All changes made to this package since the start of development can be found either on our release list or on the changelog.
Roadmap
Any planned enhancement to the package will be described and tracked in our project page