Skip to content

Webhooks and events

Meta delivers incoming messages, delivery receipts and template reviews to your app as webhooks. The package receives them, checks they're genuine, and dispatches Laravel events.

The webhook route

The package registers the route for you:

GET|POST  /whatsapp/webhook

GET answers Meta's verification request using WHATSAPP_WEBHOOK_VERIFY_TOKEN. POST receives events. The route doesn't use the web middleware group, so you don't need to exclude it from CSRF protection.

When WHATSAPP_WEBHOOK_SECRET is set to your App Secret, requests with a missing or wrong X-Hub-Signature-256 signature get a 401 response. Without it, requests are accepted and a warning is logged each time. Set it in production: otherwise anyone who knows the URL can send you fake events.

Events

All events are in the Crenspire\Whatsapp\Events namespace.

EventDispatched whenMain properties
MessageReceivedA customer sends you a messagemessageId, from, message, timestamp, phoneNumberId, contact, userId
MessageStatusUpdatedAny status change of a message you sent: sent, delivered, read or failedmessageId, recipient, status, timestamp, data, phoneNumberId
MessageDeliveredA message reaches the customer's phonemessageId, recipient, timestamp, phoneNumberId
MessageReadThe customer reads a messagemessageId, recipient, timestamp, phoneNumberId
MessageFailedA send request fails, or WhatsApp later reports that delivery failedrecipient, error, messageId, payload, phoneNumberId, tenantId
MessageSentThe API accepts a message you sentmessageId, recipient, response, payload, phoneNumberId, tenantId
TemplateStatusUpdatedMeta approves, rejects, pauses or disables a templatetemplateId, name, language, status, reason, category
TemplateQualityUpdatedA template's quality score changestemplateId, name, language, previousScore, newScore

MessageSent and MessageFailed for send requests are dispatched by your own code when it sends. The others come from webhooks.

Handling incoming messages

Register a listener in a service provider's boot() method, or create a listener class:

php
use Crenspire\Whatsapp\Events\MessageReceived;
use Crenspire\Whatsapp\Facades\Whatsapp;
use Illuminate\Support\Facades\Event;

Event::listen(function (MessageReceived $event) {
    if ($event->buttonReplyId() === 'delivery_yes') {
        // The customer confirmed tomorrow's delivery
    }

    if ($event->isType('text')) {
        Whatsapp::showTypingIndicator($event->messageId);
        Whatsapp::sendTextMessage($event->from, "Thanks {$event->senderName()}, we got your message.", replyTo: $event->messageId);
    }
});

MessageReceived has helpers for the common fields, so you rarely need the raw message array:

HelperReturns
type(), isType('image')The message type: text, image, interactive, button, location and so on
text()The text, media caption, or title of the tapped button or list item
buttonReplyId()The ID of the reply button the customer tapped
listReplyId()The ID of the list item the customer picked
buttonPayload()The payload of a template quick-reply button
flowResponse()The decoded response of a completed WhatsApp Flow
mediaId()The media ID of an image, video, audio, document or sticker
replyToMessageId()The ID of your message the customer replied to or tapped a button on
senderName()The customer's WhatsApp profile name
senderUsername()The customer's WhatsApp username, if they have one
hasPhoneNumber()Whether WhatsApp shared the customer's phone number

To save a photo a customer sent you:

php
Event::listen(function (MessageReceived $event) {
    if ($event->isType('image')) {
        $path = Whatsapp::downloadMedia($event->mediaId());
    }
});

Delivery status

php
use Crenspire\Whatsapp\Events\MessageFailed;
use Crenspire\Whatsapp\Events\MessageRead;

Event::listen(function (MessageRead $event) {
    Order::where('whatsapp_message_id', $event->messageId)->update(['customer_read_at' => $event->timestamp]);
});

Event::listen(function (MessageFailed $event) {
    // $event->messageId is set when WhatsApp reports the failure by webhook
    logger()->warning('WhatsApp message failed', ['to' => $event->recipient, 'error' => $event->error]);
});

Store the message ID from the send response ($response['messages'][0]['id']) to match status events to your own records. The message log can do this for you.

Template reviews

Subscribe to message_template_status_update and message_template_quality_update in the Meta App Dashboard, then:

php
use Crenspire\Whatsapp\Events\TemplateStatusUpdated;
use Illuminate\Support\Facades\Log;

Event::listen(function (TemplateStatusUpdated $event) {
    if ($event->isRejected()) {
        Log::warning("Template {$event->name} was rejected: {$event->reason}");
    }
});

reason is set for rejections, for example INCORRECT_CATEGORY or PROMOTIONAL, and is null otherwise. The raw webhook data, including Meta's recommendation for invalid templates, is in $event->data.

Duplicate deliveries

Meta retries webhooks that don't get a quick 200 response, so the same event can arrive more than once. The package remembers processed messages and statuses in your cache for 24 hours and skips repeats, so your listeners run once per event.

If you run several servers, use a shared cache store such as Redis or your database, or set WHATSAPP_WEBHOOK_CACHE_STORE. You can turn this off with WHATSAPP_WEBHOOK_DEDUPLICATE=false.

Keep listeners fast so Meta gets its response quickly. Queue slow work by making the listener implement ShouldQueue.

Customers without a phone number

WhatsApp users can hide their phone number behind a username. Messages from them only include a business-scoped user ID: $event->from holds that ID, $event->userId is set, and $event->hasPhoneNumber() returns false. The package can't send messages to user IDs yet.

Logging

Every webhook is logged at info level with the number of entries. Full payloads contain customers' messages, so they're only logged when WHATSAPP_DEBUG=true.

Released under the MIT License. Not affiliated with Meta or WhatsApp.