Webhooks
Receive real-time WhatsApp delivery events and inbound messages with MailerSend WhatsApp webhooks. See all events, payloads and how to verify signatures.
WhatsApp webhooks let you subscribe to real-time notifications about the WhatsApp messages you send, and about the messages your recipients send back to you.
Because WhatsApp messages are sent asynchronously, webhooks are the only way to learn what happened to a message after it was accepted with a 202 response.
WhatsApp webhooks overview
Setup
WhatsApp webhooks are created in your MailerSend account under WhatsApp → Phone numbers → your sender → Webhooks. There are no API endpoints for managing WhatsApp webhooks yet.
Each webhook belongs to a single sender and only receives events for messages sent from that sender, or received by it. You choose which events the webhook subscribes to when you create it.
Every webhook has its own signing secret, which you can reveal in the webhook's details.
Available events
These are all the events you can listen to and send a notification for.
| Event | Description |
|---|---|
whatsapp.sent | Fired when your message has been accepted by WhatsApp and sent from our sending servers. |
whatsapp.delivered | Fired when your message is delivered to the recipient's device. |
whatsapp.read | Fired when the recipient opens your message. |
whatsapp.failed | Fired when your message could not be delivered. The payload includes the WhatsApp error code and description. |
whatsapp.inbound | Fired when someone sends a message to your WhatsApp sender. |
Payload example
Our responses contain fat payloads, including the information about the event-related object, so there is no need to make an additional API request.
An example of the whatsapp.delivered event:
{
"type": "whatsapp.delivered",
"webhook_id": "7z3m5jgrogdpyo6n",
"created_at": "2026-08-31T07:24:20.577080Z",
"data": {
"id": "62f114f7165fe0d8db0288e2",
"whatsapp_account_id": "3enl6x27wmrxrl2v",
"whatsapp_message_id": "67f91abd69f79df391e9d78d",
"whatsapp_item_id": "67f91abe2202f3705540234a",
"type": "delivered",
"template_name": "Order confirmation",
"template_id": "23zxk54v6gjy6v7m",
"from": "15550001234",
"to": "48600000001",
"sender": {
"id": "3enl6x27wmrxrl2v",
"display_name": "Shop Brand",
"phone_number": "15550001234",
"username": "shopbrand"
},
"meta": {
"whatsapp_id": "wamid.HBgLMzcwNjMzMjI3OTcVAgARGBIwREE3N0EwN0EyNjhENjEwOEMA"
}
}
}Payload fields
| Field | Type | Details |
|---|---|---|
type | string | The event that triggered the webhook. |
webhook_id | string | The ID of the webhook that delivered the event. |
created_at | string | When the event was created, in ISO 8601 format. |
data.id | string | The ID of the activity record for this event. |
data.whatsapp_account_id | string | The MailerSend ID of the sender the message was sent from. |
data.whatsapp_message_id | string | The ID returned when the message was sent. One message can have up to 10 recipients. |
data.whatsapp_item_id | string | The ID of the message for this single recipient. Use it to follow one recipient through all events. |
data.type | string | The status: sent, delivered, read or failed. |
data.template_name | string | The name of the template that was sent. |
data.template_id | string | The ID of the template that was sent. |
data.from | string | The sender's phone number or username. Display only — it is an empty string for a sender connected with a Meta virtual number. |
data.to | string | The recipient's phone number or BSUID. |
data.sender | object | The sender the message was sent from. |
data.sender.id | string | The MailerSend sender ID. Same value as data.whatsapp_account_id. |
data.sender.display_name | string | The display name shown to recipients. |
data.sender.phone_number | string|null | null for a sender connected with a Meta virtual number. |
data.sender.username | string|null | null if the sender has not claimed a WhatsApp username. |
data.meta | object|null | Extra information about the event. Carries whatsapp_id on success and the error details on failure. |
Use data.whatsapp_account_id or data.sender.id to group and join events by sender. data.from is display only and can be empty.
Failed messages
The whatsapp.failed event carries the error reported by WhatsApp in data.meta, together with a human-readable title and description so you can act on it without looking the code up.
{
"type": "whatsapp.failed",
"webhook_id": "7z3m5jgrogdpyo6n",
"created_at": "2026-08-31T07:24:20.577080Z",
"data": {
"id": "62f114f7165fe0d8db0288e2",
"whatsapp_account_id": "3enl6x27wmrxrl2v",
"whatsapp_message_id": "67f91abd69f79df391e9d78d",
"whatsapp_item_id": "67f91abe2202f3705540234a",
"type": "failed",
"template_name": "Order confirmation",
"template_id": "23zxk54v6gjy6v7m",
"from": "15550001234",
"to": "48600000001",
"sender": {
"id": "3enl6x27wmrxrl2v",
"display_name": "Shop Brand",
"phone_number": "15550001234",
"username": "shopbrand"
},
"meta": {
"error_code": 131026,
"error_message": "Message undeliverable",
"error_title": "Message Undeliverable",
"error_description": "Unable to deliver the message. Common reasons include the recipient's phone number being invalid, not registered on WhatsApp, or otherwise unable to receive messages."
}
}
}| Field | Type | Details |
|---|---|---|
meta.error_code | integer | The WhatsApp Cloud API error code. |
meta.error_message | string | The raw message returned by WhatsApp. |
meta.error_title | string|null | A short name for the error. |
meta.error_description | string|null | What the error means and what to do about it. |
See Meta's error code reference for the full list of codes.
Inbound messages
The whatsapp.inbound event is fired when someone sends a message to your WhatsApp sender, including replies to your template messages and button taps.
{
"type": "whatsapp.inbound",
"webhook_id": "7z3m5jgrogdpyo6n",
"created_at": "2026-08-31T07:24:20.577080Z",
"data": {
"id": "62f114f7165fe0d8db0288e2",
"whatsapp_account_id": "3enl6x27wmrxrl2v",
"whatsapp_inbound_message_id": "62f114f7165fe0d8db0288e2",
"meta_message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBIwREE3N0EwN0EyNjhENjEwOEMA",
"from": "48600000001",
"to": "15550001234",
"sender": {
"id": "3enl6x27wmrxrl2v",
"display_name": "Shop Brand",
"phone_number": "15550001234",
"username": "shopbrand"
},
"received_at": "2026-08-31T07:24:19.000000Z",
"type": "text",
"text": {
"body": "Hello, I would like to check my order status."
},
"context": {
"message_id": "wamid.HBgLMzcwNjMzMjI3OTcVAgARGBIwREE3N0EwN0EyNjhENjEwOEMA"
}
}
}| Field | Type | Details |
|---|---|---|
data.id | string | The ID of the inbound message. |
data.whatsapp_inbound_message_id | string | Same value as data.id. |
data.meta_message_id | string | WhatsApp's own message ID. |
data.from | string | The phone number of the person who sent the message. |
data.to | string | Your sender's phone number. Empty for a sender connected with a Meta virtual number. |
data.received_at | string | When WhatsApp received the message, in ISO 8601 format. |
data.type | string | The kind of message. See the table below. |
data.context | object | Present when the message is a reply. context.message_id is the WhatsApp ID of the message that was replied to. |
The data.type field determines which additional field is present in the payload:
type | Additional field | Contents |
|---|---|---|
text | text | body with the message text. |
image, audio, video, document, sticker | attachment | The downloaded media file. See Media attachments. |
location | location | latitude, longitude, and name and address when shared. |
contacts | contacts | The shared contact cards, as sent by WhatsApp. |
reaction | reaction | emoji and the message_id of the message that was reacted to. |
button | button | text and payload of the quick reply button that was tapped. |
interactive | list_reply or interactive | id, title and description for a list reply, or the raw interactive payload for other subtypes. |
order, system, unknown, unsupported | matching the type | The raw payload as sent by WhatsApp. |
Media attachments
Media in an inbound message is downloaded and stored by MailerSend, and the payload carries a temporary signed URL to it.
{
"type": "image",
"attachment": {
"url": "https://api.mailersend.com/whatsapp/inbound-message-attachment/62f114f7165fe0d8db0288e2?account=3enl6x27wmrxrl2v&expires=1790000000&signature=...",
"status": "stored",
"mime_type": "image/jpeg",
"size": 102400,
"caption": "Here is the damaged item"
}
}| Field | Type | Details |
|---|---|---|
attachment.url | string|null | A temporary signed URL to the file. null when the download failed. |
attachment.status | string | stored when the file is available, failed when it could not be downloaded. |
attachment.mime_type | string | The media type reported by WhatsApp. |
attachment.size | integer | The file size in bytes. |
attachment.caption | string | The caption, when the sender added one. |
attachment.filename | string | The original file name, for documents. |
attachment.animated | boolean | Present for stickers only. |
Attachments expire
Inbound media is kept for 1 day and the signed URL expires with it. Download the file as soon as you receive the event if you need to keep it.
Security
Webhook requests made by MailerSend include a Signature header. It contains a string generated by hashing the data sent to your webhook endpoint with an individual Signing Secret. A signing secret is a random string that is generated when you create a webhook.
Verifying a signature:
// $signature - a header sent by MailerSend, please refer to your framework
// or PHP manual on how to read the Signature header
// $requestContent - please refer to your framework or PHP manual on how to read the request content
$computedSignature = hash_hmac('sha256', $requestContent, $signingSecret);
return hash_equals($signature, $computedSignature);Testing webhooks
You can send a test event to your endpoint from the webhook's details in your MailerSend account. Test events use the same structure as real ones and are signed with your webhook's signing secret.
The webhook's log shows every delivery attempt, the response code your endpoint returned, and lets you retry a failed attempt.
Webhook failure handling
If your endpoint is unreachable or returns a non-2xx response, MailerSend automatically retries with exponential backoff for up to about 3 days. While your webhook is failing we'll send escalating email notifications. If the endpoint stays down too long, the webhook is automatically paused. You can re-enable it from your dashboard at any time.
Retrying failed webhooks
When your webhook receives a response other than a 2xx code from your endpoint URL, or if the endpoint doesn't respond within 3 seconds, it will show up as a failed attempt in the log section of your webhook. If it receives a 2xx, then it will show as a success.
If a webhook call fails, MailerSend automatically retries with exponential backoff for up to about 3 days.
Useful tools
Webhook.site or Pipedream.com are useful tools for testing webhooks quickly, seeing how it works, and inspecting what's being sent—without any coding on your side.