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.
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:
{
"app_id": "YOUR_APP_ID",
"app_key": "YOUR_APP_KEY"
}
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:
GET https://api.enablex.io/whatsapp/v1/templates?status=APPROVED
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
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 |
Delete a Template
DELETE https://api.enablex.io/whatsapp/v1/templates/your_template_name
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
This deletes all templates with the given name across all languages. Response:
{
"success": true
}
Sending Template Messages
Once your template is approved, you use the Messages API to send it. This is a completely separate API call from template creation — you reference the template by name and provide runtime data for any placeholders.
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: Bearer YOUR_TOKEN
Content-Type: application/json
{
"messageId": "wamid.HBgMOTE5..."
}
Response:
{
"success": true
}
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: