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:
- Compute HMAC-SHA256 of the raw request body using your secret
- Compare with the header value (after removing the
sha256=prefix) - 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
- Return a 2xx status code to acknowledge receipt
- Respond within 30 seconds
- Any non-2xx response or timeout is treated as a failed delivery
Inspect failures via GET /v1/outgoing-webhooks/{webhookId}/logs.