WEBHOOKS

WAAPI Webhook Setup

Receive incoming messages, delivery status and template events on your own server. Add a webhook URL once — WAAPI handles the verification, dedup and signing for you.

Reference

A WAAPI webhook is an outbound HTTP POST that WAAPI sends to your server every time something happens on a WhatsApp number you own — a customer messages you, a message you sent is delivered or read, a template is approved, and so on.

You don't need to talk to Meta directly: WAAPI already receives Meta's raw webhooks, verifies them, deduplicates them, persists the data and then forwards a clean, signed event to your endpoint.

Configure from the dashboard

Webhooks are managed in Setup → Webhooks per WhatsApp number. Each entry is scoped to one account, has its own secret, headers, filters and schedule.

Resources

Download references

Architecture

How it works

event flow
WhatsApp user
     │ sends / reads / replies
     
Meta Cloud API
     │ delivers to WAAPI (handled for you)
     
WAAPI Backend
     │ persists, dedupes, fans out to active webhooks
     
Your server
     │ POST {your url}  +  X-Webhook-Signature: <hmac-sha256>
     │ + any custom headers you configured
     
Respond 2xx
   (failures bump a counter — no automatic retries)

A single inbound event can fan out to multiple webhooks — for example, you can keep one webhook that mirrors every event into your data lake and a second one filtered to fire only on messages that start with ORDER.

Setup

Create a webhook

  1. Open Setup → Webhooks in the WAAPI dashboard.
  2. Pick the WhatsApp number this webhook belongs to.
  3. Enter a friendly name and the public URL on your server.
  4. Choose which events should trigger the webhook (or pick all).
  5. (Optional) Set a secret — WAAPI will sign every payload with it.
  6. (Optional) Add custom headers, message content filters and an active schedule.
  7. Save — your webhook starts firing immediately on the next matching event.

Full configuration shape

webhook config
{
  "name": "Order Bot",
  "url": "https://api.yourapp.com/waapi/incoming",
  "events": ["message.received"],
  "secret": "whk_8f3c...",
  "headers": [
    { "key": "X-Tenant", "value": "acme" }
  ],
  "message_filters": [
    { "type": "starts_with", "value": "ORDER" }
  ],
  "filter_logic": "all",
  "schedule": {
    "type": "weekly",
    "days": [1, 2, 3, 4, 5],
    "start_time": "09:00",
    "end_time": "18:00"
  },
  "schedule_timezone": "Asia/Kolkata",
  "is_active": true
}
FieldTypeRequiredDescription
namestringYesFriendly name (max 100 chars)
urlstringYesPublic HTTPS endpoint on your server
eventsstring[]YesOne or more event types (see below) or ["all"]
secretstringNoUsed to sign every payload (X-Webhook-Signature). Strongly recommended.
headersarrayNoCustom headers added to every outbound request
message_filtersarrayNoText-content filters (only applied for message.received)
filter_logicstringNoany (default) or all
scheduleobjectNoTime-of-day activation rules — default { type: "always" }
schedule_timezonestringNoIANA tz for schedule (default: Asia/Kolkata)
is_activebooleanNoToggle without deleting (default: true)

Catalog

Event types

EventFires when
message.receivedA customer sends a new message to your business number
message.sentA message your account sends (via API or dashboard) is accepted by Meta
message.statusDelivery / read receipt — sent → delivered → read → failed
template.approvedA template you submitted is approved by Meta
template.rejectedA template is rejected
template.pendingA template moves into review
account.updatePhone-number or WABA-level policy / quality changes
allWildcard — match every supported event

Schema

Event payloads

Every webhook body is JSON. The event field tells you which event you're receiving — branch on it.

message.received

message.received
{
  "event": "message.received",
  "timestamp": "2026-03-05T10:30:00.000Z",
  "phone_number_id": "PHONE_NUMBER_ID",
  "display_phone_number": "+15551234567",
  "from": "+919876543210",
  "customer_name": "Jane Doe",
  "wa_message_id": "wamid.HBgL...",
  "type": "text",
  "text": "Hello, I need help with my order",
  "media_url": null,
  "media_id": null
}

message.sent

message.sent
{
  "event": "message.sent",
  "message_id": "6789abcdef1234567890abcd",
  "wa_message_id": "wamid.HBgL...",
  "conversation_id": "6789abcdef1234567890aaaa",
  "to": "+919876543210",
  "type": "text",
  "status": "sent",
  "timestamp": "2026-03-05T10:30:00.000Z"
}

message.status

message.status
{
  "event": "message.status",
  "wa_message_id": "wamid.HBgL...",
  "status": "delivered",
  "recipient": "+919876543210",
  "timestamp": "2026-03-05T10:30:04.000Z"
}

template.approved / rejected / pending

template.approved
{
  "event": "template.approved",
  "template_name": "order_confirmation",
  "language": "en_US",
  "category": "UTILITY",
  "timestamp": "2026-03-05T10:30:00.000Z"
}

account.update

