renzojohnson / whatsapp-webhook
Lightweight WhatsApp Cloud API webhook parser. Typed notifications for messages, statuses, and errors. HMAC signature verification. Zero dependencies.
v0.26.03.16
2026-03-17 02:43 UTC
Requires
- php: ^8.4
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^11.0 || ^12.0
This package is auto-updated.
Last update: 2026-03-17 02:45:07 UTC
README
Lightweight WhatsApp Cloud API webhook parser. Typed notifications for messages, statuses, and errors. HMAC signature verification. Zero dependencies.
Author: Renzo Johnson
Requirements
- PHP 8.4 or higher (uses
readonlyclasses and enums) ext-json- No other extensions or libraries required (zero dependencies)
Installation
composer require renzojohnson/whatsapp-webhook
Quick Start
use RenzoJohnson\WhatsAppWebhook\WebhookHandler; use RenzoJohnson\WhatsAppWebhook\MessageType; use RenzoJohnson\WhatsAppWebhook\Notification\MessageNotification; $handler = new WebhookHandler('your-app-secret'); // Register listeners $handler->onMessage(MessageType::Text, function (MessageNotification $msg): void { echo "Message from {$msg->from}: {$msg->text()}"; }); // Verify signature and handle $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);
Webhook Endpoint Setup
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 handle try { $notifications = $handler->handle($rawBody); http_response_code(200); } catch (WebhookException $e) { http_response_code(400); }
Message Types
| Type | Enum | Description |
|---|---|---|
text |
MessageType::Text |
Plain text message |
image |
MessageType::Image |
Image with optional caption |
video |
MessageType::Video |
Video with optional caption |
audio |
MessageType::Audio |
Audio message |
document |
MessageType::Document |
Document with filename |
sticker |
MessageType::Sticker |
Sticker |
location |
MessageType::Location |
GPS coordinates with name/address |
contacts |
MessageType::Contacts |
Contact cards |
button |
MessageType::Button |
Quick reply button tap |
interactive |
MessageType::Interactive |
Button reply or list selection |
reaction |
MessageType::Reaction |
Emoji reaction |
order |
MessageType::Order |
Product order |
Listen by Message Type
use RenzoJohnson\WhatsAppWebhook\MessageType; use RenzoJohnson\WhatsAppWebhook\Notification\MessageNotification; $handler->onMessage(MessageType::Text, function (MessageNotification $msg): void { echo $msg->text(); }); $handler->onMessage(MessageType::Image, function (MessageNotification $msg): void { echo "Image: {$msg->mediaId()} ({$msg->mediaMimeType()})"; echo "Caption: {$msg->caption()}"; }); $handler->onMessage(MessageType::Location, function (MessageNotification $msg): void { echo "Location: {$msg->latitude()}, {$msg->longitude()}"; echo "Name: {$msg->locationName()}"; }); $handler->onMessage(MessageType::Interactive, function (MessageNotification $msg): void { if ($msg->interactiveType() === 'button_reply') { echo "Button: {$msg->interactiveButtonTitle()} (ID: {$msg->interactiveButtonId()})"; } if ($msg->interactiveType() === 'list_reply') { echo "List: {$msg->interactiveListTitle()}"; } }); $handler->onMessage(MessageType::Reaction, function (MessageNotification $msg): void { echo "Reacted with {$msg->reactionEmoji()} to {$msg->reactionMessageId()}"; });
Listen to All Messages
$handler->onAnyMessage(function (MessageNotification $msg): void { error_log($msg->getSummary()); });
Status Updates
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()}"; } }); $handler->onAnyStatus(function (StatusNotification $status): void { error_log($status->getSummary()); });
Pricing and Conversation Info
$handler->onAnyStatus(function (StatusNotification $status): void { if ($status->isBillable()) { echo "Category: {$status->pricingCategory()}"; echo "Model: {$status->pricingModel()}"; } if ($status->conversationId()) { echo "Conversation: {$status->conversationId()}"; echo "Origin: {$status->conversationOrigin()}"; } });
Forwarded Messages
$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"; } if ($msg->contextMessageId()) { echo "Reply to: {$msg->contextMessageId()}"; } });
Parse Without Dispatching
$notifications = $handler->parse($rawBody); foreach ($notifications as $notification) { if ($notification instanceof MessageNotification) { echo $notification->getSummary(); } }
Error Handling
use RenzoJohnson\WhatsAppWebhook\WebhookException; try { $handler->parse($rawBody); } catch (WebhookException $e) { // Invalid JSON, wrong object type, empty entry }
Related Packages
- whatsapp-cloud-api — Send WhatsApp messages via Cloud API
Links
License
MIT License. Copyright (c) 2026 Renzo Johnson.