Messaging Webhook

Real-time webhook events for inbound messages, delivery statuses, and errors.

Looking for the high-level overview first? Start with WhatsApp Webhook and then return here for the implementation details.

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.

Starting March 31, 2026, these webhooks include new BSUID fields and some existing fields become conditional. See the BSUID transition guide for payload changes and migration guidance.

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>"
  }'

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.

Inbound Webhook POST Validation

This is the most common source of confusion in the Dualhook flow, so read this carefully before wiring up signature verification on your endpoint.

In this section, POST means the inbound webhook POST that Meta sends to your callback URL. It does not mean outbound Graph API calls such as POST /{PHONE_NUMBER_ID}/messages; outbound sends use your WhatsApp Cloud API access token in the Authorization: Bearer ... header.

When you onboard a number through Dualhook's Embedded Signup (the standard path, including Coexistence), the WhatsApp Business Account is subscribed to Dualhook's Meta app, not your own. Meta therefore signs X-Hub-Signature-256 using Dualhook's Meta App Secret, which is shared at the Meta-app level across all our customers and cannot be exposed.

So in the Dualhook-managed flow:

  • Your own META_APP_SECRET will not match the signature — your HMAC computation will be correct, but the secret is wrong.
  • The webhook verify token you configured in Dualhook is for the GET verification handshake only. It is not the POST signing secret and cannot be used to validate X-Hub-Signature-256.
  • Dualhook does not re-sign or add an X-Dualhook-Signature to message-path webhooks, because those payloads go from Meta directly to your endpoint — Dualhook is not in the message path. (X-Dualhook-Signature only applies to Dualhook Platform lifecycle events such as onboarding.completed — see Webhook Signature Verification.)

The recommended validation stack for message-path webhooks in the Dualhook-managed flow is therefore:

  1. Verify the payload shape matches the WhatsApp webhook envelope (object: "whatsapp_business_account", entry[].changes[].field === "messages"). Inbound messages, statuses, and failures appear inside entry[].changes[].value.
  2. Validate value.metadata.phone_number_id against the phone_number_id you stored for this tenant/connection. Reject missing or unknown IDs for message-path events.
  3. Validate the entry[].id (WABA ID) against the WABA you expect for this tenant.
  4. Use a high-entropy, per-tenant webhook URL path (random segment, not guessable) and treat that path as a secret. Rotate it if it leaks.
  5. Serve over HTTPS only, return 200 quickly, and reject unexpected methods.
  6. For stronger sender authentication at the transport layer, enable Meta webhook mTLS where your infrastructure supports client certificate verification. Still keep the WABA and phone-number checks above, because mTLS proves the request came from Meta, not which tenant it belongs to.

If you bring your own Meta app and subscribe it to the WABA yourself (a non-default flow that Dualhook does not manage for you), then X-Hub-Signature-256 is signed with your Meta App Secret and standard HMAC verification on the raw request body applies. This is rare and is not what Embedded Signup configures.

If cryptographic signing of message-path webhooks with a secret you control is a hard requirement for your compliance posture, contact support — it requires a different routing architecture than the standard Webhook Override flow.

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 OK as 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-200 rates independently of business logic.
  • Send status=read via the messages endpoint after your app accepts an inbound message.

Related

Browse more docsStart Free Trial