WhatsApp Business API
The EnableX WhatsApp Business API lets you send and receive messages on WhatsApp programmatically. You can send text, rich media (images, video, audio, documents, stickers), location, contacts, and interactive messages (buttons, lists) — all through a single API endpoint. You can also create WhatsApp groups and message several customers in one thread with Group Messaging.
WhatsApp messaging operates in two distinct modes. Understanding the difference is critical before you write a single line of integration code.
Authorization header. See the Authentication section below before making any API calls.
▶ Run in Postman Collection last updated Aug 12, 2024
Session Messaging vs. Template Messaging
WhatsApp enforces a 24-hour session window that determines what type of message your business can send. This is a Meta platform rule — not an EnableX limitation.
How the 24-Hour Session Window Works
When a user sends a message to your business number, a 24-hour session window opens. During this window, your business can reply with any content — text, media, interactive messages — without needing a pre-approved template. These are called Session Messages.
Once the 24-hour window expires (or if the user has never messaged you), you must use a pre-approved Message Template to initiate the conversation. This is called a Business Initiated Conversation (BIC).
| Scenario | Message Type Required | Content Restrictions |
|---|---|---|
| User messages your business first | Session Message (any content) | None — send text, media, interactive, location, contacts freely |
| Replying within 24 hours of user's last message | Session Message (any content) | None |
| 24-hour window has expired | Template Message only | Must use a pre-approved template |
| User has never messaged your business | Template Message only | Must use a pre-approved template |
Authentication
All WhatsApp Business API calls are server-to-server. EnableX supports two authentication mechanisms:
Option 1: HTTP Basic Authentication
Every EnableX WhatsApp project is assigned APP ID and APP KEY credentials. To authenticate with HTTP Basic:
- Concatenate your credentials as
APP_ID:APP_KEY(colon-separated). - Base64-encode the resulting string.
- Pass it in the
Authorizationheader with theBasicprefix.
POST https://api.enablex.io/whatsapp/v1/messages
Authorization: Basic BASE64_ENCODED_APP_ID:APP_KEY
Content-Type: application/json
Option 2: Bearer Token
For token-based auth, first generate a token using the Token API. The token is valid for 60 minutes. You can make unlimited API calls within that window.
Step 1 — Generate a Token
POST https://api.enablex.io/whatsapp/v1/token
Content-Type: application/json
Request body — pass your project credentials:
curl -X POST https://api.enablex.io/whatsapp/v1/token \
-H "Content-Type: application/json" \
-d '{
"app_id": "YOUR_APP_ID",
"app_key": "YOUR_APP_KEY"
}'
const axios = require('axios');
const response = await axios.post(
'https://api.enablex.io/whatsapp/v1/token',
{
app_id: 'YOUR_APP_ID',
app_key: 'YOUR_APP_KEY'
}
);
console.log(response.data);
import requests
response = requests.post(
'https://api.enablex.io/whatsapp/v1/token',
json={
'app_id': 'YOUR_APP_ID',
'app_key': 'YOUR_APP_KEY'
}
)
print(response.json())
<?php
$payload = ['app_id' => 'YOUR_APP_ID', 'app_key' => 'YOUR_APP_KEY'];
$ch = curl_init('https://api.enablex.io/whatsapp/v1/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Success response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiry": 1680000000
}
| Field | Type | Description |
|---|---|---|
token | String | Bearer token for subsequent API calls |
expiry | Number | Unix timestamp (UTC) when the token expires |
Step 2 — Use the Token
POST https://api.enablex.io/whatsapp/v1/messages
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
expiry timestamp or by catching auth errors and regenerating on the fly.
Authentication error response (both Basic and Bearer):
{
"message": "Authentication failed",
"status": "Unauthorized",
"reason": "invalid credentials or the account has been deactivated"
}
API Endpoint
All outgoing WhatsApp messages — template or session, text or rich media — use a single endpoint:
| Method | Endpoint |
|---|---|
POST | https://api.enablex.io/whatsapp/v1/messages |
The JSON payload you send determines the message type and content. When the API accepts a message for processing, it returns:
{
"status": "processing",
"message_id": "wamid.HBgMOTE5..."
}
The message_id is your reference for tracking delivery status via webhooks.
Template Messaging
Templates are pre-defined, Meta-approved message formats that your business can use to initiate conversations. Think of templates as message blueprints — they have a fixed structure with optional placeholder variables that you fill in at send time.
Template Lifecycle: Creation vs. Usage
There are two completely separate operations involving templates. Understanding this distinction avoids a very common integration mistake:
| Operation | API | What It Does | When |
|---|---|---|---|
| Template Creation | POST /whatsapp/v1/templates | Submits a new template to Meta for approval | One-time setup (per template) |
| Template Usage (Send Message) | POST /whatsapp/v1/messages | Sends a message using an already-approved template | Every time you message a user |
Understanding Template Placeholders
Templates use numbered placeholders — {{1}}, {{2}}, {{3}}, etc. — to mark positions where dynamic data gets inserted at send time. These placeholders are sequential and 1-indexed.
Example template body text (as defined during creation):
Hello {{1}}, your order #{{2}} has been shipped and will arrive by {{3}}.
When you send a message using this template, you provide the actual values for each placeholder in the components array. The values are mapped in order — the first parameter replaces {{1}}, the second replaces {{2}}, and so on.
At send time, the user sees:
Hello John, your order #ORD-98765 has been shipped and will arrive by April 10.
Creating a Text Template
To create a template, POST to the templates endpoint with a name, category, language, and components array defining each section of the template.
The payload below creates a template named order_shipped with one body placeholder:
{
"name": "order_shipped",
"category": "MARKETING",
"language": "en",
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your order has been shipped. Track it on our website.",
"example": {
"body_text": [
[
"John"
]
]
}
},
{
"type": "FOOTER",
"text": "Reply STOP to unsubscribe"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Unique template name (within a language). Used to reference the template when sending messages. |
category | String | Yes | One of: MARKETING, UTILITY, AUTHENTICATION |
language | String | Yes | Language code (e.g., en, hi, es) |
components | Array | Yes | Defines HEADER, BODY, FOOTER, and BUTTONS sections |
The example Field — Why It Matters
When your template body uses placeholders like {{1}}, you must provide example data. Meta uses this to review the template with realistic content. The example.body_text array maps values to placeholders in order — the first string replaces {{1}}, the second replaces {{2}}.
{
"type": "BODY",
"text": "Hi {{1}}. Your account is being migrated to {{2}}.",
"example": {
"body_text": [
[
"John",
"AWS"
]
]
}
}
In this example, "John" maps to {{1}} and "AWS" maps to {{2}}.
API response on successful submission:
{
"id": "755147549669633",
"status": "PENDING",
"category": "MARKETING"
}
The id is the Template ID. You'll receive approval/rejection notifications via webhook.
Creating a Rich Media Template
A rich media template includes a header with an image, video, or document. This is useful for sending product images, promotional videos, or PDF invoices alongside your message text.
The HEADER component specifies the media format, and you must provide an example media URL for Meta's review:
{
"name": "promo_image",
"category": "MARKETING",
"language": "en",
"components": [
{
"type": "HEADER",
"format": "IMAGE",
"example": {
"header_handle": [
"https://example.com/sample-promo.jpg"
]
}
},
{
"type": "BODY",
"text": "Your physics course {{1}}, is now available in preview!",
"example": {
"body_text": [
[
"Gravity"
]
]
}
},
{
"type": "FOOTER",
"text": "Enroll today"
}
]
}
| Header Format | Use When |
|---|---|
IMAGE | Template includes a header image (JPEG, PNG — max 5 MB) |
VIDEO | Template includes a header video (MP4, 3GP — max 16 MB) |
DOCUMENT | Template includes a header document (PDF, DOCX — max 100 MB) |
Creating a Quick Reply Button Template
Quick reply buttons let users respond with a single tap instead of typing. When creating the template, you define the button labels. When the user taps a button, you receive the response via webhook.
{
"name": "membership_renewal",
"category": "MARKETING",
"language": "en",
"components": [
{
"type": "BODY",
"text": "Hello dear Mr. {{1}}, would you like to renew your membership?",
"example": {
"body_text": [
[
"John"
]
]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "Yes, Renew"
},
{
"type": "QUICK_REPLY",
"text": "No, Thanks"
}
]
}
]
}
Creating a Call-to-Action Button Template
Call-to-action (CTA) buttons execute actions directly on the user's phone — opening a URL or dialing a phone number. Unlike quick reply buttons (which send a response back to your webhook), CTA buttons trigger native device actions.
{
"name": "appointment_confirm",
"category": "UTILITY",
"language": "en",
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"example": {
"body_text": [
[
"John",
"April 10 at 3 PM"
]
]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "PHONE_NUMBER",
"text": "Call Us",
"phone_number": "919800000000"
},
{
"type": "URL",
"text": "View Details",
"url": "https://yoursite.com/appointments/{{1}}",
"example": [
"https://yoursite.com/appointments/12345"
]
}
]
}
]
}
| Button Type | Action | Key Fields |
|---|---|---|
PHONE_NUMBER | Opens phone dialer with the specified number | text (label), phone_number (with country code, no leading +) |
URL | Opens the specified URL in the device browser | text (label), url (may contain {{1}} placeholder), example (required if URL has placeholder) |
Managing Templates
List Templates
Retrieve your templates, optionally filtered by status, category, name, or template ID:
curl "https://api.enablex.io/whatsapp/v1/templates?status=APPROVED" \
-u APP_ID:APP_KEY
const axios = require('axios');
const response = await axios.get(
'https://api.enablex.io/whatsapp/v1/templates',
{
params: { status: 'APPROVED' },
auth: { username: 'APP_ID', password: 'APP_KEY' }
}
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
response = requests.get(
'https://api.enablex.io/whatsapp/v1/templates',
params={'status': 'APPROVED'},
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$ch = curl_init('https://api.enablex.io/whatsapp/v1/templates?status=APPROVED');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Response is an array of template objects:
[
{
"status": "APPROVED",
"business_phone": "918800899287",
"wa": {
"template_id": "558534749704350"
},
"template": {
"...": "original creation payload"
},
"track": {
"posted": {
"date": "2023-03-23T09:10:26.974Z"
},
"approved": {
"date": "2023-03-23T09:18:34.055Z"
}
}
}
]
| Filter Parameter | Description |
|---|---|
name | Template name — returns a single template |
template_id | Template ID — returns a single template |
status | PENDING, APPROVED, or REJECTED |
category | Template category filter |
Edit a Template
Update an existing template's category or component content by sending a
PATCH request with the template's ID in the path. Only the fields you
include in the payload are changed — omit any field you want to leave as-is.
The components array accepts the same component types as the Create Template
endpoint (HEADER, BODY, FOOTER, BUTTONS).
See the component reference above for the full structure
of each type.
Editing a template resets its status to PENDING. Meta must re-approve the
updated template before it can be used to send messages again.
curl -X PATCH https://api.enablex.io/whatsapp/v1/templates/your_template_id \
-u APP_ID:APP_KEY \
-H "Content-Type: application/json" \
-d '{
"category": "MARKETING",
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your order {{2}} has shipped. Track it at {{3}}.",
"example": {
"body_text": [["John", "ORD-9821", "https://track.example.com"]]
}
}
]
}'
const axios = require('axios');
const templateId = 'your_template_id';
const response = await axios.patch(
`https://api.enablex.io/whatsapp/v1/templates/${templateId}`,
{
category: 'MARKETING',
components: [
{
type: 'BODY',
text: 'Hello {{1}}, your order {{2}} has shipped. Track it at {{3}}.',
example: {
body_text: [['John', 'ORD-9821', 'https://track.example.com']]
}
}
]
},
{
headers: { 'Content-Type': 'application/json' },
auth: { username: 'APP_ID', password: 'APP_KEY' }
}
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
template_id = 'your_template_id'
payload = {
'category': 'MARKETING',
'components': [
{
'type': 'BODY',
'text': 'Hello {{1}}, your order {{2}} has shipped. Track it at {{3}}.',
'example': {
'body_text': [['John', 'ORD-9821', 'https://track.example.com']]
}
}
]
}
response = requests.patch(
f'https://api.enablex.io/whatsapp/v1/templates/{template_id}',
json=payload,
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$templateId = 'your_template_id';
$payload = json_encode([
'category' => 'MARKETING',
'components' => [
[
'type' => 'BODY',
'text' => 'Hello {{1}}, your order {{2}} has shipped. Track it at {{3}}.',
'example' => [
'body_text' => [['John', 'ORD-9821', 'https://track.example.com']]
]
]
]
]);
$ch = curl_init("https://api.enablex.io/whatsapp/v1/templates/{$templateId}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
$response = curl_exec($ch);
curl_close($ch);
echo $response;
| Field | Type | Description |
|---|---|---|
category |
string | Updated template category: MARKETING, UTILITY, or AUTHENTICATION. |
components |
array | Replacement component definitions. Accepts all component types supported by the Create Template endpoint. If omitted, existing components are unchanged. |
On success the API returns:
{
"success": true
}
Delete a Template
The Delete Template endpoint accepts a query parameter to identify what to remove.
Use name to delete every language variant of a template in one call,
or use template_id to remove a single specific language variant.
| Query parameter | What gets deleted |
|---|---|
?name={template_name} |
All language variants of that template (bulk delete by name) |
?template_id={template_id} |
The single language variant identified by that ID (delete one name + language combination) |
Option 1 — Delete all language variants (by template name)
Pass ?name={template_name} as the query parameter. Every language version
registered under that name is permanently removed.
curl -X DELETE "https://api.enablex.io/whatsapp/v1/templates/?name=your_template_name" \
-u APP_ID:APP_KEY
const axios = require('axios');
const response = await axios.delete(
'https://api.enablex.io/whatsapp/v1/templates/',
{
params: { name: 'your_template_name' },
auth: { username: 'APP_ID', password: 'APP_KEY' }
}
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
response = requests.delete(
'https://api.enablex.io/whatsapp/v1/templates/',
params={'name': 'your_template_name'},
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$url = 'https://api.enablex.io/whatsapp/v1/templates/?' . http_build_query(['name' => 'your_template_name']);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Option 2 — Delete a single language variant (by template ID)
Pass ?template_id={template_id} as the query parameter to target one specific
name + language combination. The template ID is returned when you create a template and
can also be retrieved from the List Templates response.
curl -X DELETE "https://api.enablex.io/whatsapp/v1/templates/?template_id=your_template_id" \
-u APP_ID:APP_KEY
const axios = require('axios');
const response = await axios.delete(
'https://api.enablex.io/whatsapp/v1/templates/',
{
params: { template_id: 'your_template_id' },
auth: { username: 'APP_ID', password: 'APP_KEY' }
}
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
response = requests.delete(
'https://api.enablex.io/whatsapp/v1/templates/',
params={'template_id': 'your_template_id'},
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$url = 'https://api.enablex.io/whatsapp/v1/templates/?' . http_build_query(['template_id' => 'your_template_id']);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Both variants return the same response on success:
{
"success": true
}
Sending Template Messages
Once your template is approved, you use the Messages API to send it. Template messages use the same POST /whatsapp/v1/messages endpoint as session messages — you reference the approved template by name and provide runtime data for any placeholders. This is a completely separate API call from template creation.
Send a Text Template (No Placeholders)
The simplest case — a template with static text and no variables:
{
"to": "+6599999999",
"type": "template",
"template": {
"language": {
"policy": "deterministic",
"code": "en"
},
"name": "welcome_message"
}
}
Send a Text Template (With Placeholders)
When the template body has placeholders like {{1}}, {{2}}, you must supply values in the components array. The parameters array maps to placeholders in order — first parameter fills {{1}}, second fills {{2}}, and so on:
{
"to": "+6599999999",
"type": "template",
"template": {
"language": {
"policy": "deterministic",
"code": "en"
},
"name": "order_shipped",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "John"
},
{
"type": "text",
"text": "ORD-98765"
},
{
"type": "text",
"text": "April 10"
}
]
}
]
}
}
| Component Field | Type | Description |
|---|---|---|
type | String | Use body for body text placeholders |
parameters | Array | Ordered list of parameter objects — one per placeholder |
parameters[n].type | String | Use text for text placeholders |
parameters[n].text | String | The actual value to substitute into the placeholder |
Send a Rich Media Template
When your template was created with a rich media header (image, video, or document), you must provide the media URL at send time in the header component. The body component provides placeholder data as before.
{
"to": "+6599999999",
"type": "template",
"template": {
"language": {
"policy": "deterministic",
"code": "en"
},
"name": "promo_image",
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": {
"link": "https://example.com/promo-banner.jpg"
}
}
]
},
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Gravity"
}
]
}
]
}
}
To use a video or document instead of an image, change the header parameter type:
{
"type": "video",
"video": {
"link": "https://example.com/promo-video.mp4"
}
}
{
"type": "document",
"document": {
"link": "https://example.com/invoice.pdf",
"filename": "Invoice_April.pdf"
}
}
Send a Quick Reply Button Template
If the template has no placeholders in body or header, the components array can be empty. The button labels were defined during template creation — they are not supplied at send time.
{
"to": "+6599999999",
"type": "template",
"template": {
"language": {
"policy": "deterministic",
"code": "en"
},
"name": "membership_renewal",
"components": []
}
}
Send a CTA Button Template
If the CTA URL template contains a placeholder (e.g., https://yoursite.com/track/{{1}}), you must provide the dynamic part at send time using a button component. The index field identifies which button (0-indexed):
{
"to": "+6599999999",
"type": "template",
"template": {
"language": {
"policy": "deterministic",
"code": "en"
},
"name": "appointment_confirm",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "John"
},
{
"type": "text",
"text": "April 10 at 3 PM"
}
]
},
{
"type": "button",
"sub_type": "url",
"index": "1",
"parameters": [
{
"type": "text",
"text": "12345"
}
]
}
]
}
}
| Button Component Field | Type | Description |
|---|---|---|
type | String | Use button |
sub_type | String | Use url |
index | String | Button position (0-indexed). "0" = first button, "1" = second. |
parameters[n].text | String | The dynamic portion of the URL to substitute into the placeholder |
{{1}} placeholders need runtime parameters.
Session Messaging (Free-Form)
When a 24-hour session is active (the user has recently messaged your business), you can send messages with any content — no template required. All session messages use the same POST /whatsapp/v1/messages endpoint.
Send a Text Message
The simplest message type — plain text content:
{
"to": "+6599999999",
"type": "text",
"recipient_type": "individual",
"text": {
"body": "Thank you for reaching out! We'll get back to you shortly."
}
}
| Field | Type | Required | Description |
|---|---|---|---|
to | String | Yes | Recipient phone number with country code (e.g., +6599999999) |
type | String | Yes | Use text |
recipient_type | String | No | Use individual |
text.body | String | Yes | The message text content |
Rich Media Messages
Rich media messages let you send images, audio, video, documents, and stickers as session messages. Each media type requires a publicly accessible HTTPS URL.
Image
Send photographs, product images, or visual content. Supports JPEG and PNG formats, 8-bit RGB or RGBA, up to 5 MB.
{
"to": "+6599999999",
"type": "image",
"image": {
"link": "https://example.com/product-photo.jpg",
"caption": "Our latest product — now 20% off!"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Use image |
image.link | String | Yes | Publicly accessible HTTPS URL of the image file |
image.caption | String | No | Caption displayed below the image |
Audio
Send audio files — voice notes, podcasts, music clips. Supports AAC, MP4, MPEG, AMR, OGG (not Opus or base OGG). Up to 16 MB.
{
"to": "+6599999999",
"type": "audio",
"audio": {
"link": "https://example.com/support-recording.mp3"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Use audio |
audio.link | String | Yes | Publicly accessible HTTPS URL of the audio file |
Video
Send video content — product demos, tutorials, promotional clips. Supports MP4 and 3GP with H.264 video codec and AAC audio codec. Up to 16 MB.
{
"to": "+6599999999",
"type": "video",
"video": {
"link": "https://example.com/tutorial.mp4",
"caption": "Quick setup guide — 2 minutes"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Use video |
video.link | String | Yes | Publicly accessible HTTPS URL of the video file |
video.caption | String | No | Caption displayed with the video |
Document
Send files — PDF invoices, Word contracts, Excel spreadsheets, PowerPoint presentations. Supports PDF, DOCX, XLSX, PPTX, TXT, and more. Up to 100 MB.
{
"to": "+6599999999",
"type": "document",
"document": {
"link": "https://example.com/invoice-april.pdf",
"filename": "Invoice_April_2025.pdf",
"caption": "Your invoice for April 2025"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Use document |
document.link | String | Yes | Publicly accessible HTTPS URL of the document |
document.filename | String | No | File name with extension (displayed to user) |
document.caption | String | No | Caption for the document |
Sticker
Send WebP sticker images. Static stickers up to 100 KB. Only WebP format is supported.
{
"to": "+6599999999",
"type": "sticker",
"sticker": {
"link": "https://example.com/celebration.webp"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Use sticker |
sticker.link | String | Yes | Publicly accessible HTTPS URL of the WebP sticker |
Rich Media Specifications Summary
| Media Type | Supported Formats | Max Size | Notes |
|---|---|---|---|
| Image | JPEG, PNG (8-bit, RGB/RGBA) | 5 MB | Caption optional |
| Audio | AAC, MP4, MPEG, AMR, OGG | 16 MB | Opus and base OGG not supported. No caption. |
| Video | MP4, 3GP (H.264 + AAC only) | 16 MB | Caption optional |
| Document | PDF, DOCX, XLSX, PPTX, TXT | 100 MB | Caption and filename optional |
| Sticker | WebP | 100 KB | No caption. Static only. |
Location
Share a geographic location with coordinates, name, and address. Useful for sharing store locations, delivery drop-off points, or meeting venues.
{
"to": "+6599999999",
"type": "location",
"location": {
"longitude": "103.8198",
"latitude": "1.3521",
"name": "EnableX Singapore Office",
"address": "1 Raffles Place, Tower 1, Singapore"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type | String | Yes | Use location |
location.longitude | String | Yes | Longitude of the location |
location.latitude | String | Yes | Latitude of the location |
location.name | String | No | Name of the location |
location.address | String | No | Full address text |
Contacts
Share a contact card containing name, phone numbers, email, address, organization, and URLs. The recipient sees it as a tappable contact card in WhatsApp.
{
"to": "+6599999999",
"type": "contacts",
"contacts": {
"name": {
"formatted_name": "John Smith",
"first_name": "John",
"last_name": "Smith"
},
"phones": [
{
"phone": "+6591234567",
"wa_id": "6591234567",
"type": "work"
}
],
"emails": [
{
"email": "[email protected]",
"type": "work"
}
],
"org": {
"company": "EnableX",
"department": "Engineering",
"title": "Senior Developer"
},
"addresses": [
{
"street": "1 Raffles Place",
"city": "Singapore",
"state": "Singapore",
"zip": "048616",
"country": "Singapore",
"country_code": "SG",
"type": "work"
}
],
"urls": [
{
"url": "https://www.enablex.io",
"type": "work"
}
],
"birthday": "1990-05-15"
}
}
| Field | Type | Description |
|---|---|---|
contacts.name | Object | Required. Name object with formatted_name (required), first_name, last_name |
contacts.phones | Array | Phone numbers with phone, wa_id, and type (home/work) |
contacts.emails | Array | Email addresses with email and type |
contacts.org | Object | Organization with company, department, title |
contacts.addresses | Array | Physical addresses with street, city, state, zip, country |
contacts.urls | Array | URLs with url and type |
contacts.birthday | String | Date of birth (YYYY-MM-DD format) |
Interactive Messaging
Interactive messages let users respond by tapping buttons or selecting items from a list — instead of typing. These are session messages (usable within the 24-hour window) and create richer, more guided user experiences.
Interactive Reply Buttons
Display up to 3 quick-reply buttons alongside a text message. The user taps one button to respond. You receive the button click via webhook.
{
"to": "+6599999999",
"recipient_type": "individual",
"type": "interactive",
"interactive": {
"type": "button",
"body": {
"text": "Would you like to schedule a callback?"
},
"action": {
"buttons": [
{
"type": "reply",
"reply": {
"id": "btn_yes",
"title": "Yes, Please"
}
},
{
"type": "reply",
"reply": {
"id": "btn_no",
"title": "No, Thanks"
}
},
{
"type": "reply",
"reply": {
"id": "btn_later",
"title": "Maybe Later"
}
}
]
}
}
}
| Field | Type | Description |
|---|---|---|
interactive.type | String | Use button |
interactive.body.text | String | Message body text |
action.buttons | Array | 1-3 button objects |
reply.id | String | Unique ID for the button — returned in the webhook when clicked |
reply.title | String | Button label (visible to user) |
btn_yes, btn_schedule_morning) rather than generic numbers. These IDs are returned in the webhook payload, and clear IDs make your webhook handler code more readable.
Interactive List Messages
Display a structured list of options organized into sections (categories). The user taps an action button to open the list, then selects one item. Ideal for menus, catalogs, or multi-step selection flows.
{
"to": "+6599999999",
"recipient_type": "individual",
"type": "interactive",
"interactive": {
"type": "list",
"header": {
"type": "text",
"text": "Choose Your Plan"
},
"body": {
"text": "We offer three subscription plans. Select one to learn more:"
},
"footer": {
"text": "Prices are monthly. Cancel anytime."
},
"action": {
"button": "View Plans",
"sections": [
{
"title": "Individual Plans",
"rows": [
{
"id": "plan_basic",
"title": "Basic",
"description": "$9/month — 1,000 messages"
},
{
"id": "plan_pro",
"title": "Pro",
"description": "$29/month — 10,000 messages"
}
]
},
{
"title": "Business Plans",
"rows": [
{
"id": "plan_enterprise",
"title": "Enterprise",
"description": "Custom pricing — unlimited messages"
}
]
}
]
}
}
}
| Field | Type | Description |
|---|---|---|
interactive.type | String | Use list |
interactive.header.text | String | Header text (optional) |
interactive.body.text | String | Body message text |
interactive.footer.text | String | Footer text (optional) |
action.button | String | Label for the action button that opens the list |
action.sections | Array | One or more section objects, each with a title and rows |
rows[n].id | String | Unique ID for the list item — returned in webhook when selected |
rows[n].title | String | Item title (visible to user) |
rows[n].description | String | Item description (optional) |
Mark Messages as Read
Mark an incoming message as "read" — the sender sees double blue ticks on their device. Use this to signal to the user that your business has seen their message.
POST https://api.enablex.io/whatsapp/v1/messages/read
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
curl -X POST https://api.enablex.io/whatsapp/v1/messages/read \
-u APP_ID:APP_KEY \
-H "Content-Type: application/json" \
-d '{ "messageId": "wamid.HBgMOTE5..." }'
const axios = require('axios');
const response = await axios.post(
'https://api.enablex.io/whatsapp/v1/messages/read',
{ messageId: 'wamid.HBgMOTE5...' },
{ auth: { username: 'APP_ID', password: 'APP_KEY' } }
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
response = requests.post(
'https://api.enablex.io/whatsapp/v1/messages/read',
json={'messageId': 'wamid.HBgMOTE5...'},
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$ch = curl_init('https://api.enablex.io/whatsapp/v1/messages/read');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['messageId' => 'wamid.HBgMOTE5...']));
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Response:
{
"success": true
}
Group Messaging
Group messaging lets your business create a WhatsApp group, invite customers into it, and exchange messages with all participants in a single thread. Instead of sending the same message to each customer one by one, you send it once to the group, and every participant receives it.
Use group messaging when several people need to follow the same conversation — for example, a support thread shared by a customer and their colleagues, an order or project update for a small team, or a service conversation that involves more than one person on the customer's side.
Group messaging uses its own set of endpoints under /whatsapp/v1/groups. The standard POST /whatsapp/v1/messages endpoint remains for 1:1 messaging only — see Sending to a Group Through the 1:1 Endpoint.
How Group Messaging Works
Creating a group and getting participants into it happens asynchronously. Your API calls start each step, and webhook notifications tell you when it has completed. A typical integration follows this flow:
1. Create the group. Call the create-group endpoint with a subject and join settings. The API accepts the request and returns a request_id — it does not return the group ID yet.
2. Receive the group ID. When WhatsApp has created the group, your webhook receives a Group Created notification carrying the same request_id, the new group_id, and the group's invite_link.
3. Invite participants. Send an invite template to each customer you want in the group, or share the invite link with them.
4. Admit participants. With auto_approve, customers who use the invite join directly. With approval_required, you approve their join requests through the API. Either way, your webhook receives a Participant Joined notification for each new participant.
5. Exchange messages. Send text, media, or template messages to the group. Delivery statuses arrive per participant, and messages that participants send to the group arrive as incoming group messages.
Group Endpoints
All group endpoints use the same base URL and authentication as the rest of the WhatsApp API. In the paths below, replace {group_id} with the group ID from the Group Created notification.
| Method | Endpoint | Description |
|---|---|---|
POST | /whatsapp/v1/groups | Create a group |
POST | /whatsapp/v1/groups/{group_id}/invite | Send an invite template to a customer |
POST | /whatsapp/v1/groups/{group_id}/join-requests | Approve join requests |
DELETE | /whatsapp/v1/groups/{group_id}/participants | Remove participants |
POST | /whatsapp/v1/groups/{group_id}/messages | Send a text, media, or template message to the group |
POST | /whatsapp/v1/groups/{group_id}/messages/read | Mark a group message as read |
Create a Group
Creating a group sets its name, an optional description, and how new participants are admitted.
POST https://api.enablex.io/whatsapp/v1/groups
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
The request body has three fields. subject is the group name participants see in WhatsApp, description is the group description, and join_approval_mode decides whether customers who use an invite join immediately (auto_approve) or wait for you to approve them (approval_required):
curl -X POST https://api.enablex.io/whatsapp/v1/groups \
-u APP_ID:APP_KEY \
-H "Content-Type: application/json" \
-d '{
"subject": "Support Group",
"description": "Customer support thread",
"join_approval_mode": "auto_approve"
}'
const axios = require('axios');
const response = await axios.post(
'https://api.enablex.io/whatsapp/v1/groups',
{
subject: 'Support Group',
description: 'Customer support thread',
join_approval_mode: 'auto_approve'
},
{ auth: { username: 'APP_ID', password: 'APP_KEY' } }
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
response = requests.post(
'https://api.enablex.io/whatsapp/v1/groups',
json={
'subject': 'Support Group',
'description': 'Customer support thread',
'join_approval_mode': 'auto_approve'
},
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$payload = [
'subject' => 'Support Group',
'description' => 'Customer support thread',
'join_approval_mode' => 'auto_approve'
];
$ch = curl_init('https://api.enablex.io/whatsapp/v1/groups');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
curl_close($ch);
echo $response;
| Field | Type | Description |
|---|---|---|
subject | String | Group name shown to participants |
description | String | Group description |
join_approval_mode | String | auto_approve — customers join directly from an invite. approval_required — each join request waits for your approval. |
Response:
{
"messaging_product": "whatsapp",
"request_id": "MTY0NjcwNDM1OTU6MTIwMzYzNDA0Njk0MjMzODIw"
}
request_id, then wait for the Group Created webhook with the same request_id to get the group_id and invite_link. You need the group_id for every other group endpoint.
Invite Participants
Participants join a group through its invite link. There are two ways to get the link to a customer:
- Share the invite link yourself — the
invite_linkfrom the Group Created notification can be shared through any channel, such as email, SMS, or your app. - Send an invite template — send the customer an approved WhatsApp template that carries the group invite, using the endpoint below.
POST https://api.enablex.io/whatsapp/v1/groups/{group_id}/invite
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
The payload names the customer to invite in to (phone number with country code) and the template to send. Inside the template's body component, pass a parameter of type group_id containing the group to invite them to:
{
"to": "919876543210",
"template": {
"name": "group_invite_template",
"language": {
"code": "en"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "group_id",
"group_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD"
}
]
}
]
}
}
group_invite_template is an example name. Use the name of an invite template that has been approved for your WhatsApp Business Account.
Approve Join Requests
If the group was created with join_approval_mode set to approval_required, a customer who uses the invite does not join immediately — they create a join request that you must approve. Use this when you need to control exactly who enters the group, for example to verify the customer first.
POST https://api.enablex.io/whatsapp/v1/groups/{group_id}/join-requests
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
Pass the IDs of the join requests to approve in the join_requests array. You can approve several requests in one call:
{
"join_requests": [
"MTY0NjcwNDM1OTU6MTIwMzYzNDA0Njk0MjMzODIw"
]
}
Each approved customer joins the group, and your webhook receives a Participant Joined notification.
Remove Participants
Remove one or more participants from the group — for example, when a support case is resolved for one of them, or someone was added by mistake.
DELETE https://api.enablex.io/whatsapp/v1/groups/{group_id}/participants
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
List each participant to remove as an object in the participants array, identified by phone number with country code in user:
{
"participants": [
{
"user": "919876543210"
},
{
"user": "919988776655"
}
]
}
Send a Message to a Group
Sending to a group works like sending a 1:1 message, except that the group is identified in the URL instead of a to phone number in the body. The body uses the same message objects as the standard API — type plus the matching content object — so you can send free-form text and media, or templates.
POST https://api.enablex.io/whatsapp/v1/groups/{group_id}/messages
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
Text message. Set type to text and put the message in text.body:
curl -X POST https://api.enablex.io/whatsapp/v1/groups/GROUP_ID/messages \
-u APP_ID:APP_KEY \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"text": {
"body": "Hello everyone in the group"
}
}'
const axios = require('axios');
const groupId = 'GROUP_ID';
const response = await axios.post(
`https://api.enablex.io/whatsapp/v1/groups/${groupId}/messages`,
{
type: 'text',
text: {
body: 'Hello everyone in the group'
}
},
{ auth: { username: 'APP_ID', password: 'APP_KEY' } }
);
console.log(response.data);
import requests
from requests.auth import HTTPBasicAuth
group_id = 'GROUP_ID'
response = requests.post(
f'https://api.enablex.io/whatsapp/v1/groups/{group_id}/messages',
json={
'type': 'text',
'text': {
'body': 'Hello everyone in the group'
}
},
auth=HTTPBasicAuth('APP_ID', 'APP_KEY')
)
print(response.json())
<?php
$groupId = 'GROUP_ID';
$payload = [
'type' => 'text',
'text' => [
'body' => 'Hello everyone in the group'
]
];
$ch = curl_init("https://api.enablex.io/whatsapp/v1/groups/$groupId/messages");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, 'APP_ID:APP_KEY');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Template message. Set type to template and pass the template name, language, and any parameters, exactly as for a 1:1 template:
{
"type": "template",
"template": {
"name": "order_update_group",
"language": {
"code": "en"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Order #12345"
}
]
}
]
}
}
When the API accepts the message, it returns the group and the message ID. Use the message_id to match the per-participant delivery statuses that follow:
{
"status": "processing",
"group_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"message_id": "wamid.HBgMOTE5ODg3NjU0MzIxFQIAERgSQjVCM0Q4RjA5Q0QxMkMzQTYA"
}
Mark a Group Message as Read
Mark an incoming group message as read so participants see that your business has seen it.
POST https://api.enablex.io/whatsapp/v1/groups/{group_id}/messages/read
Authorization: Basic BASE64(APP_ID:APP_KEY)
Content-Type: application/json
Pass the ID of the incoming message in messageId:
{
"messageId": "wamid.HBgMOTE5ODg3NjU0MzIxFQIAERgSQjVCM0Q4RjA5Q0QxMkMzQTYA"
}
Sending to a Group Through the 1:1 Endpoint
The standard POST /whatsapp/v1/messages endpoint does not accept group messages. If you send a request to it with recipient_type set to group, the API returns HTTP 400 and points you to the group endpoint:
{
"message": "Group messages must be sent via POST /whatsapp/v1/groups/:group_id/messages",
"status": "Invalid request",
"reason": "recipient_type=group is not supported on this endpoint"
}
How Group Messages Are Charged
Group messages use the same per-country rates as 1:1 messages, but they are charged per participant the message is delivered to — not once per API call. A marketing template sent to a group of 4 participants, all of whom receive it, is charged as 4 marketing messages.
Keep these rules in mind when you plan group messaging costs:
- Only actual deliveries are charged. If you send a template to 5 participants and only 3 receive it, you are charged for 3.
- The customer service window covers the whole group. In 1:1 messaging, the 24-hour window opens only between you and the user who messaged you. In a group, when any participant sends a message, the window opens for the entire group. While it is open, utility templates and free-form messages to the group are free.
- Group utility messages are not eligible for volume-tier pricing.
Each delivery status reports how that delivery was categorised in its pricing object. The group categories map to the 1:1 categories as follows:
| Message | 1:1 Category | Group Category | Charged in a Group? |
|---|---|---|---|
| Marketing template | marketing | group_marketing | Yes, per delivered participant |
| Utility template, window closed | utility | group_utility | Yes, per delivered participant |
| Utility template, window open | utility | group_utility | No — pricing.type is free_group_customer_service |
| Free-form text or media | service | group_service | No |
| Authentication template | authentication | — | Not supported in groups |
Group Webhook Notifications
Group events are delivered to the same webhook URL as your 1:1 notifications, in the same format: the event data plus your business_phone. Four group events matter for an integration — the group being created, participants joining, participants sending messages, and delivery statuses for your messages.
200 to acknowledge every group webhook, just as for 1:1 notifications. See Acknowledging Webhook Notifications.
Group Created
Trigger: WhatsApp has finished creating a group you requested with Create a Group.
Payload: A groups array with one entry whose type is group_create. It carries the request_id from your create-group call, the new group_id, the group subject, its invite_link, and the join_approval_mode.
{
"groups": [
{
"timestamp": "1751798400",
"group_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"type": "group_create",
"request_id": "REQUEST_ID",
"subject": "Support Group",
"invite_link": "https://chat.whatsapp.com/LINK_ID",
"join_approval_mode": "auto_approve"
}
],
"business_phone": "919811122233"
}
What to do: Match request_id to the request you stored when creating the group, then save the group_id and invite_link against it. From here on, use the group_id in all group endpoint paths.
Participant Joined
Trigger: A customer joins the group — directly from an invite, or after you approve their join request.
Payload: A groups entry whose type is group_participants_add. reason states how they joined (for example, invite_link), and added_participants lists the new participants by WhatsApp ID.
{
"groups": [
{
"timestamp": "1751798500",
"group_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"type": "group_participants_add",
"reason": "invite_link",
"added_participants": [
{
"wa_id": "919876543210"
}
]
}
],
"business_phone": "919811122233"
}
What to do: Add each wa_id to your record of the group's members. Keep the member count in mind — a group can have at most 8 participants.
Incoming Group Message
Trigger: A participant sends a message to the group.
Payload: The same messages format as an incoming 1:1 message, with one addition: group_id identifies the group the message was sent in, while from identifies the participant who sent it.
{
"messages": [
{
"from": "919876543210",
"id": "wamid.INCOMING_MSG_ID",
"timestamp": "1751798600",
"type": "text",
"group_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"text": {
"body": "Thanks for the update"
}
}
],
"business_phone": "919811122233"
}
What to do: Route the message to the group conversation by group_id, not to a 1:1 chat with the sender. This message also opens the customer service window for the whole group — see How Group Messages Are Charged. Use the message id to mark it as read.
Group Delivery Status
Trigger: A message you sent to the group changes status for a participant — for example, it is delivered to them. Because a group message reaches several people, you receive a status for each participant.
Payload: A statuses array, like a 1:1 delivery notification, with group-specific fields. recipient_id is the group ID, recipient_type is group, and recipient_participant_id is the participant the status applies to. A group_context object repeats the group and participant for convenience.
{
"statuses": [
{
"id": "wamid.HBgMOTE5ODg3NjU0MzIxFQIAERgSQjVCM0Q4RjA5Q0QxMkMzQTYA",
"status": "delivered",
"timestamp": "1751798700",
"recipient_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"recipient_type": "group",
"recipient_participant_id": "919876543210",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"type": "regular",
"category": "group_marketing"
}
}
],
"business_phone": "919811122233",
"group_context": {
"recipient_type": "group",
"group_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"participant_id": "919876543210"
}
}
| Field | Type | Description |
|---|---|---|
statuses[].id | String | The message_id returned when you sent the group message |
statuses[].status | String | Delivery status for this participant, such as delivered |
statuses[].recipient_id | String | The group ID (in 1:1 notifications, this is the user's phone number) |
statuses[].recipient_type | String | group for group messages |
statuses[].recipient_participant_id | String | Phone number of the participant this status applies to |
statuses[].pricing.billable | Boolean | true if this delivery is charged |
statuses[].pricing.type | String | regular for a charged delivery; free_group_customer_service when the delivery is free because the group's service window is open |
statuses[].pricing.category | String | group_marketing, group_utility, or group_service |
group_context | Object | The group_id and participant_id the notification relates to |
Several participants in one notification. Statuses can also arrive aggregated, with one entry per participant in the same statuses array:
{
"statuses": [
{
"id": "wamid.HBgMOTE5ODg3NjU0MzIxFQIAERgSQjVCM0Q4RjA5Q0QxMkMzQTYA",
"status": "delivered",
"recipient_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"recipient_type": "group",
"recipient_participant_id": "919876543210",
"pricing": {
"billable": true,
"type": "regular",
"category": "group_marketing"
}
},
{
"id": "wamid.HBgMOTE5ODg3NjU0MzIxFQIAERgSQjVCM0Q4RjA5Q0QxMkMzQTYA",
"status": "delivered",
"recipient_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"recipient_type": "group",
"recipient_participant_id": "919988776655",
"pricing": {
"billable": true,
"type": "regular",
"category": "group_marketing"
}
}
],
"business_phone": "919811122233"
}
Free delivery inside the service window. When the group's customer service window is open, a utility template or free-form message is delivered free of charge. The status shows billable as false and type as free_group_customer_service:
{
"statuses": [
{
"id": "wamid.FREE_MSG_ID",
"status": "delivered",
"recipient_id": "Y2FwaV9ncm91cDoxNzA1NTU1MDEzOToxMjAzNjM0MDQ2OTQyMzM4MjAZD",
"recipient_type": "group",
"recipient_participant_id": "919876543210",
"pricing": {
"billable": false,
"pricing_model": "PMP",
"type": "free_group_customer_service",
"category": "group_utility"
}
}
],
"business_phone": "919811122233"
}
What to do: Always loop over the whole statuses array — never read only the first entry, or you will miss participants in aggregated notifications. Track status per participant, keyed on the message id plus recipient_participant_id, so you can tell who has received the message.
id, recipient_participant_id, and status as unique, and ignore repeats.
Error Handling
When a message fails to send or deliver, WhatsApp returns error information either in the API response or via webhook notification. Common error scenarios include:
| Error Code | Scenario | Description |
|---|---|---|
| 131047 | 24-hour window expired | More than 24 hours since the customer last replied. Use a template instead. |
| 131053 | Media size exceeded | Media file exceeds the allowed size limit for the content type. |
| 100 | Invalid parameter | JSON payload contains invalid or missing required fields. |
Webhook-delivered failure notification example:
{
"statuses": [
{
"id": "wamid.HBgMOTE5...",
"recipient_id": "+6599999999",
"status": "failed",
"timestamp": "1680000000",
"type": "message",
"errors": [
{
"code": 131047,
"title": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
"href": "https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/"
}
]
}
],
"business_phone": "918800899287"
}
Next Steps
Now that you understand how to send messages, set up your webhook server to receive incoming messages, delivery notifications, and user interactions: