WhatsApp Webhook PHP is a lightweight Composer package that parses Meta’s WhatsApp Cloud API webhook payloads into typed PHP objects. Instead of manually decoding nested JSON arrays and checking for keys, you install one package and receive strongly-typed notification objects with dedicated methods for every message type.
Moreover, the package includes HMAC-SHA256 signature verification, Meta’s verification challenge handler, and an event listener system — all with zero external dependencies. As a result, it adds no bloat to your project.
Furthermore, the package is open source under the MIT license and available on both GitHub and Packagist.
What Is WhatsApp Webhook PHP?
WhatsApp Webhook PHP is a standalone webhook parser for the WhatsApp Business Cloud API. Specifically, it converts raw webhook JSON payloads into two types of readonly notification objects: MessageNotification for incoming messages and StatusNotification for delivery status updates.
In addition, the package supports all 13 message types the Cloud API sends: text, image, video, audio, document, sticker, location, contacts, button reply, interactive (button and list), reaction, order, and system messages. Consequently, you can handle every webhook event without writing custom JSON parsing logic.
Why a Separate Webhook Package?
Most WhatsApp PHP libraries bundle webhook handling inside a full SDK that also sends messages. As a result, you end up pulling in HTTP clients like Guzzle and other dependencies just to receive webhooks.
This package takes a different approach. It does one thing well: parse incoming webhook payloads. Therefore, it has zero external dependencies — only PHP’s built-in ext-json. If you also need to send messages, pair it with renzojohnson/whatsapp-cloud-api.
Requirements
Before installing, make sure your environment meets these requirements:
- PHP 8.4 or higher — the package uses readonly classes and backed enums.
- ext-json — for decoding webhook payloads.
No other extensions or libraries are required. In other words, this is a true zero-dependency package.
Installation
Install the package via Composer:
composer require renzojohnson/whatsapp-webhook
After that, Composer autoloads the classes automatically. No manual setup or configuration is required.
Quick Start
The following example sets up a complete webhook endpoint in a few lines:
use RenzoJohnson\WhatsAppWebhook\WebhookHandler;
use RenzoJohnson\WhatsAppWebhook\MessageType;
use RenzoJohnson\WhatsAppWebhook\Notification\MessageNotification;
$handler = new WebhookHandler('your-app-secret');
$handler->onMessage(MessageType::Text, function (MessageNotification $msg): void {
echo "Message from {$msg->from}: {$msg->text()}";
});
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if (!$handler->verifySignature($rawBody, $signature)) {
http_response_code(401);
exit;
}
$handler->handle($rawBody);
http_response_code(200);
Specifically, you create a WebhookHandler with your Meta App Secret, register message listeners, verify the HMAC signature, and call handle(). As a result, the library parses the payload, creates typed notification objects, and dispatches them to your listeners.
Setting Up the Webhook Endpoint
Meta requires two things from your webhook URL: a GET handler for verification and a POST handler for notifications. Here is a complete endpoint:
use RenzoJohnson\WhatsAppWebhook\WebhookHandler;
use RenzoJohnson\WhatsAppWebhook\WebhookException;
$handler = new WebhookHandler('your-app-secret');
// Step 1: Handle Meta's verification challenge (GET)
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$challenge = $handler->handleVerification($_GET, 'your-verify-token');
if ($challenge !== null) {
echo $challenge;
exit;
}
http_response_code(403);
exit;
}
// Step 2: Verify signature (POST)
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if (!$handler->verifySignature($rawBody, $signature)) {
http_response_code(401);
exit;
}
// Step 3: Parse and dispatch
try {
$handler->handle($rawBody);
http_response_code(200);
} catch (WebhookException $e) {
http_response_code(400);
}
HMAC Signature Verification
Meta signs every webhook payload with your App Secret using HMAC-SHA256. The signature arrives in the X-Hub-Signature-256 header. The package verifies this signature with a single method call:
$isValid = $handler->verifySignature($rawBody, $signatureHeader);
Internally, the package uses hash_equals() for timing-safe comparison. Consequently, this prevents timing attacks that could allow an attacker to forge valid signatures.
Supported Message Types
The package supports all message types that the WhatsApp Cloud API sends via webhooks:
| Type | Enum Value | Description |
|---|---|---|
| Text | MessageType::Text |
Plain text messages |
| Image | MessageType::Image |
Images with optional caption |
| Video | MessageType::Video |
Videos with optional caption |
| Audio | MessageType::Audio |
Audio messages and voice notes |
| Document | MessageType::Document |
Documents with filename |
| Sticker | MessageType::Sticker |
Stickers |
| Location | MessageType::Location |
GPS coordinates with name and address |
| Contacts | MessageType::Contacts |
Contact cards |
| Button | MessageType::Button |
Quick reply button taps |
| Interactive | MessageType::Interactive |
Button replies and list selections |
| Reaction | MessageType::Reaction |
Emoji reactions to messages |
| Order | MessageType::Order |
Product orders from catalogs |
Listening by Message Type
Register listeners for specific message types using the onMessage() method. Each listener receives a typed MessageNotification object:
use RenzoJohnson\WhatsAppWebhook\MessageType;
use RenzoJohnson\WhatsAppWebhook\Notification\MessageNotification;
// Text messages
$handler->onMessage(MessageType::Text, function (MessageNotification $msg): void {
echo $msg->text(); // "Hello World"
});
// Images
$handler->onMessage(MessageType::Image, function (MessageNotification $msg): void {
echo "Image ID: {$msg->mediaId()}";
echo "MIME: {$msg->mediaMimeType()}";
echo "Caption: {$msg->caption()}";
});
// Locations
$handler->onMessage(MessageType::Location, function (MessageNotification $msg): void {
echo "Lat: {$msg->latitude()}, Lng: {$msg->longitude()}";
echo "Name: {$msg->locationName()}";
});
// Interactive button replies
$handler->onMessage(MessageType::Interactive, function (MessageNotification $msg): void {
if ($msg->interactiveType() === 'button_reply') {
echo "Button: {$msg->interactiveButtonTitle()}";
}
if ($msg->interactiveType() === 'list_reply') {
echo "Selected: {$msg->interactiveListTitle()}";
}
});
// Reactions
$handler->onMessage(MessageType::Reaction, function (MessageNotification $msg): void {
echo "Reacted with {$msg->reactionEmoji()} to {$msg->reactionMessageId()}";
});
Additionally, use onAnyMessage() to listen to all message types at once:
$handler->onAnyMessage(function (MessageNotification $msg): void {
error_log($msg->getSummary());
});
Status Updates
WhatsApp sends status notifications when your outbound messages are sent, delivered, read, or fail. The package parses these into StatusNotification objects:
use RenzoJohnson\WhatsAppWebhook\StatusType;
use RenzoJohnson\WhatsAppWebhook\Notification\StatusNotification;
$handler->onStatus(StatusType::Delivered, function (StatusNotification $status): void {
echo "Delivered to {$status->recipientId}";
});
$handler->onStatus(StatusType::Read, function (StatusNotification $status): void {
echo "Read by {$status->recipientId}";
});
$handler->onStatus(StatusType::Failed, function (StatusNotification $status): void {
if ($status->hasErrors()) {
echo "Error {$status->errorCode()}: {$status->errorTitle()}";
}
});
Furthermore, the StatusType enum provides convenience methods that reflect WhatsApp’s delivery hierarchy. For example, if a message is read, it was also delivered and sent:
$status->isRead(); // true
$status->isDelivered(); // true (implied by read)
$status->isSent(); // true (implied by delivered)
Pricing and Conversation Tracking
WhatsApp includes pricing information in status webhooks. The package exposes this data through dedicated methods:
$handler->onAnyStatus(function (StatusNotification $status): void {
if ($status->isBillable()) {
echo "Category: {$status->pricingCategory()}"; // "marketing", "utility", etc.
echo "Model: {$status->pricingModel()}"; // "CBP"
}
if ($status->conversationId()) {
echo "Conversation: {$status->conversationId()}";
echo "Origin: {$status->conversationOrigin()}"; // "user_initiated", "business_initiated"
}
});
Forwarded Message Detection
WhatsApp marks messages that have been forwarded. The package exposes this metadata:
$handler->onAnyMessage(function (MessageNotification $msg): void {
if ($msg->isForwarded()) {
echo "This message was forwarded";
}
if ($msg->isFrequentlyForwarded()) {
echo "This message has been forwarded many times";
}
// Reply context
if ($msg->contextMessageId()) {
echo "This is a reply to: {$msg->contextMessageId()}";
}
});
Parse Without Dispatching
If you prefer to handle notifications manually without the listener system, use parse() instead of handle():
$notifications = $handler->parse($rawBody);
foreach ($notifications as $notification) {
if ($notification instanceof MessageNotification) {
// Handle message
echo $notification->getSummary();
}
if ($notification instanceof StatusNotification) {
// Handle status update
echo $notification->getSummary();
}
}
Error Handling
The package throws a WebhookException for invalid payloads:
use RenzoJohnson\WhatsAppWebhook\WebhookException;
try {
$handler->parse($rawBody);
} catch (WebhookException $e) {
// Invalid JSON, wrong object type, or empty entry
error_log($e->getMessage());
}
MessageNotification API Reference
Every incoming message is represented as a readonly MessageNotification object with the following properties and methods:
| Property / Method | Type | Description |
|---|---|---|
$msg->id |
string | WhatsApp message ID (wamid.xxx) |
$msg->from |
string | Sender’s phone number |
$msg->timestamp |
string | Unix timestamp |
$msg->type |
MessageType | Backed enum of the message type |
$msg->contact |
Contact | Sender’s WhatsApp ID and profile name |
$msg->text() |
?string | Text body (text messages) |
$msg->mediaId() |
?string | Media ID (image, video, audio, document, sticker) |
$msg->mediaMimeType() |
?string | MIME type of the media |
$msg->caption() |
?string | Media caption |
$msg->filename() |
?string | Document filename |
$msg->latitude() |
?float | Location latitude |
$msg->longitude() |
?float | Location longitude |
$msg->buttonText() |
?string | Quick reply button text |
$msg->interactiveType() |
?string | “button_reply” or “list_reply” |
$msg->reactionEmoji() |
?string | Reaction emoji |
$msg->isForwarded() |
bool | Whether the message was forwarded |
$msg->getSummary() |
string | One-line human-readable summary |
Related Packages
- renzojohnson/whatsapp-cloud-api — Send WhatsApp messages via Cloud API
- renzojohnson/slack-api — Slack Web API wrapper
- renzojohnson/discord-api — Discord REST API wrapper
- renzojohnson/mailgun-api — Mailgun email API wrapper