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.

GET/ping

Authentication

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.

FieldTypeReq?Description
X-API-KeyheaderrequiredPublic API key, starts with `waapi_`
X-API-SecretheaderrequiredSecret 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 401Missing API key

{
  "success": false,
  "message": "API key required (Authorization: Bearer <waapi_… key or JWT> or X-API-Key header)"
}

HTTP 401Missing secret

{
  "success": false,
  "message": "API secret required (X-API-Secret header)"
}

HTTP 401Invalid 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"
}
StatusMeaning
400Bad request — body validation failed or Meta returned an error code
401Auth — missing/invalid X-API-Key or X-API-Secret
403Forbidden — key lacks the required permission or scope
404Not found — resource (contact, message, template) does not exist
409Conflict — duplicate or state-incompatible
429Rate limited — slow down to your key's per-minute/hour quota
500Server error — please retry; if persistent, open a ticket with the response body

Cloud API — Send endpoints (/whatsapp-api/*)

POST/whatsapp-api/messages/template

1. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164 (e.g. 917890000198)
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
namestringoptionalTemplate name (case-sensitive). Provide EITHER name+language OR templateId.
templateIdstringoptionalLocal template id — an alternative to name+language (language is taken from the stored template).
languagestringoptionalBCP-47 language code (e.g. en, en_US, hi). Required when you pass name.
variablesstring[]optionalPositional 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"].
componentsarrayoptionalMeta-shaped components (header / body / button). Use this for LTO and any parameter that is not a plain positional body value.
templateobjectoptionalFriendly payload shape ({ header, body:{ variables }, buttons, copy_code, lto }) — projected to Meta components server-side.
replyToWaMessageIdstringoptionalQuote 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 400Template not found

{
  "success": false,
  "error": "(#132001) Template name does not exist in the translation"
}

HTTP 403Wrong scope

{
  "success": false,
  "message": "API key scope 'read' does not permit send actions"
}
POST/whatsapp-api/messages/buttons

2. Send Interactive Message (Buttons)

Permissions: messages.send

Send a quick-reply button message — up to 3 buttons. Customer taps a button → you receive a button_reply webhook with the button id you assigned.

Note: Text fields accept body/header/footer (canonical) or bodyText/headerText/footerText (aliases). header/footer may be a plain string. Variants: /messages/list (sectioned list), /messages/cta (call-to-action URL), /messages/flow (Flow embed).

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
bodystringrequiredMain message body (1024 char max). Alias: bodyText.
buttonsarray<{id,title}>required1–3 buttons. id ≤ 256 chars; title ≤ 20 chars.
headerstringoptionalOptional header line. Alias: headerText.
footerstringoptionalOptional footer line. Alias: footerText.

Request

curl --request POST \
  --url 'https://cname.lamda.thewaapi.com/api/v1/external/whatsapp-api/messages/buttons' \
  --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",
  "header": "Order #1234",
  "body": "Your order is ready. What would you like to do?",
  "footer": "WAAPI demo",
  "buttons": [
    {
      "id": "track",
      "title": "Track"
    },
    {
      "id": "cancel",
      "title": "Cancel"
    },
    {
      "id": "support",
      "title": "Talk to support"
    }
  ]
}'

Success response

{
  "success": true,
  "data": {
    "messageId": "wamid.HBgM...",
    "status": "sent",
    "to": "917890000198",
    "type": "interactive"
  }
}
Error responses (1)

HTTP 400Too many buttons

{
  "success": false,
  "error": "(#100) buttons must be 1-3 items"
}
POST/whatsapp-api/messages/text

3a. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
bodystringrequiredMessage text (4096 char max)
previewUrlbooleanoptionalIf 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 400Outside service window

{
  "success": false,
  "error": "(#131047) Re-engagement message: outside 24h window — send a template instead"
}
POST/whatsapp-api/messages/image

3b. 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).

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
linkstringoptionalPublic HTTPS URL of a JPG/PNG (≤5 MB)
mediaIdstringoptionalMeta media_id from /media upload (≤30 days)
captionstringoptionalOptional 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"
  }
}
POST/whatsapp-api/messages/document

3c. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
linkstringoptionalPublic HTTPS URL of the file
mediaIdstringoptionalMeta media_id from /media upload
filenamestringoptionalDisplay filename shown to the recipient
captionstringoptionalOptional 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"
  }
}
POST/whatsapp-api/messages/video

3d. Send Video

Permissions: messages.send

Send a video by public URL (`link`) or a previously uploaded Meta media id (`mediaId`). mp4/3gp, ≤16 MB.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
linkstringoptionalPublic HTTPS URL of the video
mediaIdstringoptionalMeta media_id from a prior upload
captionstringoptionalOptional 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"
  }
}
POST/whatsapp-api/messages/audio

3e. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
linkstringoptionalPublic HTTPS URL of the audio file
mediaIdstringoptionalMeta 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"
  }
}
POST/whatsapp-api/messages/sticker

3f. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
linkstringoptionalPublic HTTPS URL of the .webp sticker
mediaIdstringoptionalMeta 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"
  }
}
POST/whatsapp-api/messages/location

3g. Send Location

Permissions: messages.send

Send a map pin. latitude + longitude are required; name + address are optional labels shown on the pin.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
latitudenumber|stringrequiredDecimal latitude, e.g. 22.5726
longitudenumber|stringrequiredDecimal longitude, e.g. 88.3639
namestringoptionalPlace name label
addressstringoptionalAddress 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"
  }
}
POST/whatsapp-api/messages/contact

3h. Send Contact (vCard)

Permissions: messages.send

Share one or more contact cards. `contacts` is the Meta contacts array (name + phones, optional emails/org).

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
contactsarrayrequiredMeta-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"
  }
}
POST/whatsapp-api/messages/reaction

3i. Send Reaction

Permissions: messages.send

React to a message with an emoji. Send an empty `emoji` ("") to REMOVE a previously sent reaction.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
messageIdstringrequiredwamid of the message you are reacting to
emojistringrequiredA 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"
  }
}
POST/whatsapp-api/messages/list

3j. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
bodystringrequiredMain message body
buttonTextstringrequiredLabel of the button that opens the list (≤20 chars)
sectionsarrayrequiredsections[]: { title, rows:[{ id, title, description? }] }
headerstringoptionalOptional header text
footerstringoptionalOptional 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"
  }
}
POST/whatsapp-api/messages/cta

3k. 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).

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
bodystringrequiredMain message body
ctaTextstringrequiredButton label (≤20 chars)
ctaUrlstringrequiredHTTPS URL the button opens
headerstringoptionalOptional header text
footerstringoptionalOptional 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"
  }
}
POST/whatsapp-api/messages/flow

3l. 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).

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
bodystringrequiredMain message body
flowIdstringrequiredPublished Flow id
flowCtastringrequiredCTA button label (≤20 chars)
flowTokenstringoptionalOpaque token echoed back on completion
flowActionstringoptional'navigate' (default) or 'data_exchange'
flowActionPayloadobjectoptionalREQUIRED by Meta when flowAction is 'navigate': { screen, data? } naming the entry screen.
modestringoptional'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"
  }
}
POST/whatsapp-api/messages/raw

3n. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
bodyobjectrequiredThe 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"
  }
}
POST/whatsapp-api/media

3o. 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>" } }].

FieldTypeReq?Description
filemultipartoptionalThe media file (multipart/form-data field `file`). Use this OR `link`.
linkstringoptionalA public HTTPS URL to the media — we fetch + upload it. Use this OR a multipart `file`.
fromPhoneNostringoptionalUpload under this connected number's WABA (defaults to your default number).
filenamestringoptionalDisplay 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 400No media

{
  "success": false,
  "error": "Provide a multipart `file` OR a public `link` (https URL) to upload."
}

HTTP 502Upload failed

{
  "success": false,
  "error": "Upload to WhatsApp failed"
}
POST/whatsapp-api/messages/send

5. 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.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`.
countryCodestringoptionalDefaults to +91
contactobjectoptionalSame shape as POST /contacts (first_name, last_name, email, tags…). Pass an empty {} to skip upsert and only send.
typeenumrequiredOne of: 'text' | 'image' | 'video' | 'audio' | 'document' | 'template' | 'buttons' | 'list' | 'cta'
payloadobjectrequiredType-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 403Missing perm

{
  "success": false,
  "error": "API key needs 'contacts.write' for combined send+upsert"
}

HTTP 400Bad type

{
  "success": false,
  "error": "Unsupported type 'sticker-pack'"
}

Cloud API — Read endpoints (/whatsapp-api/*)

GET/whatsapp-api/messages?contactPhone=917890000198&limit=50

List Messages

Permissions: messages.read

List messages newest-first. Filter by `conversationId`, `contactPhone`, and a `before`/`after` ISO timestamp window. `limit` caps at 200.

FieldTypeReq?Description
conversationIdqueryoptionalRestrict to one conversation
contactPhonequeryoptionalRestrict to messages to/from this number
beforequeryoptionalISO timestamp — only messages older than this
afterqueryoptionalISO timestamp — only messages newer than this
limitqueryoptionalMax 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
}
GET/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).

FieldTypeReq?Description
idpathrequiredThe 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 404Not found

{
  "success": false,
  "error": "Not found"
}
GET/whatsapp-api/conversations?status=open&limit=50

List Conversations

Permissions: conversations.read

List conversations newest-activity-first. Optional `status` filter (open / resolved / closed). `limit` caps at 200.

FieldTypeReq?Description
statusqueryoptionalFilter by conversation status
limitqueryoptionalMax 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
}
GET/whatsapp-api/templates

List 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
}
GET/whatsapp-api/accounts

List 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
}
GET/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.

FieldTypeReq?Description
publicCodepathrequiredThe 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 404Unknown code

{
  "success": false,
  "error": "Template not found for this public code"
}
POST/whatsapp-api/templates/code/{publicCode}/verify

Verify 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.

FieldTypeReq?Description
publicCodepathrequiredTemplate public code
variablesstring[] | objectoptionalPositional values (or a named object) for the body — exactly as for /messages/template. Auth: ["<otp>"].
templateobjectoptionalFriendly payload { header, body:{variables}, buttons, copy_code, lto }
componentsarrayoptionalRaw 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 422Invalid payload

{
  "success": false,
  "data": {
    "valid": false,
    "errors": [
      {
        "field": "body",
        "message": "Body needs 3 variable(s) ({{1}}, {{2}}, {{3}}); received 1."
      }
    ]
  }
}

HTTP 404Unknown code

{
  "success": false,
  "error": "Template not found for this public code"
}

Contacts (/contacts)

PATCH/contacts

4. 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.

FieldTypeReq?Description
phoneNumberstringrequiredPhone in any format (we normalize)
countryCodestringoptionalDefaults to +91 if absent
updates.first_namestringoptional
updates.last_namestringoptional
updates.profile_namestringoptionalWhatsApp display name override
updates.emailstringoptional
updates.tagsstring[]optionalReplaces 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 404Not found

{
  "success": false,
  "error": "Contact not found"
}

HTTP 400Nothing to update

{
  "success": false,
  "error": "No updatable fields provided"
}
POST/contacts

6. 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).

FieldTypeReq?Description
phoneNumberstringrequiredPhone in any format (normalized server-side)
countryCodestringoptionalDefaults to +91 if absent
first_namestringoptional
last_namestringoptional
profile_namestringoptionalWhatsApp display name override
emailstringoptional
tagsstring[]optionalReplaces 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"
  }
}
GET/contacts?search=sayantan&limit=50

7. 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.

FieldTypeReq?Description
searchqueryoptionalCase-insensitive match on phone / name fields
limitqueryoptionalMax 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.

POST/whatsapp-qr/messages/text

QR · 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).

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164 (no @s.whatsapp.net needed)
bodystringrequiredMessage text
fromPhoneNostringrequiredE.164 of the connected QR number to send FROM (matches a session). Required unless you pass `sessionId`.
sessionIdstringoptionalExplicit 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 400No session

{
  "success": false,
  "error": "No connected WhatsApp QR session found. Connect a device first."
}

HTTP 403Wrong service

{
  "success": false,
  "message": "This API key is not authorized for the WhatsApp QR service"
}
POST/whatsapp-qr/messages/image

QR · Send Image

Permissions: messages.send

Send an image from a connected QR session by public URL.

FieldTypeReq?Description
tostringrequiredRecipient phone in E.164
linkstringrequiredPublic HTTPS URL of the image
captionstringoptionalOptional caption
fromPhoneNostringrequiredE.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"
  }
}
GET/whatsapp-qr/sessions

QR · 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
}
GET/whatsapp-qr/messages?contactPhone=917890000198&limit=50

QR · 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).

FieldTypeReq?Description
contactPhonequeryoptionalRestrict to messages to/from this number
limitqueryoptionalMax 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

  1. Create (or edit) an API key and set its Webhook URL to your HTTPS endpoint.
  2. Copy the webhookVerifyToken shown once at create — it is your HMAC secret.
  3. Pick which events the key receives (no selection = all events).
  4. Click Send test event to confirm your endpoint returns a 2xx.

Delivery headers

HeaderValue
X-Hub-Signature-256sha256=<hmac> — HMAC-SHA-256 of the raw body, keyed by webhookVerifyToken
X-WAAPI-EventComma-separated event types in this delivery (e.g. messages, message_status)
X-WAAPI-DeliveryUnique delivery id (stable across retries of the same event)
User-AgentWAAPI-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 sent
  • message_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 2xx within ~10s. A timeout or a 5xx/408/429 is retried up to 3 times with backoff (1s, 5s, 15s); any other 4xx stops immediately.
  • Each event has a stable X-WAAPI-Delivery id 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.
POST{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).

FieldTypeReq?Description
objectstringrequiredAlways 'whatsapp_business_account'
entry[].idstringrequiredWABA id
entry[].changes[].value.messages[]arrayrequiredThe actual messages
entry[].changes[].value.metadata.phone_number_idstringrequiredWhich of your numbers received the message
entry[].changes[].value.contacts[]arrayrequiredSender 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
}
POST{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
}
POST{your-callback-url}

Incoming Button Reply

Delivered when a customer taps a button on an interactive message you sent.

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": "1718635950",
                "type": "interactive",
                "interactive": {
                  "type": "button_reply",
                  "button_reply": {
                    "id": "track",
                    "title": "Track"
                  }
                },
                "context": {
                  "from": "+91 78900 00199",
                  "id": "wamid.HBgMO_THE_MESSAGE_THAT_HAD_BUTTONS"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}'

Success response

{
  "ok": true
}
POST{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).

FieldTypeReq?Description
statuses[].idstringrequiredThe wamid you got from our send response
statuses[].statusstringrequired'sent' | 'delivered' | 'read' | 'failed'
statuses[].conversation.idstringoptionalMeta 24h conversation id (CBP)
statuses[].conversation.origin.typestringoptionalmarketing | utility | authentication | service
statuses[].pricingobjectoptionalbillable, 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
}
POST{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
}
POST{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.

FieldTypeReq?Description
value.eventstringrequiredAPPROVED | REJECTED | FLAGGED | PAUSED | PENDING_DELETION
value.message_template_idnumberrequiredMeta template id
value.message_template_namestringrequiredTemplate name
value.message_template_languagestringrequiredTemplate language code
value.reasonstring|nulloptionalRejection / 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.

Download WAAPI_External_API.md

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.

Download Vendor_External_API.json