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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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/typing

Send Typing Indicator

Permissions: messages.send

Show the customer a "typing…" indicator in response to a message they sent you — and mark that message read (blue ticks). Anchor it on the inbound `messageId` (wamid). WhatsApp shows "typing…" until you send your reply, or auto-dismisses it after 25 seconds. There is no `to`: the recipient is whoever sent `messageId`.

Note: Only valid in response to a received message, inside the 24h customer-service window. Best practice: trigger it only when you are actually about to reply, and don't re-fire it repeatedly.

FieldTypeReq?Description
fromPhoneNostringrequiredE.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. This endpoint has no `to`; the recipient is whoever sent `messageId`.
messageIdstringrequiredwamid of the customer's inbound message to respond to (the indicator shows on that chat)

Request

curl --request POST \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/whatsapp-api/messages/typing' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
  "fromPhoneNo": "+917890000199",
  "messageId": "wamid.HBgM..."
}'

Success response

{
  "success": true,
  "data": {
    "typing": true,
    "messageId": "wamid.HBgM..."
  }
}
Error responses (3)

HTTP 400Missing messageId

{
  "success": false,
  "error": "messageId (the inbound message wamid to respond to) is required"
}

HTTP 400Outside window / stale wamid

{
  "success": false,
  "error": "(#131047) Re-engagement message: outside 24h window"
}

HTTP 403Wrong scope

{
  "success": false,
  "message": "API key scope 'read' does not permit send actions"
}
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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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?labels=VIP&excludeLabels=Cold&limit=50

List Conversations

Permissions: conversations.read

List conversations newest-activity-first. Optional `status` filter (open / resolved / closed). Filter by colored labels AND tags with include/exclude + any/all — same params as List Contacts. `limit` caps at 200.

FieldTypeReq?Description
statusqueryoptionalFilter by conversation status
labelsqueryoptionalComma-separated label names to INCLUDE
labelMatchqueryoptional`any` (default) or `all`
excludeLabelsqueryoptionalComma-separated label names to EXCLUDE
tagsqueryoptionalComma-separated tags to INCLUDE
excludeTagsqueryoptionalComma-separated tags to EXCLUDE
limitqueryoptionalMax rows (default 50, max 200)

Request

curl --request GET \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/whatsapp-api/conversations?labels=VIP&excludeLabels=Cold&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",
      "labels": [
        {
          "name": "VIP",
          "color": "#FF0000"
        }
      ],
      "lastMessageAt": "2026-06-17T12:00:00.000Z",
      "unreadCount": 0
    }
  ],
  "count": 1
}
GET/logs?limit=50

List API Call Logs

Permissions: logs.read

List this vendor's external API call history (method, endpoint, status, duration, IP, timestamp) newest-first. Filter by date range, endpoint, method, or status code. Rows auto-expire after 90 days.

FieldTypeReq?Description
startDatequeryoptionalISO date — only calls on/after this
endDatequeryoptionalISO date — only calls on/before this
endpointqueryoptionalSubstring match on the endpoint path
methodqueryoptionalHTTP method filter (GET/POST/…)
statusCodequeryoptionalExact HTTP status filter
limitqueryoptionalMax rows (default 50, max 200)
pagequeryoptionalPage number (1-based)

Request

curl --request GET \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/logs?limit=50' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET'

Success response

{
  "success": true,
  "data": [
    {
      "requestId": "a1b2...",
      "request": {
        "method": "POST",
        "endpoint": "/api/v1/external/contacts/labels/apply"
      },
      "response": {
        "statusCode": 200,
        "success": true
      },
      "timing": {
        "duration": 42,
        "timestamp": "2026-07-16T12:00:00.000Z"
      }
    }
  ],
  "count": 1,
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "pages": 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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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?labels=VIP&excludeLabels=Cold&limit=50

7. List Contacts

Permissions: contacts.read

List the vendor's contacts, newest-active first. Optional `search` matches phone / profile / first / last name. Filter by colored labels AND free-text tags, with include/exclude and any/all semantics. `limit` caps at 200.

FieldTypeReq?Description
searchqueryoptionalCase-insensitive match on phone / name fields
labelsqueryoptionalComma-separated label names to INCLUDE (e.g. `VIP,Lead`)
labelMatchqueryoptional`any` (default, OR) or `all` (AND) for the included labels
excludeLabelsqueryoptionalComma-separated label names to EXCLUDE (none-of)
tagsqueryoptionalComma-separated tags to INCLUDE
tagMatchqueryoptional`any` (default) or `all` for the included tags
excludeTagsqueryoptionalComma-separated tags to EXCLUDE
matchqueryoptionalShared default (`any`|`all`) applied to labels + tags when the specific one is omitted
limitqueryoptionalMax rows (default 50, max 200)

Request

curl --request GET \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts?labels=VIP&excludeLabels=Cold&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"
      ],
      "labels": [
        {
          "name": "VIP",
          "color": "#FF0000"
        }
      ]
    }
  ],
  "count": 1
}
GET/contacts/labels

8. List Labels (catalog)

Permissions: labels.read

List your reusable label catalog ({name, color}). Labels are what the include/exclude filters on contacts + conversations match against.

Request

curl --request GET \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts/labels' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET'

Success response

{
  "success": true,
  "data": [
    {
      "_id": "6a3...",
      "name": "VIP",
      "color": "#FF0000"
    }
  ],
  "count": 1
}
POST/contacts/labels

9. Create Label

Permissions: labels.write

Create a catalog label. Names are unique per vendor (case-insensitive). `color` is optional (defaults to the brand green).

