WhatsApp Webhook PHP: Parse Cloud API Webhooks with Typed Notifications

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

Links

Built by Renzo Johnson