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.

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:

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
}
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:

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 ParameterDescription
nameTemplate name — returns a single template
template_idTemplate ID — returns a single template
statusPENDING, APPROVED, or REJECTED
categoryTemplate 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.

Re-approval required after editing

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;
FieldTypeDescription
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 parameterWhat 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 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: 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.

Important: Group messaging is only available for WhatsApp Business Accounts with Official Business Account (OBA) status, as required by Meta. Contact EnableX support to confirm that group messaging is enabled for your business phone number before you integrate.

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.

Note: A group can have a maximum of 8 participants. Plan your invites with this limit in mind.

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.

MethodEndpointDescription
POST/whatsapp/v1/groupsCreate a group
POST/whatsapp/v1/groups/{group_id}/inviteSend an invite template to a customer
POST/whatsapp/v1/groups/{group_id}/join-requestsApprove join requests
DELETE/whatsapp/v1/groups/{group_id}/participantsRemove participants
POST/whatsapp/v1/groups/{group_id}/messagesSend a text, media, or template message to the group
POST/whatsapp/v1/groups/{group_id}/messages/readMark 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;
FieldTypeDescription
subjectStringGroup name shown to participants
descriptionStringGroup description
join_approval_modeStringauto_approve — customers join directly from an invite. approval_required — each join request waits for your approval.

Response:

{
  "messaging_product": "whatsapp",
  "request_id": "MTY0NjcwNDM1OTU6MTIwMzYzNDA0Njk0MjMzODIw"
}
Important: The response confirms only that the request was accepted — it does not contain the group ID. Store the 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_link from 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"
          }
        ]
      }
    ]
  }
}
Note: 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"
}
Note: Authentication templates cannot be sent to groups. Marketing and utility templates, as well as free-form text and media, are supported.

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"
}
Tip: In 1:1 chats, marking a message as read can also mark earlier messages as read. In groups, each message must be marked as read individually — call this endpoint once for every message you want to mark.

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:

Message1:1 CategoryGroup CategoryCharged in a Group?
Marketing templatemarketinggroup_marketingYes, per delivered participant
Utility template, window closedutilitygroup_utilityYes, per delivered participant
Utility template, window openutilitygroup_utilityNo — pricing.type is free_group_customer_service
Free-form text or mediaservicegroup_serviceNo
Authentication templateauthentication—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.

Important: Your server MUST return HTTP 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"
  }
}
FieldTypeDescription
statuses[].idStringThe message_id returned when you sent the group message
statuses[].statusStringDelivery status for this participant, such as delivered
statuses[].recipient_idStringThe group ID (in 1:1 notifications, this is the user's phone number)
statuses[].recipient_typeStringgroup for group messages
statuses[].recipient_participant_idStringPhone number of the participant this status applies to
statuses[].pricing.billableBooleantrue if this delivery is charged
statuses[].pricing.typeStringregular for a charged delivery; free_group_customer_service when the delivery is free because the group's service window is open
statuses[].pricing.categoryStringgroup_marketing, group_utility, or group_service
group_contextObjectThe 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.

Tip: The same status for the same participant can arrive more than once. Make your handler idempotent: treat the combination of 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 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 →