Webhooks

Webhooks let you receive real-time notifications when events happen. Register a URL and we send HTTP POST requests to it whenever a subscribed event occurs.


Event Types

Event Description
message.received A message was received from a contact
message.sent A message was sent to a contact
message.status_updated A message status changed (delivered, read, failed)
phone_number.connected A phone number was connected via onboarding
* Subscribe to all events

Create a Webhook

POST /v1/outgoing-webhooks

{
  "url": "https://yourapp.com/webhooks/whatsapp",
  "secret": "whsec_your_secret_key_min_16_chars",
  "events": ["message.received", "message.sent"],
  "isActive": true
}
Field Type Required Description
url string Yes Your HTTPS endpoint URL
secret string No Signing secret for payload verification (16-256 chars)
events string[] Yes At least one event type
isActive boolean No Whether webhook is active (default: true)

Manage Webhooks

Operation Endpoint
List GET /v1/outgoing-webhooks
Get GET /v1/outgoing-webhooks/{webhookId}
Update PATCH /v1/outgoing-webhooks/{webhookId}
Delete DELETE /v1/outgoing-webhooks/{webhookId}
Test POST /v1/outgoing-webhooks/{webhookId}/test

Payload Format

Every webhook delivery is an HTTP POST with a JSON body:

{
  "event": "message.received",
  "timestamp": "2026-03-03T15:30:00.000Z",
  "organizationId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "messageId": "msg-uuid",
    "conversationId": "conv-uuid",
    "phoneNumberId": "phone-uuid",
    "phoneNumber": "+15551234567",
    "contactPhone": "5491155551234",
    "contactName": "John Doe",
    "direction": "inbound",
    "type": "text",
    "content": { "body": "Hi, I'd like to place an order" },
    "wamid": "wamid.HBgLNTQ5MTE1NTU...",
    "status": "delivered",
    "externalId": "customer-123",
    "error": null
  }
}

The externalId field is included in all message webhook events (message.received, message.sent, message.status_updated). It contains the same externalId set during onboarding link creation, or null if the phone was not onboarded via an onboarding link.

Media messages: For inbound media messages (image, video, audio, document, sticker), the content object includes both a metadata URL and a direct download URL:

{
  "type": "image",
  "content": {
    "mediaId": "1234567890",
    "mediaUrl": "https://api.example.com/v1/phones/phone-uuid/media/1234567890",
    "downloadUrl": "https://api.example.com/v1/phones/phone-uuid/media/1234567890/download",
    "caption": "Photo of the product"
  }
}

Use downloadUrl to stream the actual file bytes.

Use mediaUrl when you need metadata (id, mimeType, fileSize, filename) plus the temporary Meta download URL.

Both endpoints require Authorization: Bearer <apiKey>.


Recovering from Missed Webhooks

If your system suspects it missed webhook deliveries, fetch the phone health snapshot:

curl https://api.example.com/v1/phones/{phoneId}/health \
  -H "Authorization: Bearer sk_org_..."

This returns the current registration state plus data.capabilities, and also kicks off an asynchronous resync against Meta. Use it as your first recovery step before alerting or disabling a phone.

phone_number.connected Payload

{
  "event": "phone_number.connected",
  "timestamp": "2024-01-15T10:35:00.000Z",
  "organizationId": "org_uuid",
  "data": {
    "onboardingLinkId": "link_uuid",
    "externalId": "customer-123",
    "phoneNumberId": "phone_uuid",
    "phoneNumber": "+5491155551234",
    "wabaId": "waba_uuid",
    "metaWabaId": "meta_waba_id",
    "isPhoneRegistered": true
  }
}

HTTP Headers

Every delivery includes:

Header Description
Content-Type application/json
User-Agent WhatsApp-CloudAPI-Webhook/1.0
X-Webhook-Event The event type
X-Webhook-Timestamp ISO 8601 timestamp
X-Webhook-Delivery Unique delivery ID (for deduplication)
X-Webhook-Signature-256 HMAC-SHA256 signature (only if secret is set)

Verifying Signatures

If you set a secret, verify the X-Webhook-Signature-256 header:

  1. Compute HMAC-SHA256 of the raw request body using your secret
  2. Compare with the header value (after removing the sha256= prefix)
  3. Use constant-time comparison to prevent timing attacks

Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhookSignature(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = signatureHeader.replace("sha256=", "");
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(received, "hex"));
}

Python:

import hmac, hashlib

def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature.replace("sha256=", ""))

What Your Endpoint Should Return

Inspect failures via GET /v1/outgoing-webhooks/{webhookId}/logs.