WAAPI External API
The WAAPI programmatic API, organized by service namespace. WhatsApp Cloud API endpoints live under /whatsapp-api/*; WhatsApp QR session endpoints live under /whatsapp-qr/*; contacts are cross-cutting at /contacts. Each API key may be bound to any mix of services, accounts, numbers and scopes — and you pick which number to send from per-request via fromPhoneNo.
Base URL
https://cname.lamda.thewaapi.com/api/v1/external
Auth headers
X-API-Key · X-API-Secret
Namespaces
/whatsapp-api · /whatsapp-qr · /contacts
Authentication
Every request must carry your API key in the X-API-Key header AND your secret in X-API-Secret. Both are returned once when you create the key in your WAAPI panel — copy them to a safe place at that moment.
Every send must carry both the recipient to and the sending number fromPhoneNo (E.164). fromPhoneNo must be one of the WhatsApp numbers your key is bound to — a key can be bound to one number, several, or all of them.
/pingAuthentication
Every request must carry both the X-API-Key header AND the X-API-Secret header. A key is bound to one or more of your WhatsApp numbers; every send must name which one to use via `fromPhoneNo` (E.164), alongside the recipient `to`. The ping endpoint echoes back your vendor id and the name of the key you authenticated with.
| Field | Type | Req? | Description |
|---|---|---|---|
| X-API-Key | header | required | Public API key, starts with `waapi_` |
| X-API-Secret | header | required | Secret token, starts with `secr_`. Required when the key was created with a secret. |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/ping' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"pong": true,
"vendorId": "6a2a74...",
"apiKeyName": "Production CRM"
}Error responses (3)
HTTP 401 — Missing API key
{
"success": false,
"message": "API key required (Authorization: Bearer <waapi_… key or JWT> or X-API-Key header)"
}HTTP 401 — Missing secret
{
"success": false,
"message": "API secret required (X-API-Secret header)"
}HTTP 401 — Invalid credential
{
"success": false,
"message": "Invalid API secret"
}Error format
Errors return a non-2xx HTTP status with a JSON body. Validation problems are 400, auth problems 401, missing permissions 403, missing resources 404, conflicts 409, and Meta-side send failures bubble up as 400 with Meta's error code in the message.
{
"success": false,
"error": "(#131026) Recipient phone number does not have WhatsApp installed"
}| Status | Meaning |
|---|---|
| 400 | Bad request — body validation failed or Meta returned an error code |
| 401 | Auth — missing/invalid X-API-Key or X-API-Secret |
| 403 | Forbidden — key lacks the required permission or scope |
| 404 | Not found — resource (contact, message, template) does not exist |
| 409 | Conflict — duplicate or state-incompatible |
| 429 | Rate limited — slow down to your key's per-minute/hour quota |
| 500 | Server error — please retry; if persistent, open a ticket with the response body |
Cloud API — Send endpoints (/whatsapp-api/*)
/whatsapp-api/messages/template1. Send Template Message
Permissions: messages.send
Send an approved WhatsApp template by name. The template must be APPROVED in Meta Business Manager. Pass parameters using the Meta-shaped `components` array.
Note: AUTHENTICATION templates: pass variables:["<otp>"] — the OTP body value AND the copy-code button param are filled with the code for you. LTO templates: pass components with a limited_time_offer part (expiration_time_ms) and the copy_code button param (coupon_code) at index 0. CAROUSEL templates use the dedicated POST /whatsapp-api/messages/carousel endpoint below.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 (e.g. 917890000198) |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| name | string | optional | Template name (case-sensitive). Provide EITHER name+language OR templateId. |
| templateId | string | optional | Local template id — an alternative to name+language (language is taken from the stored template). |
| language | string | optional | BCP-47 language code (e.g. en, en_US, hi). Required when you pass name. |
| variables | string[] | optional | Positional body values for {{1}},{{2}}… — the simplest way to fill a text template. For an AUTHENTICATION template pass the single OTP code, e.g. ["123456"]. |
| components | array | optional | Meta-shaped components (header / body / button). Use this for LTO and any parameter that is not a plain positional body value. |
| template | object | optional | Friendly payload shape ({ header, body:{ variables }, buttons, copy_code, lto }) — projected to Meta components server-side. |
| replyToWaMessageId | string | optional | Quote a prior message (wamid) |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/template' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"name": "order_shipped",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Sayantan"
},
{
"type": "text",
"text": "AWB-12345"
}
]
}
]
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"id": "6a2dbff6...",
"status": "sent",
"to": "917890000198",
"type": "template",
"timestamp": "2026-06-17T12:00:00.000Z"
}
}Error responses (2)
HTTP 400 — Template not found
{
"success": false,
"error": "(#132001) Template name does not exist in the translation"
}HTTP 403 — Wrong scope
{
"success": false,
"message": "API key scope 'read' does not permit send actions"
}/whatsapp-api/messages/text3a. Send Text Message
Permissions: messages.send
Send a plain text message. Outside the 24-hour customer service window only TEMPLATES are allowed; this endpoint will error there.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| body | string | required | Message text (4096 char max) |
| previewUrl | boolean | optional | If true, the first URL in body gets a link preview card |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/text' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"body": "Hello from the WAAPI public API.",
"previewUrl": false
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"to": "917890000198",
"type": "text"
}
}Error responses (1)
HTTP 400 — Outside service window
{
"success": false,
"error": "(#131047) Re-engagement message: outside 24h window — send a template instead"
}/whatsapp-api/messages/image3b. Send Image
Permissions: messages.send
Send an image by public URL (`link`) or by a previously uploaded Meta media id (`mediaId`). One of the two is required.
Note: Variants: /whatsapp-api/messages/video (mp4/3gp, ≤16 MB), /messages/audio (aac/mp3/ogg/amr/m4a, ≤16 MB), /messages/sticker (webp).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of a JPG/PNG (≤5 MB) |
| mediaId | string | optional | Meta media_id from /media upload (≤30 days) |
| caption | string | optional | Optional caption (1024 char max) |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/image' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"link": "https://example.com/order.jpg",
"caption": "Your order, packed"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "image"
}
}/whatsapp-api/messages/document3c. Send Document
Permissions: messages.send
Send any document file (PDF, DOCX, XLSX, CSV, etc.) up to 100 MB. The filename you pass is what the recipient sees in WhatsApp.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of the file |
| mediaId | string | optional | Meta media_id from /media upload |
| filename | string | optional | Display filename shown to the recipient |
| caption | string | optional | Optional caption (1024 char max) |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/document' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"link": "https://example.com/invoice-1234.pdf",
"filename": "Invoice 1234.pdf",
"caption": "Your invoice"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "document"
}
}/whatsapp-api/messages/video3d. Send Video
Permissions: messages.send
Send a video by public URL (`link`) or a previously uploaded Meta media id (`mediaId`). mp4/3gp, ≤16 MB.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of the video |
| mediaId | string | optional | Meta media_id from a prior upload |
| caption | string | optional | Optional caption (1024 char max) |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/video' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"link": "https://example.com/clip.mp4",
"caption": "Unboxing your order"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "video"
}
}/whatsapp-api/messages/audio3e. Send Audio
Permissions: messages.send
Send an audio / voice note by public URL or media id. aac/mp3/ogg/amr/m4a, ≤16 MB. No caption.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of the audio file |
| mediaId | string | optional | Meta media_id from a prior upload |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/audio' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"link": "https://example.com/voice.ogg"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "audio"
}
}/whatsapp-api/messages/sticker3f. Send Sticker
Permissions: messages.send
Send a WebP sticker by public URL or media id. Static stickers ≤100 KB, animated ≤500 KB; must be exactly 512×512.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of the .webp sticker |
| mediaId | string | optional | Meta media_id from a prior upload |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/sticker' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"link": "https://example.com/sticker.webp"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "sticker"
}
}/whatsapp-api/messages/location3g. Send Location
Permissions: messages.send
Send a map pin. latitude + longitude are required; name + address are optional labels shown on the pin.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| latitude | number|string | required | Decimal latitude, e.g. 22.5726 |
| longitude | number|string | required | Decimal longitude, e.g. 88.3639 |
| name | string | optional | Place name label |
| address | string | optional | Address label |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/location' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"latitude": 22.5726,
"longitude": 88.3639,
"name": "WAAPI HQ",
"address": "Kolkata, India"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "location"
}
}/whatsapp-api/messages/contact3h. Send Contact (vCard)
Permissions: messages.send
Share one or more contact cards. `contacts` is the Meta contacts array (name + phones, optional emails/org).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| contacts | array | required | Meta-shaped contacts[]: each has name.formatted_name and phones[].phone |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/contact' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"contacts": [
{
"name": {
"formatted_name": "WAAPI Support",
"first_name": "WAAPI"
},
"phones": [
{
"phone": "+917890000199",
"type": "WORK",
"wa_id": "917890000199"
}
]
}
]
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "contacts"
}
}/whatsapp-api/messages/reaction3i. Send Reaction
Permissions: messages.send
React to a message with an emoji. Send an empty `emoji` ("") to REMOVE a previously sent reaction.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| messageId | string | required | wamid of the message you are reacting to |
| emoji | string | required | A single emoji, or "" to remove the reaction |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/reaction' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"messageId": "wamid.HBgM...",
"emoji": "👍"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "reaction"
}
}/whatsapp-api/messages/list3j. Send Interactive List
Permissions: messages.send
Send a sectioned list menu. The customer taps "buttonText" to open the list, picks a row → you get a list_reply webhook with the row id.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| body | string | required | Main message body |
| buttonText | string | required | Label of the button that opens the list (≤20 chars) |
| sections | array | required | sections[]: { title, rows:[{ id, title, description? }] } |
| header | string | optional | Optional header text |
| footer | string | optional | Optional footer text |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/list' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"body": "Pick a delivery slot",
"buttonText": "Choose slot",
"sections": [
{
"title": "Today",
"rows": [
{
"id": "slot_am",
"title": "Morning"
},
{
"id": "slot_pm",
"title": "Afternoon"
}
]
}
]
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "interactive"
}
}/whatsapp-api/messages/cta3k. Send CTA URL Button
Permissions: messages.send
Send a call-to-action message with a single tappable URL button (no template approval needed inside the 24h window).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| body | string | required | Main message body |
| ctaText | string | required | Button label (≤20 chars) |
| ctaUrl | string | required | HTTPS URL the button opens |
| header | string | optional | Optional header text |
| footer | string | optional | Optional footer text |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/cta' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"body": "Your invoice is ready.",
"ctaText": "View invoice",
"ctaUrl": "https://example.com/invoice/1234"
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "interactive"
}
}/whatsapp-api/messages/flow3l. Send Flow
Permissions: messages.send
Embed a published WhatsApp Flow. The customer taps the CTA to open the Flow form; you receive the completion payload on your webhook.
Note: Needs a REAL published flowId. For flowAction "navigate", Meta requires flowActionPayload.screen (the entry screen) — without it Meta returns (#131009).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| body | string | required | Main message body |
| flowId | string | required | Published Flow id |
| flowCta | string | required | CTA button label (≤20 chars) |
| flowToken | string | optional | Opaque token echoed back on completion |
| flowAction | string | optional | 'navigate' (default) or 'data_exchange' |
| flowActionPayload | object | optional | REQUIRED by Meta when flowAction is 'navigate': { screen, data? } naming the entry screen. |
| mode | string | optional | 'published' (default) or 'draft' for testing |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/flow' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"body": "Complete your KYC",
"flowId": "1234567890",
"flowCta": "Start",
"flowToken": "kyc-abc",
"flowActionPayload": {
"screen": "WELCOME"
}
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "interactive"
}
}/whatsapp-api/messages/carousel3m. Send Carousel Template
Permissions: messages.send
Send an APPROVED carousel template (MARKETING). Pass the bubble body params plus a `carousel` component whose `cards[]` mirror the template — each card supplies its header media (image/video by `id` or `link`) and any body params. Upload media first to get a media id.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| name | string | required | Carousel template name (must be APPROVED) |
| language | string | required | BCP-47 language code (e.g. en_US) |
| components | array | required | Meta carousel components: a body part (bubble) + a carousel part with cards[]. |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/carousel' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"name": "summer_promo_carousel",
"language": "en_US",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Sayantan"
}
]
},
{
"type": "carousel",
"cards": [
{
"card_index": 0,
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"id": "1234567890"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "$9.99"
}
]
}
]
},
{
"card_index": 1,
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"id": "1234567890"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "$19.99"
}
]
}
]
}
]
}
]
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"to": "917890000198",
"type": "template"
}
}Error responses (1)
HTTP 400 — Card mismatch
{
"success": false,
"error": "(#131009) number of cards does not match the template"
}/whatsapp-api/messages/raw3n. Send Raw (passthrough)
Permissions: messages.send
Power-user escape hatch: send a raw Meta Cloud-API message body verbatim (we add messaging_product + the resolved sender). Use for message shapes not yet wrapped by a dedicated endpoint.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| body | object | required | The raw Meta message object (everything after messaging_product/to). |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/raw' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"body": {
"type": "text",
"text": {
"body": "Sent via raw passthrough",
"preview_url": false
}
}
}'Success response
{
"success": true,
"data": {
"messageId": "wamid.HBgM...",
"status": "sent",
"type": "text"
}
}/whatsapp-api/media3o. Upload Media (→ media id)
Permissions: media.upload
Upload an image / video / document to WhatsApp and get a reusable media id (valid ~30 days). Use the id in image/video/document template headers and carousel cards — ideal when you have no public URL. Send a multipart `file`, OR a JSON body with a public `link` we fetch for you.
Note: Reference the id on a send: template header { type:"image", id:"<mediaId>" }, or a carousel card header parameters:[{ type:"image", image:{ id:"<mediaId>" } }].
| Field | Type | Req? | Description |
|---|---|---|---|
| file | multipart | optional | The media file (multipart/form-data field `file`). Use this OR `link`. |
| link | string | optional | A public HTTPS URL to the media — we fetch + upload it. Use this OR a multipart `file`. |
| fromPhoneNo | string | optional | Upload under this connected number's WABA (defaults to your default number). |
| filename | string | optional | Display filename (used for documents). |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/media' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"link": "https://example.com/product.jpg",
"fromPhoneNo": "+917890000199"
}'Success response
{
"success": true,
"data": {
"mediaId": "1234567890123456",
"mimeType": "image/jpeg",
"fileName": "product.jpg",
"size": 42012
}
}Error responses (2)
HTTP 400 — No media
{
"success": false,
"error": "Provide a multipart `file` OR a public `link` (https URL) to upload."
}HTTP 502 — Upload failed
{
"success": false,
"error": "Upload to WhatsApp failed"
}/whatsapp-api/messages/send5. Send Message + Update Contact (combined)
Permissions: messages.sendcontacts.write
Flagship endpoint: atomically upserts the contact AND dispatches ANY supported message type in one request. Saves a network round trip for CRM-style flows.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| countryCode | string | optional | Defaults to +91 |
| contact | object | optional | Same shape as POST /contacts (first_name, last_name, email, tags…). Pass an empty {} to skip upsert and only send. |
| type | enum | required | One of: 'text' | 'image' | 'video' | 'audio' | 'document' | 'template' | 'buttons' | 'list' | 'cta' |
| payload | object | required | Type-specific payload — same shape as the dedicated /messages/<type> endpoint |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/send' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"contact": {
"first_name": "Sayantan",
"email": "sayantan@example.com",
"tags": [
"new-lead"
]
},
"type": "text",
"payload": {
"body": "Hi Sayantan, thanks for signing up. Your account is now active."
}
}'Success response
{
"success": true,
"data": {
"contact": {
"_id": "6a2c5...",
"phoneNumber": "917890000198",
"first_name": "Sayantan",
"email": "sayantan@example.com",
"tags": [
"new-lead"
]
},
"message": {
"messageId": "wamid.HBgM...",
"status": "sent",
"to": "917890000198",
"type": "text"
}
}
}Error responses (2)
HTTP 403 — Missing perm
{
"success": false,
"error": "API key needs 'contacts.write' for combined send+upsert"
}HTTP 400 — Bad type
{
"success": false,
"error": "Unsupported type 'sticker-pack'"
}Cloud API — Read endpoints (/whatsapp-api/*)
/whatsapp-api/messages?contactPhone=917890000198&limit=50List Messages
Permissions: messages.read
List messages newest-first. Filter by `conversationId`, `contactPhone`, and a `before`/`after` ISO timestamp window. `limit` caps at 200.
| Field | Type | Req? | Description |
|---|---|---|---|
| conversationId | query | optional | Restrict to one conversation |
| contactPhone | query | optional | Restrict to messages to/from this number |
| before | query | optional | ISO timestamp — only messages older than this |
| after | query | optional | ISO timestamp — only messages newer than this |
| limit | query | optional | Max rows (default 50, max 200) |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages?contactPhone=917890000198&limit=50' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"_id": "6a2d...",
"messageId": "wamid.HBgM...",
"direction": "outgoing",
"type": "template",
"to": "917890000198",
"status": "read",
"timestamp": "2026-06-17T12:00:00.000Z"
}
],
"count": 1
}/whatsapp-api/messages/{id}Get Message by id
Permissions: messages.read
Fetch a single message document by its WAAPI id (the `id` field returned in a send response).
| Field | Type | Req? | Description |
|---|---|---|---|
| id | path | required | The message _id (not the wamid) |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/{id}' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": {
"_id": "6a2d...",
"messageId": "wamid.HBgM...",
"type": "template",
"status": "read"
}
}Error responses (1)
HTTP 404 — Not found
{
"success": false,
"error": "Not found"
}/whatsapp-api/conversations?status=open&limit=50List Conversations
Permissions: conversations.read
List conversations newest-activity-first. Optional `status` filter (open / resolved / closed). `limit` caps at 200.
| Field | Type | Req? | Description |
|---|---|---|---|
| status | query | optional | Filter by conversation status |
| limit | query | optional | Max rows (default 50, max 200) |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/conversations?status=open&limit=50' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"_id": "6a2e...",
"contactPhone": "917890000198",
"status": "open",
"lastMessageAt": "2026-06-17T12:00:00.000Z",
"unreadCount": 0
}
],
"count": 1
}/whatsapp-api/templatesList Templates
Permissions: templates.read
List the vendor's message templates with their current Meta status — use this to discover names + languages you can send.
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/templates' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"_id": "6a2f...",
"name": "order_shipped",
"language": "en",
"category": "utility",
"status": "approved"
}
],
"count": 1
}/whatsapp-api/accountsList WhatsApp Accounts
Permissions: messages.read
List the vendor's connected Cloud-API numbers — the valid `fromPhoneNo` senders for this key.
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/accounts' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"wabaId": "1111943367030697",
"phoneNumberId": "1169001622966003",
"displayNumber": "+1 555-972-8948",
"verifiedName": "WAapi",
"qualityRating": "GREEN"
}
],
"count": 1
}/whatsapp-api/templates/code/{publicCode}Template Requirements (by public code)
Permissions: templates.read
Introspect an approved template by its WAAPI public code (the 10-char ID shown on each template card). Returns exactly what you must supply to send it — body / header variables ({{1}} or {{name}}), media + location header details, carousel cards as a LIST, the authentication OTP (one code used in BOTH the body and the button), and LTO — plus a copy-paste sample send payload. Case-insensitive; vendor-scoped (a key only sees its own templates).
Note: Auth templates → requirements.authentication.otp { usedIn:["body","button"] }. Carousels → requirements.carousel.cards (a list, each with header/body/buttons). Media headers → requirements.header.media { accepts:["link","id"] }. Location headers → requirements.header.fields.
| Field | Type | Req? | Description |
|---|---|---|---|
| publicCode | path | required | The template public code, e.g. 98QJPN2EG5 (case-insensitive) |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/templates/code/{publicCode}' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": {
"publicCode": "98QJPN2EG5",
"name": "order_update",
"language": "en_US",
"category": "utility",
"subCategory": "standard",
"status": "approved",
"parameterFormat": "positional",
"requirements": {
"body": {
"format": "positional",
"variables": [
{
"key": "{{1}}",
"name": "1",
"type": "text",
"required": true,
"example": "Ravi"
},
{
"key": "{{2}}",
"name": "2",
"type": "text",
"required": true,
"example": "ORD-12345"
}
]
}
},
"sampleSendPayload": {
"variables": [
"Ravi",
"ORD-12345"
]
}
}
}Error responses (1)
HTTP 404 — Unknown code
{
"success": false,
"error": "Template not found for this public code"
}/whatsapp-api/templates/code/{publicCode}/verifyVerify Template Send Payload (dry-run)
Permissions: templates.read
Dry-run a send WITHOUT sending. Pass the SAME body you would POST to /whatsapp-api/messages/template (variables | template | components). We build it with the exact same engine real sends use, validate it against the template requirements, and return { valid, errors, warnings, components, preview }. Returns 200 when valid, 422 when not — the body always carries the full detail.
Note: Always validate against GET …/templates/code/{publicCode} first. The components returned here are exactly what a real send would dispatch.
| Field | Type | Req? | Description |
|---|---|---|---|
| publicCode | path | required | Template public code |
| variables | string[] | object | optional | Positional values (or a named object) for the body — exactly as for /messages/template. Auth: ["<otp>"]. |
| template | object | optional | Friendly payload { header, body:{variables}, buttons, copy_code, lto } |
| components | array | optional | Raw Meta components (passthrough) — used for carousel / media sends |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/templates/code/{publicCode}/verify' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"variables": [
"Ravi",
"ORD-12345",
"30 June"
]
}'Success response
{
"success": true,
"data": {
"publicCode": "98QJPN2EG5",
"name": "order_update",
"language": "en_US",
"valid": true,
"errors": [],
"warnings": [],
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Ravi"
},
{
"type": "text",
"text": "ORD-12345"
},
{
"type": "text",
"text": "30 June"
}
]
}
],
"preview": {
"renderedBody": "Hi Ravi, your order ORD-12345 ships by 30 June."
}
}
}Error responses (2)
HTTP 422 — Invalid payload
{
"success": false,
"data": {
"valid": false,
"errors": [
{
"field": "body",
"message": "Body needs 3 variable(s) ({{1}}, {{2}}, {{3}}); received 1."
}
]
}
}HTTP 404 — Unknown code
{
"success": false,
"error": "Template not found for this public code"
}Contacts (/contacts)
/contacts4. Update Contact
Permissions: contacts.write
Partial update keyed by phone number. Only the fields you include in `updates` are written — every other field is untouched. Use this for CRM-driven name/email/tag sync without risking data loss.
Note: Use POST /contacts instead when you want create-or-update (upsert). PATCH only updates existing rows.
| Field | Type | Req? | Description |
|---|---|---|---|
| phoneNumber | string | required | Phone in any format (we normalize) |
| countryCode | string | optional | Defaults to +91 if absent |
| updates.first_name | string | optional | |
| updates.last_name | string | optional | |
| updates.profile_name | string | optional | WhatsApp display name override |
| updates.email | string | optional | |
| updates.tags | string[] | optional | Replaces the existing tag list |
Request
curl --request PATCH \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/contacts' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"phoneNumber": "917890000198",
"countryCode": "+91",
"updates": {
"first_name": "Sayantan",
"last_name": "Kar",
"tags": [
"vip",
"crm-sync"
]
}
}'Success response
{
"success": true,
"data": {
"_id": "6a2c5...",
"phoneNumber": "917890000198",
"first_name": "Sayantan",
"last_name": "Kar",
"tags": [
"vip",
"crm-sync"
]
}
}Error responses (2)
HTTP 404 — Not found
{
"success": false,
"error": "Contact not found"
}HTTP 400 — Nothing to update
{
"success": false,
"error": "No updatable fields provided"
}/contacts6. Create or Update Contact (upsert)
Permissions: contacts.write
Create-or-update a contact keyed by phone number. Inserts a new row if none exists, otherwise updates only the fields you pass (same dynamic-set rule as PATCH — omitted fields are never stomped). Returns 201 on insert, 200 on update.
Note: Use PATCH /contacts (above) when you only ever want to update an EXISTING row (404 if missing).
| Field | Type | Req? | Description |
|---|---|---|---|
| phoneNumber | string | required | Phone in any format (normalized server-side) |
| countryCode | string | optional | Defaults to +91 if absent |
| first_name | string | optional | |
| last_name | string | optional | |
| profile_name | string | optional | WhatsApp display name override |
| string | optional | ||
| tags | string[] | optional | Replaces the existing tag list |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/contacts' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"phoneNumber": "917890000198",
"countryCode": "+91",
"first_name": "Sayantan",
"email": "sayantan@example.com",
"tags": [
"new-lead"
]
}'Success response
{
"success": true,
"data": {
"_id": "6a2c5...",
"phoneNumber": "917890000198",
"first_name": "Sayantan",
"email": "sayantan@example.com",
"tags": [
"new-lead"
],
"source": "api"
}
}/contacts?search=sayantan&limit=507. List Contacts
Permissions: contacts.read
List the vendor's contacts, newest-active first. Optional `search` matches phone / profile / first / last name. `limit` caps at 200.
| Field | Type | Req? | Description |
|---|---|---|---|
| search | query | optional | Case-insensitive match on phone / name fields |
| limit | query | optional | Max rows (default 50, max 200) |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/contacts?search=sayantan&limit=50' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"_id": "6a2c5...",
"phoneNumber": "917890000198",
"first_name": "Sayantan",
"tags": [
"vip"
]
}
],
"count": 1
}WhatsApp QR — Send endpoints (/whatsapp-qr/*)
Send through a connected QR linked-device session. Requires an API key with the WhatsApp QR service enabled. A QR "number" is a session, not a Meta phoneNumberId — select it with sessionId or fromPhoneNo.
/whatsapp-qr/messages/textQR · Send Text
Permissions: messages.send
Send a plain text message from a connected WhatsApp QR session. Requires a key with the WhatsApp QR service enabled. No 24-hour customer-service-window restriction applies.
Note: Variants: /whatsapp-qr/messages/image, /video, /audio, /document, /location (use `link` for the media URL).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 (no @s.whatsapp.net needed) |
| body | string | required | Message text |
| fromPhoneNo | string | required | E.164 of the connected QR number to send FROM (matches a session). Required unless you pass `sessionId`. |
| sessionId | string | optional | Explicit QR session id — an alternative to fromPhoneNo for choosing the sending session. |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-qr/messages/text' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"body": "Hello from a WAAPI QR session."
}'Success response
{
"success": true,
"data": {
"messageId": "BAE5...",
"from": "917890000199",
"sessionId": "sess_abc",
"to": "917890000198@s.whatsapp.net",
"type": "text"
}
}Error responses (2)
HTTP 400 — No session
{
"success": false,
"error": "No connected WhatsApp QR session found. Connect a device first."
}HTTP 403 — Wrong service
{
"success": false,
"message": "This API key is not authorized for the WhatsApp QR service"
}/whatsapp-qr/messages/imageQR · Send Image
Permissions: messages.send
Send an image from a connected QR session by public URL.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| link | string | required | Public HTTPS URL of the image |
| caption | string | optional | Optional caption |
| fromPhoneNo | string | required | E.164 of the QR number to send FROM. Required unless you pass `sessionId`. |
Request
curl --request POST \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-qr/messages/image' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"to": "917890000198",
"fromPhoneNo": "+917890000199",
"link": "https://example.com/order.jpg",
"caption": "Your order"
}'Success response
{
"success": true,
"data": {
"messageId": "BAE5...",
"sessionId": "sess_abc",
"type": "image"
}
}/whatsapp-qr/sessionsQR · List Sessions
Permissions: messages.read
List the vendor's connected QR sessions so you can discover the numbers you can send from.
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-qr/sessions' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"sessionId": "sess_abc",
"sessionName": "Primary",
"phoneNumber": "917890000199",
"status": "connected"
}
],
"count": 1
}/whatsapp-qr/messages?contactPhone=917890000198&limit=50QR · List Messages
Permissions: messages.read
List messages from your QR sessions, newest-first. Same filters as the Cloud-API list (conversationId / contactPhone / before / after / limit).
| Field | Type | Req? | Description |
|---|---|---|---|
| contactPhone | query | optional | Restrict to messages to/from this number |
| limit | query | optional | Max rows (default 50, max 200) |
Request
curl --request GET \
--url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-qr/messages?contactPhone=917890000198&limit=50' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET'Success response
{
"success": true,
"data": [
{
"_id": "6a2d...",
"messageId": "BAE5...",
"direction": "outgoing",
"type": "text",
"status": "sent"
}
],
"count": 1
}Webhooks
Configure a Webhook URL on an API key and the platform forwards every matching event it receives from Meta — incoming messages, delivery/read/failed status updates, and template status changes — to that URL. The payload is Meta's native envelope ({ object, entry:[{ id, changes:[…] }] }), untransformed. Each key is one webhook; create more keys to fan the same events out to unlimited endpoints. Pick which events a key receives in the panel (no selection = all events).
Signature. Every delivery carries X-Hub-Signature-256: sha256=<hmac> — HMAC-SHA-256 of the raw body using your key's webhookVerifyToken (shown once at create). Verify it before trusting the payload. Deliveries also carry X-WAAPI-Event (the event type, e.g. messages) and X-WAAPI-Delivery (a unique id). ACK with any 2xx; non-2xx / timeouts are retried with backoff (5xx/408/429 only). Use the Send test event button on a key to verify your endpoint.
Set up a webhook
- Create (or edit) an API key and set its Webhook URL to your HTTPS endpoint.
- Copy the
webhookVerifyTokenshown once at create — it is your HMAC secret. - Pick which events the key receives (no selection = all events).
- Click Send test event to confirm your endpoint returns a
2xx.
Delivery headers
| Header | Value |
|---|---|
| X-Hub-Signature-256 | sha256=<hmac> — HMAC-SHA-256 of the raw body, keyed by webhookVerifyToken |
| X-WAAPI-Event | Comma-separated event types in this delivery (e.g. messages, message_status) |
| X-WAAPI-Delivery | Unique delivery id (stable across retries of the same event) |
| User-Agent | WAAPI-Webhook/1.0 |
Event types (subscribe per key)
messages— inbound messages from customers (text, media, interactive replies…)message_status— delivery receipts (sent / delivered / read / failed) for messages you sentmessage_template_status_update— a template was approved / rejected / paused by Meta
Verify the signature (Node.js / Express)
const crypto = require('crypto');
// Verify against the RAW body, so capture it as a Buffer (not parsed JSON).
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const sig = req.get('X-Hub-Signature-256') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WAAPI_WEBHOOK_VERIFY_TOKEN)
.update(req.body) // req.body is the raw Buffer
.digest('hex');
const valid = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!valid) return res.sendStatus(401); // reject forgeries
const payload = JSON.parse(req.body.toString());
// payload.entry[0].changes[0] holds the Meta-native event
res.sendStatus(200); // ACK within ~5s, then process async
});Delivery, retries & limits
- ACK with any
2xxwithin ~10s. A timeout or a5xx/408/429is retried up to 3 times with backoff (1s, 5s, 15s); any other4xxstops immediately. - Each event has a stable
X-WAAPI-Deliveryid across its retries — use it to dedupe. - Payloads above 1 MB are skipped. One webhook URL per key — add more keys to fan the same events out to more endpoints. Pause a key's deliveries any time with its enable toggle.
{your-callback-url}Incoming Text Message
Delivered when a customer sends you a text message. Acknowledge with HTTP 200 within 5 seconds (Meta retries with exponential backoff if you don't).
| Field | Type | Req? | Description |
|---|---|---|---|
| object | string | required | Always 'whatsapp_business_account' |
| entry[].id | string | required | WABA id |
| entry[].changes[].value.messages[] | array | required | The actual messages |
| entry[].changes[].value.metadata.phone_number_id | string | required | Which of your numbers received the message |
| entry[].changes[].value.contacts[] | array | required | Sender profile (name, wa_id) |
Request
curl --request POST \
--url '{your-callback-url}' \
--header 'X-Hub-Signature-256: sha256=<hmac of the raw body, keyed by your webhookVerifyToken>' \
--header 'X-WAAPI-Event: messages' \
--header 'X-WAAPI-Delivery: <unique-delivery-id>' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"contacts": [
{
"profile": {
"name": "Sayantan Kar"
},
"wa_id": "917890000198"
}
],
"messages": [
{
"from": "917890000198",
"id": "wamid.HBgMOTE4NTk3NDA2Njk0FQIAEhgUM0EwMzQy...",
"timestamp": "1718635789",
"text": {
"body": "Hi, is the order shipped?"
},
"type": "text"
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Incoming Image (with caption)
Delivered when a customer sends a picture. Use `image.id` + GET /media to download bytes.
Request
curl --request POST \
--url '{your-callback-url}' \
--header 'X-Hub-Signature-256: sha256=<hmac of the raw body, keyed by your webhookVerifyToken>' \
--header 'X-WAAPI-Event: messages' \
--header 'X-WAAPI-Delivery: <unique-delivery-id>' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"contacts": [
{
"profile": {
"name": "Sayantan Kar"
},
"wa_id": "917890000198"
}
],
"messages": [
{
"from": "917890000198",
"id": "wamid.HBgMO...",
"timestamp": "1718635892",
"type": "image",
"image": {
"caption": "Is this the right colour?",
"mime_type": "image/jpeg",
"sha256": "A2k...",
"id": "1234567890123456"
}
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Status — Delivered (with pricing + conversation)
Delivered when Meta finishes routing a message to the recipient device. Includes the `conversation` block (24-hour billing window id) and the `pricing` block (category Meta charged at).
| Field | Type | Req? | Description |
|---|---|---|---|
| statuses[].id | string | required | The wamid you got from our send response |
| statuses[].status | string | required | 'sent' | 'delivered' | 'read' | 'failed' |
| statuses[].conversation.id | string | optional | Meta 24h conversation id (CBP) |
| statuses[].conversation.origin.type | string | optional | marketing | utility | authentication | service |
| statuses[].pricing | object | optional | billable, pricing_model, category |
Request
curl --request POST \
--url '{your-callback-url}' \
--header 'X-Hub-Signature-256: sha256=<hmac of the raw body, keyed by your webhookVerifyToken>' \
--header 'X-WAAPI-Event: message_status' \
--header 'X-WAAPI-Delivery: <unique-delivery-id>' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"statuses": [
{
"id": "wamid.HBgMOTE4NTk3NDA2Njk0FQIAEhgUM0EwMzQy...",
"status": "delivered",
"timestamp": "1718635790",
"recipient_id": "917890000198",
"conversation": {
"id": "7e8d9f3a1c2b4d5e8f9a0b1c2d3e4f5a",
"expiration_timestamp": "1718722190",
"origin": {
"type": "marketing"
}
},
"pricing": {
"billable": true,
"pricing_model": "CBP",
"category": "marketing",
"type": "regular"
}
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Status — Failed
Delivered when a message could not be delivered. `errors[]` has Meta's error code + reason.
Request
curl --request POST \
--url '{your-callback-url}' \
--header 'X-Hub-Signature-256: sha256=<hmac of the raw body, keyed by your webhookVerifyToken>' \
--header 'X-WAAPI-Event: message_status' \
--header 'X-WAAPI-Delivery: <unique-delivery-id>' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"statuses": [
{
"id": "wamid.HBgMO...",
"status": "failed",
"timestamp": "1718635800",
"recipient_id": "917890000198",
"errors": [
{
"code": 131026,
"title": "Message undeliverable",
"error_data": {
"details": "Recipient phone number does not have WhatsApp installed"
}
}
]
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Template status update (approved / rejected)
Delivered when one of your message templates changes review state in Meta (APPROVED / REJECTED / FLAGGED / PAUSED / PENDING_DELETION). Subscribe a key to message_template_status_update (or leave its events empty = all). Use it to flip a template "live" in your own system the instant Meta approves it. Header X-WAAPI-Event: message_template_status_update.
| Field | Type | Req? | Description |
|---|---|---|---|
| value.event | string | required | APPROVED | REJECTED | FLAGGED | PAUSED | PENDING_DELETION |
| value.message_template_id | number | required | Meta template id |
| value.message_template_name | string | required | Template name |
| value.message_template_language | string | required | Template language code |
| value.reason | string|null | optional | Rejection / flag reason (null when approved) |
Request
curl --request POST \
--url '{your-callback-url}' \
--header 'X-Hub-Signature-256: sha256=<hmac of the raw body, keyed by your webhookVerifyToken>' \
--header 'X-WAAPI-Event: message_template_status_update' \
--header 'X-WAAPI-Delivery: <unique-delivery-id>' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "message_template_status_update",
"value": {
"event": "APPROVED",
"message_template_id": 1071234567890123,
"message_template_name": "order_shipped",
"message_template_language": "en_US",
"reason": null
}
}
]
}
]
}'Success response
{
"ok": true
}Downloads
Everything on this page is also available to take with you for a 3rd-party integration.
Markdown reference
A single self-contained .md file documenting every endpoint, field, request/response, error, and the full webhook spec — drop it into your repo or hand it to an integrator / an AI assistant to generate client code.
Postman collection (v2.1)
The full collection covers every endpoint plus the webhook examples. Import into Postman, set the baseUrl, apiKey, and apiSecret collection variables, and run.