> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rovax.io/llms.txt
> Use this file to discover all available pages before exploring further.

# WhatsApp Business API (Meta)

> Raw format of WhatsApp Business API webhooks — how to configure, validate, and process message and status payloads

The Meta **WhatsApp Business API (WABA)** sends events directly to an endpoint you register in the Facebook App. When you connect a WhatsApp number in Timely.ai, our platform receives those events, processes them, and transforms them into the workspace's [internal webhooks](/en/webhooks/overview). This page documents the raw format that Meta sends — useful for debugging, direct integrations, or when you need to understand what arrives before any transformation.

## Configure the webhook in the Facebook App

<Steps>
  <Step title="Access the Meta App Dashboard">
    Go to [developers.facebook.com](https://developers.facebook.com) → your App → **WhatsApp** → **Settings** → **Webhook**.
  </Step>

  <Step title="Enter the callback URL">
    Paste the URL of the endpoint that will receive the events. In production, always use HTTPS with a valid certificate.

    If you are testing locally, use ngrok or Hookdeck to expose your server:

    ```bash theme={null}
    ngrok http 3000
    # Forwarding: https://abc123.ngrok.io -> localhost:3000
    ```
  </Step>

  <Step title="Set the Verify Token">
    Choose a random, secure string (e.g. `my_secret_token_123`). Meta makes a `GET` request to your endpoint at setup time with the parameters `hub.mode`, `hub.verify_token`, and `hub.challenge`. Your server must respond with the value of `hub.challenge` if the token matches.

    ```typescript Node.js theme={null}
    app.get("/webhook/whatsapp", (req, res) => {
      const mode = req.query["hub.mode"];
      const token = req.query["hub.verify_token"];
      const challenge = req.query["hub.challenge"];

      if (mode === "subscribe" && token === process.env.VERIFY_TOKEN) {
        res.status(200).send(challenge);
      } else {
        res.sendStatus(403);
      }
    });
    ```
  </Step>

  <Step title="Select fields">
    Enable at least **messages** to receive messages and statuses. Other useful fields: `message_template_status_update`, `phone_number_quality_update`.
  </Step>
</Steps>

## Difference between Meta webhook and Timely webhook

| Aspect           | Meta webhook (raw)                     | Timely webhook (platform)             |
| ---------------- | -------------------------------------- | ------------------------------------- |
| Source           | Meta → your server                     | Timely → your server                  |
| Authentication   | `X-Hub-Signature-256` with App Secret  | `X-Timely-Signature` with your secret |
| Format           | Meta envelope with `entry[].changes[]` | Normalized payload by event type      |
| Contact resolved | No — only the `wa_id`                  | Yes — includes Timely `contact_id`    |
| Transformation   | None — raw Meta data                   | Enriched with workspace context       |

If you use Timely as an intermediary (the default case), you do not need to register a webhook with Meta — Timely handles that for you. This documentation is for cases where you connect WABA directly or need to debug the full flow.

## Signature validation

Meta signs each request with the **App Secret** of your Facebook App using HMAC-SHA256. The header sent is `X-Hub-Signature-256`.

```typescript Node.js theme={null}
import crypto from "crypto";

const APP_SECRET = process.env.META_APP_SECRET!;

function verifyMetaSignature(rawBody: Buffer, signature: string): boolean {
  const expected = "sha256=" + crypto
    .createHmac("sha256", APP_SECRET)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post("/webhook/whatsapp", (req, res) => {
  const sig = req.headers["x-hub-signature-256"] as string;
  if (!verifyMetaSignature(req.rawBody, sig)) {
    return res.sendStatus(401);
  }
  // process the event...
  res.sendStatus(200);
});
```

<Warning>
  Never process a Meta webhook without validating `X-Hub-Signature-256`. The App Secret is in your Facebook App dashboard — never expose it in source code or logs.
</Warning>

## Payload: received text message

```json theme={null}
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "WABA_ID",
      "changes": [
        {
          "value": {
            "messaging_product": "whatsapp",
            "metadata": {
              "display_phone_number": "5511999990000",
              "phone_number_id": "PHONE_NUMBER_ID"
            },
            "contacts": [
              {
                "profile": { "name": "João Silva" },
                "wa_id": "5511988887777"
              }
            ],
            "messages": [
              {
                "from": "5511988887777",
                "id": "wamid.HBgLNTUxMTk4ODg4Nzc3FQIAERgSMjRBOTQ5MzJBMTIzNDU2Nzg5AA==",
                "timestamp": "1713456789",
                "text": { "body": "Olá, quero saber sobre os planos." },
                "type": "text"
              }
            ]
          },
          "field": "messages"
        }
      ]
    }
  ]
}
```

## Payload: sent message status

Statuses arrive when the message changes state: `sent` → `delivered` → `read`.

```json theme={null}
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "WABA_ID",
      "changes": [
        {
          "value": {
            "messaging_product": "whatsapp",
            "metadata": {
              "display_phone_number": "5511999990000",
              "phone_number_id": "PHONE_NUMBER_ID"
            },
            "statuses": [
              {
                "id": "wamid.HBgLNTUxMTk4ODg4Nzc3FQIAERgSMjRBOTQ5MzJBMTIzNDU2Nzg5AA==",
                "status": "delivered",
                "timestamp": "1713456800",
                "recipient_id": "5511988887777",
                "conversation": {
                  "id": "CONVERSATION_ID",
                  "expiration_timestamp": "1713543200",
                  "origin": { "type": "user_initiated" }
                },
                "pricing": {
                  "billable": true,
                  "pricing_model": "CBP",
                  "category": "user_initiated"
                }
              }
            ]
          },
          "field": "messages"
        }
      ]
    }
  ]
}
```

## Payload: media message (image, audio, document)

```json theme={null}
{
  "messages": [
    {
      "from": "5511988887777",
      "id": "wamid.EXEMPLO",
      "timestamp": "1713456900",
      "type": "image",
      "image": {
        "caption": "Segue o comprovante",
        "mime_type": "image/jpeg",
        "sha256": "abc123...",
        "id": "MEDIA_ID"
      }
    }
  ]
}
```

To download the media, make a `GET https://graph.facebook.com/v19.0/{MEDIA_ID}` request with your access token. Timely.ai performs this download automatically and stores the public URL in the `media_url` field of the normalized message.

## Key payload fields

| Field                                      | Type   | Description                                                                        |
| ------------------------------------------ | ------ | ---------------------------------------------------------------------------------- |
| `entry[].id`                               | string | WhatsApp Business Account ID                                                       |
| `changes[].value.metadata.phone_number_id` | string | Phone number ID at Meta                                                            |
| `messages[].from`                          | string | Sender's number (international format without `+`)                                 |
| `messages[].id`                            | string | Unique message ID (`wamid.*`)                                                      |
| `messages[].timestamp`                     | string | Unix timestamp in seconds                                                          |
| `messages[].type`                          | string | `text`, `image`, `audio`, `video`, `document`, `location`, `interactive`, `button` |
| `statuses[].status`                        | string | `sent`, `delivered`, `read`, `failed`                                              |
| `statuses[].pricing.category`              | string | Conversation billing category                                                      |

## Next steps

<CardGroup cols={2}>
  <Card title="Platform webhooks" icon="bell" href="/en/webhooks/overview">
    Understand Timely.ai's normalized outbound webhooks.
  </Card>

  <Card title="Channels — API Reference" icon="plug" href="/en/api-reference/channels/referencia">
    Manage WhatsApp channels via API.
  </Card>
</CardGroup>
