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.

Important: Every WhatsApp API call requires authentication. You must include either HTTP Basic credentials or a Bearer Token in the 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).

ScenarioMessage Type RequiredContent Restrictions
User messages your business firstSession Message (any content)None — send text, media, interactive, location, contacts freely
Replying within 24 hours of user's last messageSession Message (any content)None
24-hour window has expiredTemplate Message onlyMust use a pre-approved template
User has never messaged your businessTemplate Message onlyMust use a pre-approved template
Tip: You can always use a template message — even within an active session window. Templates are mandatory only when no session exists. Many businesses use templates for follow-ups and session messages for real-time conversations.

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:

  1. Concatenate your credentials as APP_ID:APP_KEY (colon-separated).
  2. Base64-encode the resulting string.
  3. Pass it in the Authorization header with the Basic prefix.
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
}
FieldTypeDescription
tokenStringBearer token for subsequent API calls
expiryNumberUnix 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
Important: When a token expires, any API call using it returns an authentication error. Your code must handle token renewal — either by tracking the 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:

MethodEndpoint
POSThttps://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:

OperationAPIWhat It DoesWhen
Template CreationPOST /whatsapp/v1/templatesSubmits a new template to Meta for approvalOne-time setup (per template)
Template Usage (Send Message)POST /whatsapp/v1/messagesSends a message using an already-approved templateEvery time you message a user
Note: You create a template once. You use it to send messages many times. The creation payload defines the template structure. The send payload fills in the runtime data for placeholders.

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"
    }
  ]
}
FieldTypeRequiredDescription
nameStringYesUnique template name (within a language). Used to reference the template when sending messages.
categoryStringYesOne of: MARKETING, UTILITY, AUTHENTICATION
languageStringYesLanguage code (e.g., en, hi, es)
componentsArrayYesDefines 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 FormatUse When
IMAGETemplate includes a header image (JPEG, PNG — max 5 MB)
VIDEOTemplate includes a header video (MP4, 3GP — max 16 MB)
DOCUMENTTemplate 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 TypeActionKey Fields
PHONE_NUMBEROpens phone dialer with the specified numbertext (label), phone_number (with country code, no leading +)
URLOpens the specified URL in the device browsertext (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 ParameterDescription
nameTemplate name — returns a single template
template_idTemplate ID — returns a single template
statusPENDING, APPROVED, or REJECTED
categoryTemplate 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 FieldTypeDescription
typeStringUse body for body text placeholders
parametersArrayOrdered list of parameter objects — one per placeholder
parameters[n].typeStringUse text for text placeholders
parameters[n].textStringThe 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": []
  }
}
Note: Quick reply templates may also have body/header placeholders. If so, include the corresponding body and/or header components with parameters as shown in earlier examples.

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 FieldTypeDescription
typeStringUse button
sub_typeStringUse url
indexStringButton position (0-indexed). "0" = first button, "1" = second.
parameters[n].textStringThe dynamic portion of the URL to substitute into the placeholder
Tip: Phone number CTA buttons do not require parameters at send time — the number was fixed during template creation. Only URL buttons with {{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."
  }
}
FieldTypeRequiredDescription
toStringYesRecipient phone number with country code (e.g., +6599999999)
typeStringYesUse text
recipient_typeStringNoUse individual
text.bodyStringYesThe 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!"
  }
}
FieldTypeRequiredDescription
typeStringYesUse image
image.linkStringYesPublicly accessible HTTPS URL of the image file
image.captionStringNoCaption 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"
  }
}
FieldTypeRequiredDescription
typeStringYesUse audio
audio.linkStringYesPublicly 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"
  }
}
FieldTypeRequiredDescription
typeStringYesUse video
video.linkStringYesPublicly accessible HTTPS URL of the video file
video.captionStringNoCaption 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"
  }
}
FieldTypeRequiredDescription
typeStringYesUse document
document.linkStringYesPublicly accessible HTTPS URL of the document
document.filenameStringNoFile name with extension (displayed to user)
document.captionStringNoCaption 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"
  }
}
FieldTypeRequiredDescription
typeStringYesUse sticker
sticker.linkStringYesPublicly accessible HTTPS URL of the WebP sticker

Rich Media Specifications Summary

Media TypeSupported FormatsMax SizeNotes
ImageJPEG, PNG (8-bit, RGB/RGBA)5 MBCaption optional
AudioAAC, MP4, MPEG, AMR, OGG16 MBOpus and base OGG not supported. No caption.
VideoMP4, 3GP (H.264 + AAC only)16 MBCaption optional
DocumentPDF, DOCX, XLSX, PPTX, TXT100 MBCaption and filename optional
StickerWebP100 KBNo caption. Static only.
Important: If you send a media file that doesn't comply with these specifications, the message will be accepted for processing but will fail to deliver. You'll receive a failure notification via webhook with the error details.

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"
  }
}
FieldTypeRequiredDescription
typeStringYesUse location
location.longitudeStringYesLongitude of the location
location.latitudeStringYesLatitude of the location
location.nameStringNoName of the location
location.addressStringNoFull 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"
  }
}
FieldTypeDescription
contacts.nameObjectRequired. Name object with formatted_name (required), first_name, last_name
contacts.phonesArrayPhone numbers with phone, wa_id, and type (home/work)
contacts.emailsArrayEmail addresses with email and type
contacts.orgObjectOrganization with company, department, title
contacts.addressesArrayPhysical addresses with street, city, state, zip, country
contacts.urlsArrayURLs with url and type
contacts.birthdayStringDate 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"
          }
        }
      ]
    }
  }
}
FieldTypeDescription
interactive.typeStringUse button
interactive.body.textStringMessage body text
action.buttonsArray1-3 button objects
reply.idStringUnique ID for the button — returned in the webhook when clicked
reply.titleStringButton label (visible to user)
Tip: Use meaningful IDs for buttons (e.g., 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"
            }
          ]
        }
      ]
    }
  }
}
FieldTypeDescription
interactive.typeStringUse list
interactive.header.textStringHeader text (optional)
interactive.body.textStringBody message text
interactive.footer.textStringFooter text (optional)
action.buttonStringLabel for the action button that opens the list
action.sectionsArrayOne or more section objects, each with a title and rows
rows[n].idStringUnique ID for the list item — returned in webhook when selected
rows[n].titleStringItem title (visible to user)
rows[n].descriptionStringItem 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 CodeScenarioDescription
13104724-hour window expiredMore than 24 hours since the customer last replied. Use a template instead.
131053Media size exceededMedia file exceeds the allowed size limit for the content type.
100Invalid parameterJSON 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:

WhatsApp Webhooks →