FieldTypeReq?Description
namestringrequiredLabel name (unique per vendor)
colorstringoptionalHex text color, e.g. #FF0000

Request

curl --request POST \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts/labels' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "VIP",
  "color": "#FF0000"
}'

Success response

{
  "success": true,
  "data": {
    "name": "VIP",
    "color": "#FF0000"
  }
}
PATCH/contacts/labels

10. Rename / Recolor Label

Permissions: labels.write

Rename and/or recolor a label. The change CASCADES to every contact + conversation the label is on, so chips stay in sync.

FieldTypeReq?Description
namestringrequiredCurrent label name
newNamestringoptionalNew name (omit to keep)
colorstringoptionalNew hex color (omit to keep)

Request

curl --request PATCH \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts/labels' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "VIP",
  "newName": "VIP Gold",
  "color": "#DAA520"
}'

Success response

{
  "success": true,
  "data": {
    "name": "VIP Gold",
    "color": "#DAA520",
    "cascaded": {
      "contacts": 12,
      "conversations": 3
    }
  }
}
DELETE/contacts/labels

11. Delete Label

Permissions: labels.write

Delete a label from the catalog AND strip it off every contact + conversation. Fires a `label_removed` webhook.

FieldTypeReq?Description
namestringrequiredLabel name to delete

Request

curl --request DELETE \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts/labels' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Cold"
}'

Success response

{
  "success": true,
  "data": {
    "name": "Cold",
    "cascaded": {
      "contacts": 5,
      "conversations": 0
    }
  }
}
POST/contacts/labels/apply

12. Apply Labels (bulk)

Permissions: labels.write

Add, remove, or replace labels on ONE or MANY contacts OR conversations in a single call. `set` replaces the whole label list; otherwise `add` + `remove` are applied. Unknown label names are auto-created in the catalog. Label names may contain DATE TOKENS that expand server-side: `{today}`, `{today+3}` / `{day-1}` (N days after/before today), and an optional format `{today:DD-MM-YYYY}` — e.g. "Lead {today}" becomes "Lead 2026-07-16". Fires `label_added` / `label_removed` webhooks.

FieldTypeReq?Description
targetstringrequired'contact' or 'conversation'
idsstring[]requiredOne or more contact/conversation _ids
addstring[]optionalLabel names to add
removestring[]optionalLabel names to remove
setstring[]optionalReplace ALL labels with these (overrides add/remove)

Request

curl --request POST \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts/labels/apply' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET' \
  --header 'Content-Type: application/json' \
  --data '{
  "target": "contact",
  "ids": [
    "6a2c5...",
    "6a2c6..."
  ],
  "add": [
    "VIP",
    "Lead"
  ],
  "remove": [
    "Cold"
  ]
}'

Success response

{
  "success": true,
  "data": {
    "matched": 2,
    "add": [
      {
        "name": "VIP",
        "color": "#FF0000"
      },
      {
        "name": "Lead",
        "color": "#115D33"
      }
    ],
    "removed": [
      "Cold"
    ],
    "set": null
  }
}
GET/contacts/phone-list?labels=VIP&excludeLabels=Cold

13. Phone List by Label

Permissions: contacts.read

Return just the E.164 phone numbers for a label/tag selection — built for campaign targeting. Same include/exclude params as List Contacts. Opted-out numbers are excluded unless `includeOptedOut=true`.

FieldTypeReq?Description
labelsqueryoptionalComma-separated label names to INCLUDE
excludeLabelsqueryoptionalComma-separated label names to EXCLUDE
tagsqueryoptionalComma-separated tags to INCLUDE
excludeTagsqueryoptionalComma-separated tags to EXCLUDE
matchqueryoptional`any` (default) or `all`
includeOptedOutqueryoptionalSet `true` to include opted-out numbers

Request

curl --request GET \
  --url 'https://vendor.waapi.easycrm4u.com/api/v1/external/contacts/phone-list?labels=VIP&excludeLabels=Cold' \
  --header 'X-API-Key: waapi_YOUR_API_KEY' \
  --header 'X-API-Secret: secr_YOUR_API_SECRET'

Success response

{
  "success": true,
  "data": {
    "phones": [
      "917890000198",
      "917890000199"
    ],
    "count": 2
  }
}

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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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://vendor.waapi.easycrm4u.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
}
POST{your-callback-url}

Label added / removed (app event)

A WAAPI-native APP event (NOT from Meta) delivered when a label is applied to or removed from a contact or conversation — from the UI, the API, a bot-flow Label node, or a catalog delete. Subscribe a key to `label_added` and/or `label_removed` (or leave its events empty = all). The envelope object is `waapi_app_event` (Meta events use `whatsapp_business_account`) so you can tell them apart. Header X-WAAPI-Event: label_added | label_removed. Signed with X-Hub-Signature-256 exactly like Meta events.

FieldTypeReq?Description
objectstringrequiredAlways 'waapi_app_event' for app events
eventstringrequiredlabel_added | label_removed
entry[].data.targetstringrequired'contact' | 'conversation' (or 'contact,conversation' from a flow node)
entry[].data.idsstring[]requiredThe affected contact/conversation ids
entry[].data.labelsstring[]requiredThe label names added or removed
entry[].data.sourcestringoptional'api' | 'bot_flow' | omitted (UI)

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": "waapi_app_event",
  "event": "label_added",
  "entry": [
    {
      "vendorId": "6a347b55e9cbc0e0b5b90738",
      "time": 1784000000,
      "data": {
        "source": "api",
        "target": "contact",
        "ids": [
          "6a2c5...",
          "6a2c6..."
        ],
        "labels": [
          "VIP",
          "Lead"
        ]
      }
    }
  ]
}'

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