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.betazen.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.
/pingAuthentication
Every request must carry both the X-API-Key header AND the X-API-Secret header. A key is bound to one or more of your WhatsApp numbers; every send must name which one to use via `fromPhoneNo` (E.164), alongside the recipient `to`. The ping endpoint echoes back your vendor id and the name of the key you authenticated with.
| Field | Type | Req? | Description |
|---|---|---|---|
| X-API-Key | header | required | Public API key, starts with `waapi_` |
| X-API-Secret | header | required | Secret token, starts with `secr_`. Required when the key was created with a secret. |
Request
curl --request GET \
--url 'https://cname.betazen.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 401 — Missing API key
{
"success": false,
"message": "API key required (Authorization: Bearer <waapi_… key or JWT> or X-API-Key header)"
}HTTP 401 — Missing secret
{
"success": false,
"message": "API secret required (X-API-Secret header)"
}HTTP 401 — Invalid credential
{
"success": false,
"message": "Invalid API secret"
}Error format
Errors return a non-2xx HTTP status with a JSON body. Validation problems are 400, auth problems 401, missing permissions 403, missing resources 404, conflicts 409, and Meta-side send failures bubble up as 400 with Meta's error code in the message.
{
"success": false,
"error": "(#131026) Recipient phone number does not have WhatsApp installed"
}| Status | Meaning |
|---|---|
| 400 | Bad request — body validation failed or Meta returned an error code |
| 401 | Auth — missing/invalid X-API-Key or X-API-Secret |
| 403 | Forbidden — key lacks the required permission or scope |
| 404 | Not found — resource (contact, message, template) does not exist |
| 409 | Conflict — duplicate or state-incompatible |
| 429 | Rate limited — slow down to your key's per-minute/hour quota |
| 500 | Server error — please retry; if persistent, open a ticket with the response body |
Cloud API — Send endpoints (/whatsapp-api/*)
/whatsapp-api/messages/template1. Send Template Message
Permissions: messages.send
Send an approved WhatsApp template by name. The template must be APPROVED in Meta Business Manager. Pass parameters using the Meta-shaped `components` array.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 (e.g. 917890000198) |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| name | string | required | Template name (case-sensitive) — must match an approved template |
| language | string | required | BCP-47 language code (e.g. en, en_US, hi) |
| components | array | optional | Meta-shaped components: header / body / button (url) for template parameters |
| replyToWaMessageId | string | optional | Quote a prior message (wamid) |
Request
curl --request POST \
--url 'https://cname.betazen.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 400 — Template not found
{
"success": false,
"error": "(#132001) Template name does not exist in the translation"
}HTTP 403 — Wrong scope
{
"success": false,
"message": "API key scope 'read' does not permit send actions"
}/whatsapp-api/messages/text3a. Send Text Message
Permissions: messages.send
Send a plain text message. Outside the 24-hour customer service window only TEMPLATES are allowed; this endpoint will error there.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| body | string | required | Message text (4096 char max) |
| previewUrl | boolean | optional | If true, the first URL in body gets a link preview card |
Request
curl --request POST \
--url 'https://cname.betazen.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 400 — Outside service window
{
"success": false,
"error": "(#131047) Re-engagement message: outside 24h window — send a template instead"
}/whatsapp-api/messages/image3b. Send Image
Permissions: messages.send
Send an image by public URL (`link`) or by a previously uploaded Meta media id (`mediaId`). One of the two is required.
Note: Variants: /whatsapp-api/messages/video (mp4/3gp, ≤16 MB), /messages/audio (aac/mp3/ogg/amr/m4a, ≤16 MB), /messages/sticker (webp).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of a JPG/PNG (≤5 MB) |
| mediaId | string | optional | Meta media_id from /media upload (≤30 days) |
| caption | string | optional | Optional caption (1024 char max) |
Request
curl --request POST \
--url 'https://cname.betazen.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"
}
}/whatsapp-api/messages/document3c. Send Document
Permissions: messages.send
Send any document file (PDF, DOCX, XLSX, CSV, etc.) up to 100 MB. The filename you pass is what the recipient sees in WhatsApp.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| link | string | optional | Public HTTPS URL of the file |
| mediaId | string | optional | Meta media_id from /media upload |
| filename | string | optional | Display filename shown to the recipient |
| caption | string | optional | Optional caption (1024 char max) |
Request
curl --request POST \
--url 'https://cname.betazen.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"
}
}/whatsapp-api/messages/send5. Send Message + Update Contact (combined)
Permissions: messages.sendcontacts.write
Flagship endpoint: atomically upserts the contact AND dispatches ANY supported message type in one request. Saves a network round trip for CRM-style flows.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| fromPhoneNo | string | required | E.164 number to send FROM — one of the WhatsApp numbers your API key is bound to. REQUIRED on every send, alongside `to`. |
| countryCode | string | optional | Defaults to +91 |
| contact | object | optional | Same shape as POST /contacts (first_name, last_name, email, tags…). Pass an empty {} to skip upsert and only send. |
| type | enum | required | One of: 'text' | 'image' | 'video' | 'audio' | 'document' | 'template' | 'buttons' | 'list' | 'cta' |
| payload | object | required | Type-specific payload — same shape as the dedicated /messages/<type> endpoint |
Request
curl --request POST \
--url 'https://cname.betazen.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 403 — Missing perm
{
"success": false,
"error": "API key needs 'contacts.write' for combined send+upsert"
}HTTP 400 — Bad type
{
"success": false,
"error": "Unsupported type 'sticker-pack'"
}Contacts (/contacts)
/contacts4. Update Contact
Permissions: contacts.write
Partial update keyed by phone number. Only the fields you include in `updates` are written — every other field is untouched. Use this for CRM-driven name/email/tag sync without risking data loss.
Note: Use POST /contacts instead when you want create-or-update (upsert). PATCH only updates existing rows.
| Field | Type | Req? | Description |
|---|---|---|---|
| phoneNumber | string | required | Phone in any format (we normalize) |
| countryCode | string | optional | Defaults to +91 if absent |
| updates.first_name | string | optional | |
| updates.last_name | string | optional | |
| updates.profile_name | string | optional | WhatsApp display name override |
| updates.email | string | optional | |
| updates.tags | string[] | optional | Replaces the existing tag list |
Request
curl --request PATCH \
--url 'https://cname.betazen.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 404 — Not found
{
"success": false,
"error": "Contact not found"
}HTTP 400 — Nothing to update
{
"success": false,
"error": "No updatable fields provided"
}WhatsApp QR — Send endpoints (/whatsapp-qr/*)
Send through a connected QR linked-device session. Requires an API key with the WhatsApp QR service enabled. A QR "number" is a session, not a Meta phoneNumberId — select it with sessionId or fromPhoneNo.
/whatsapp-qr/messages/textQR · Send Text
Permissions: messages.send
Send a plain text message from a connected WhatsApp QR session. Requires a key with the WhatsApp QR service enabled. No 24-hour customer-service-window restriction applies.
Note: Variants: /whatsapp-qr/messages/image, /video, /audio, /document, /location (use `link` for the media URL).
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 (no @s.whatsapp.net needed) |
| body | string | required | Message text |
| fromPhoneNo | string | required | E.164 of the connected QR number to send FROM (matches a session). Required unless you pass `sessionId`. |
| sessionId | string | optional | Explicit QR session id — an alternative to fromPhoneNo for choosing the sending session. |
Request
curl --request POST \
--url 'https://cname.betazen.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 400 — No session
{
"success": false,
"error": "No connected WhatsApp QR session found. Connect a device first."
}HTTP 403 — Wrong service
{
"success": false,
"message": "This API key is not authorized for the WhatsApp QR service"
}/whatsapp-qr/messages/imageQR · Send Image
Permissions: messages.send
Send an image from a connected QR session by public URL.
| Field | Type | Req? | Description |
|---|---|---|---|
| to | string | required | Recipient phone in E.164 |
| link | string | required | Public HTTPS URL of the image |
| caption | string | optional | Optional caption |
| fromPhoneNo | string | required | E.164 of the QR number to send FROM. Required unless you pass `sessionId`. |
Request
curl --request POST \
--url 'https://cname.betazen.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"
}
}/whatsapp-qr/sessionsQR · List Sessions
Permissions: messages.read
List the vendor's connected QR sessions so you can discover the numbers you can send from.
Request
curl --request GET \
--url 'https://cname.betazen.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
}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.
{your-callback-url}Incoming Text Message
Delivered when a customer sends you a text message. Acknowledge with HTTP 200 within 5 seconds (Meta retries with exponential backoff if you don't).
| Field | Type | Req? | Description |
|---|---|---|---|
| object | string | required | Always 'whatsapp_business_account' |
| entry[].id | string | required | WABA id |
| entry[].changes[].value.messages[] | array | required | The actual messages |
| entry[].changes[].value.metadata.phone_number_id | string | required | Which of your numbers received the message |
| entry[].changes[].value.contacts[] | array | required | Sender profile (name, wa_id) |
Request
curl --request POST \
--url 'https://cname.betazen.vendor.waapi.easycrm4u.com/api/v1/external{your-callback-url}' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"contacts": [
{
"profile": {
"name": "Sayantan Kar"
},
"wa_id": "917890000198"
}
],
"messages": [
{
"from": "917890000198",
"id": "wamid.HBgMOTE4NTk3NDA2Njk0FQIAEhgUM0EwMzQy...",
"timestamp": "1718635789",
"text": {
"body": "Hi, is the order shipped?"
},
"type": "text"
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Incoming Image (with caption)
Delivered when a customer sends a picture. Use `image.id` + GET /media to download bytes.
Request
curl --request POST \
--url 'https://cname.betazen.vendor.waapi.easycrm4u.com/api/v1/external{your-callback-url}' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"contacts": [
{
"profile": {
"name": "Sayantan Kar"
},
"wa_id": "917890000198"
}
],
"messages": [
{
"from": "917890000198",
"id": "wamid.HBgMO...",
"timestamp": "1718635892",
"type": "image",
"image": {
"caption": "Is this the right colour?",
"mime_type": "image/jpeg",
"sha256": "A2k...",
"id": "1234567890123456"
}
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Status — Delivered (with pricing + conversation)
Delivered when Meta finishes routing a message to the recipient device. Includes the `conversation` block (24-hour billing window id) and the `pricing` block (category Meta charged at).
| Field | Type | Req? | Description |
|---|---|---|---|
| statuses[].id | string | required | The wamid you got from our send response |
| statuses[].status | string | required | 'sent' | 'delivered' | 'read' | 'failed' |
| statuses[].conversation.id | string | optional | Meta 24h conversation id (CBP) |
| statuses[].conversation.origin.type | string | optional | marketing | utility | authentication | service |
| statuses[].pricing | object | optional | billable, pricing_model, category |
Request
curl --request POST \
--url 'https://cname.betazen.vendor.waapi.easycrm4u.com/api/v1/external{your-callback-url}' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--header 'Content-Type: application/json' \
--data '{
"object": "whatsapp_business_account",
"entry": [
{
"id": "959648497055756",
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+91 78900 00199",
"phone_number_id": "1226212787235581"
},
"statuses": [
{
"id": "wamid.HBgMOTE4NTk3NDA2Njk0FQIAEhgUM0EwMzQy...",
"status": "delivered",
"timestamp": "1718635790",
"recipient_id": "917890000198",
"conversation": {
"id": "7e8d9f3a1c2b4d5e8f9a0b1c2d3e4f5a",
"expiration_timestamp": "1718722190",
"origin": {
"type": "marketing"
}
},
"pricing": {
"billable": true,
"pricing_model": "CBP",
"category": "marketing",
"type": "regular"
}
}
]
}
}
]
}
]
}'Success response
{
"ok": true
}{your-callback-url}Status — Failed
Delivered when a message could not be delivered. `errors[]` has Meta's error code + reason.
Request
curl --request POST \
--url 'https://cname.betazen.vendor.waapi.easycrm4u.com/api/v1/external{your-callback-url}' \
--header 'X-API-Key: waapi_YOUR_API_KEY' \
--header 'X-API-Secret: secr_YOUR_API_SECRET' \
--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
}Postman collection
The full collection (v2.1) covers every endpoint on this page plus the webhook examples. Import into Postman, set your baseUrl, apiKey, and apiSecret collection variables, and run.