Purpose
Messaging webhooks are the real-time event stream for WhatsApp traffic. They include inbound user messages (messages), outbound delivery/read status changes (statuses), and messaging errors (errors).
In Dualhook, message-path events are delivered by Meta directly to your endpoint through Webhook Override. Dualhook handles management-event processing separately.
Webhook Setup in Dualhook
When a connection is subscribed, Dualhook configures callback override through Meta Graph API:
curl -X POST "https://graph.facebook.com/<GRAPH_VERSION>/<WABA_ID>/subscribed_apps" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"override_callback_uri":"https://api.yourdomain.com/webhooks/meta/messages",
"verify_token":"<YOUR_VERIFY_TOKEN>",
"subscribed_fields":["messages","history","smb_message_echoes"]
}'
Callback URL Requirements
Use a publicly reachable HTTPS endpoint:
- Use standard TLS endpoint URLs.
- Avoid underscores in hostnames.
- Avoid custom port patterns — use standard HTTPS serving.
Verification Handshake (GET)
Meta verifies callback ownership with a challenge request:
GET /webhooks/meta/messages?hub.mode=subscribe&hub.verify_token=<TOKEN>&hub.challenge=1903260781
Return HTTP 200 with the challenge value as the response body.
Signature Validation (POST)
For webhook POST requests, validate X-Hub-Signature-256 using your app secret and the raw request body before processing. Reject requests with invalid signatures.
Webhook Envelope
All messaging webhook payloads use this outer structure:
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"field": "messages",
"value": {}
}
]
}
]
}
Inside entry[].changes[].value, expect messages for inbound events, statuses for outbound lifecycle updates, and errors for messaging failures.
Inbound Text Message
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550001234",
"phone_number_id": "123456789012345"
},
"contacts": [
{
"profile": { "name": "Alex Example" },
"wa_id": "12015550123"
}
],
"messages": [
{
"from": "12015550123",
"id": "wamid.HBgLMTIwMTU1NTAxMjMVAgARGBI...",
"timestamp": "1735939200",
"type": "text",
"text": { "body": "Hello from customer" }
}
]
}
}
]
}
]
}
Inbound Button Reply
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550001234",
"phone_number_id": "123456789012345"
},
"messages": [
{
"from": "12015550123",
"id": "wamid.HBgLMTIwMTU1NTAxMjMVAgARGBJ...",
"timestamp": "1735939300",
"type": "button",
"button": {
"text": "Track order",
"payload": "track_order"
},
"context": {
"id": "wamid.HBgLMTIwMTU1NTAxMjMVAgARGBI..."
}
}
]
}
}
]
}
]
}
Outbound Status Update
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"statuses": [
{
"id": "wamid.HBgLMTIwMTU1NTAxMjMVAgARGBI...",
"status": "delivered",
"timestamp": "1735939400",
"recipient_id": "12015550123",
"conversation": {
"id": "d9f6f2f6d17f4b20b0",
"origin": { "type": "utility" }
},
"pricing": {
"billable": true,
"pricing_model": "CBP",
"category": "utility"
}
}
]
}
}
]
}
]
}
Error Notification
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"errors": [
{
"code": 131026,
"title": "Message Undeliverable",
"message": "Recipient is not reachable at this time.",
"error_data": {
"details": "The message could not be delivered to the user."
}
}
]
}
}
]
}
]
}
Acknowledgment and Latency
- Return
HTTP 200 OKas soon as payload authenticity and basic shape are verified. - Do heavy processing asynchronously after ack.
- Keep median callback response time around sub-250 ms.
- Keep slow responses (>1s) to a very small fraction of requests.
Throughput Planning
Outbound sends produce multiple status callbacks (sent, delivered, read). Size your webhook capacity for roughly 3x outbound message rate + inbound message rate during busy windows.
If your endpoint does not return 200, Meta retries delivery with exponential backoff. Retries can continue for multiple days, so idempotent handling is required.
Processing Best Practices
- Persist raw webhook payloads with an internal trace ID.
- Deduplicate by
wamid(and status tuple where relevant). - Separate inbound message consumers from status/error consumers.
- Monitor webhook ack latency and non-
200rates independently of business logic. - Send
status=readvia the messages endpoint after your app accepts an inbound message.