account.update
{
  "event": "account.update",
  "phone_number_id": "PHONE_NUMBER_ID",
  "display_phone_number": "+15551234567",
  "field": "quality_update",
  "value": { "quality_score": "GREEN" },
  "timestamp": "2026-03-05T10:30:00.000Z"
}

Routing

Message content filters

When the event is message.received, WAAPI evaluates filters against the incoming text before firing. This lets you wire one webhook per intent — orders, support, OTPs — without parsing on your side. Filters do not apply to other event types.

Filter typeMatches when text…
containscontains the value (case-insensitive)
not_containsdoes NOT contain the value
equalsis exactly the value (case-insensitive)
not_equalsis anything other than the value
starts_withstarts with the value
ends_withends with the value

Combine multiple filters with filter_logic: any (fire if at least one matches) or all (fire only when every filter matches).

filter example
{
  "message_filters": [
    { "type": "starts_with", "value": "ORDER" },
    { "type": "not_contains", "value": "test" }
  ],
  "filter_logic": "all"
}

Time control

Active schedule

Limit when a webhook fires — useful for routing business-hours traffic to one endpoint and after-hours to another. Times are evaluated in schedule_timezone (default Asia/Kolkata).

schedule.typeBehaviorShape
alwaysFires whenever the event matches (default){ "type": "always" }
datetimeFires only inside the given start/end windows{ "type": "datetime", "ranges": [{ "start": "2026-03-05T09:00", "end": "2026-03-05T18:00" }] }
weeklyFires on the given weekdays, within start_time / end_time{ "type": "weekly", "days": [1,2,3,4,5], "start_time": "09:00", "end_time": "18:00" }
specialHoliday / promo overrides on specific dates{ "type": "special", "entries": [{ "date": "2026-12-25", "start_time": "00:00", "end_time": "23:59", "label": "Christmas" }] }
Weekdays use JavaScript's Date.getDay() convention: 0 = Sunday … 6 = Saturday.

Customization

Custom headers

Each webhook can carry arbitrary key/value headers on every request — handy for static tenant ids, internal bearer tokens, or a custom signature your gateway expects.

headers
{
  "headers": [
    { "key": "X-Tenant",     "value": "acme" },
    { "key": "Authorization", "value": "Bearer your_internal_token" }
  ]
}
Avoid setting Content-Type here — WAAPI always sends application/json.

Security

Signing & verification

When you set a secret on the webhook, WAAPI signs every body with HMAC-SHA256 and sends the hex digest in X-Webhook-Signature. Verify it on your side before trusting the payload.

verify (Node/Express)
import crypto from 'crypto';
import express from 'express';

const app = express();

app.post('/waapi/incoming', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const expected = crypto
    .createHmac('sha256', process.env.WAAPI_WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expected) return res.sendStatus(401);

  // Branch on event type
  switch (req.body.event) {
    case 'message.received':  /* … */ break;
    case 'message.status':    /* … */ break;
    case 'template.approved': /* … */ break;
  }

  res.sendStatus(200);
});

Other languages

Python
import hmac, hashlib, os

def verify(body_bytes, signature):
    expected = hmac.new(
        os.environ['WAAPI_WEBHOOK_SECRET'].encode(),
        body_bytes,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
PHP
function verify($body, $signature) {
    $ expected = hash_hmac('sha256', $body, getenv('WAAPI_WEBHOOK_SECRET'));
    return hash_equals($expected, $signature);
}

Use the raw bytes you received

Re-stringifying parsed JSON can change key ordering and break the hash. If your framework gives you the raw body buffer, use that directly with createHmac.

Operations

Delivery & retries

PropertyValue
MethodPOST
Content-Typeapplication/json
Signature headerX-Webhook-Signature (only when a secret is configured)
Timeout10 seconds per request
SuccessAny 2xx response — failure_count is reset to 0
FailureNon-2xx or timeout — failure_count is incremented
Automatic retryNone — WAAPI does not retry. Use a queue on your side if you need durability.
Auto-disableWebhooks repeatedly failing should be reviewed; check failure_count in the dashboard.

Idempotency on your side

Multiple webhooks can subscribe to the same event, and a single Meta delivery can be retried inside WAAPI. Use wa_message_id or message_id as your deduplication key.

Operations

Troubleshooting

SymptomMost likely cause
Webhook is not firingis_active is off, the event isn't subscribed, or the schedule is outside the current time
Fires for some messages onlyA message_filters rule is rejecting the rest — try filter_logic: "any"
401 on your serverSignature mismatch — make sure you're hashing the raw body bytes with the exact secret
Timeout / repeated 5xxYour handler is too slow — return 2xx first, then process async
Missing eventsAdd the event type, or use "all" as a wildcard
Duplicate processingDedupe on wa_message_id / message_id on your side
Plan limit hitWebhook creation is plan-gated (per-account count + monthly creation cap). Check Setup → Billing.

Tip — test from the playground

Use the API Tester to send a real message to your number and watch the webhook fire on your server in real time.