Send WhatsApp Message
Send text, media, template, or interactive messages via WhatsApp.
Request Parameters
| Parameter | Required | Type | Description |
|---|---|---|---|
from | Mandatory | String | Your WhatsApp-enabled phone number (E.164) |
to | Mandatory | String | Recipient phone number (E.164) |
content | Mandatory | Object | Message content object (see below) |
status_callback | Optional | String | Webhook URL for delivery status updates |
custom_data | Optional | String | Application-specific metadata |
Content Object
{
"recipient_type": "individual",
"type": "text",
"text": {
"body": "Hello! How can we help you today?"
}
}
Supported Content Types
Text:
{ "type": "text", "text": { "body": "Your message here", "preview_url": false } }
preview_url (optional boolean): set to true to show a URL preview if the body contains an https:// or http:// link.
Image:
{ "type": "image", "image": { "link": "https://example.com/image.jpg", "caption": "Check this out" } }
Audio:
{ "type": "audio", "audio": { "link": "https://example.com/audio.mp3" } }
Video:
{ "type": "video", "video": { "link": "https://example.com/video.mp4", "caption": "Watch this" } }
Document:
{ "type": "document", "document": { "link": "https://example.com/invoice.pdf", "caption": "Invoice", "filename": "invoice.pdf" } }
filename controls the display name and format shown in WhatsApp.
Sticker:
{ "type": "sticker", "sticker": { "link": "https://example.com/sticker.webp" } }
Static stickers: 512×512 px, max 100 KB, image/webp. Animated stickers: 512×512 px, max 500 KB, image/webp.
Location:
{ "type": "location", "location": { "longitude": 77.5946, "latitude": 12.9716, "name": "Exotel Office", "address": "Bangalore, India" } }
name and address are optional; address is only displayed when name is also provided.
Contacts:
{
"type": "contacts",
"contacts": [{
"name": { "formatted_name": "John Doe", "first_name": "John", "last_name": "Doe" },
"phones": [{ "phone": "+919876543210", "type": "CELL" }],
"emails": [{ "email": "john@example.com", "type": "WORK" }],
"org": { "company": "Acme Corp", "title": "Engineer" }
}]
}
Interactive (Reply Buttons):
{
"type": "interactive",
"interactive": {
"type": "button",
"header": { "type": "text", "text": "Order Update" },
"body": { "text": "Choose an option:" },
"footer": { "text": "Reply below" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "btn_yes", "title": "Yes" } },
{ "type": "reply", "reply": { "id": "btn_no", "title": "No" } }
]
}
}
}
Up to 3 reply buttons. Button title max 20 characters, button id max 256 characters.
Interactive (List Message):
{
"type": "interactive",
"interactive": {
"type": "list",
"header": { "type": "text", "text": "Choose a category" },
"body": { "text": "Select an option from the list below" },
"footer": { "text": "Powered by Exotel" },
"action": {
"button": "View Options",
"sections": [
{
"title": "Support",
"rows": [
{ "id": "row_billing", "title": "Billing", "description": "Payment and invoice queries" },
{ "id": "row_tech", "title": "Technical", "description": "Product and technical issues" }
]
}
]
}
}
}
action.button (string, max 20 chars) is the list button label. Up to 10 sections, up to 10 rows total across all sections. Row title max 24 chars, description max 72 chars, id max 200 chars. Section title is required when there are multiple sections (max 24 chars).
Interactive Header types: text (max 60 chars), image, video, document. Header is optional for list messages.
Interactive Body: max 1024 characters. Interactive Footer: max 60 characters.
Code Examples
- cURL
- Python
- Node.js
- PHP
- Go
curl -u '<api_key>:<api_token>' -X POST "https://api.exotel.com/v2/accounts/<your_sid>/messages" \
-H "Content-Type: application/json" \
-d '{
"from": "+919876500001",
"to": "+919876543210",
"content": {
"recipient_type": "individual",
"type": "text",
"text": { "body": "Hello from Exotel!" }
},
"status_callback": "https://your-server.com/wa-status"
}'
import requests
import json
payload = {
"from": "+919876500001",
"to": "+919876543210",
"content": {
"recipient_type": "individual",
"type": "text",
"text": {"body": "Hello from Exotel!"}
},
"status_callback": "https://your-server.com/wa-status"
}
response = requests.post(
"https://api.exotel.com/v2/accounts/<your_sid>/messages",
auth=('<api_key>', '<api_token>'),
headers={"Content-Type": "application/json"},
data=json.dumps(payload)
)
print(response.json())
const payload = {
from: '+919876500001',
to: '+919876543210',
content: {
recipient_type: 'individual',
type: 'text',
text: { body: 'Hello from Exotel!' }
},
status_callback: 'https://your-server.com/wa-status'
};
const response = await fetch(
'https://api.exotel.com/v2/accounts/<your_sid>/messages',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
const data = await response.json();
console.log(data);
<?php
$payload = json_encode(array(
"from" => "+919876500001",
"to" => "+919876543210",
"content" => array(
"recipient_type" => "individual",
"type" => "text",
"text" => array("body" => "Hello from Exotel!")
),
"status_callback" => "https://your-server.com/wa-status"
));
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_USERPWD => "<api_key>:<api_token>",
CURLOPT_URL => "https://api.exotel.com/v2/accounts/<your_sid>/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => array("Content-Type: application/json"),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
)
func main() {
url := "https://api.exotel.com/v2/accounts/<your_sid>/messages"
payload := strings.NewReader(`{
"from": "+919876500001",
"to": "+919876543210",
"content": {
"recipient_type": "individual",
"type": "text",
"text": { "body": "Hello from Exotel!" }
},
"status_callback": "https://your-server.com/wa-status"
}`)
req, _ := http.NewRequest("POST", url, payload)
req.SetBasicAuth("<api_key>", "<api_token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
Response
HTTP 202 Accepted
{
"request_id": "b434e927a5844175b23059cd96feea3d",
"method": "POST",
"http_code": 202,
"metadata": {
"total": 1,
"success": 1,
"failed": 0
},
"response": {
"whatsapp": {
"messages": [
{
"code": 202,
"error_data": null,
"status": "success",
"data": {
"sid": "2FdiiEQUosckPhpZfuVwfjxiSlc16a4"
}
}
]
}
}
}
Response Fields
| Field | Description |
|---|---|
request_id | Unique ID of the request — use for debugging and tracing |
method | HTTP method of the request (e.g., POST) |
http_code | HTTP status code |
metadata.total | Total messages in the request |
metadata.success | Messages successfully accepted |
metadata.failed | Messages that failed to be accepted |
response.whatsapp.messages[].code | Per-message HTTP response code |
response.whatsapp.messages[].error_data | Error details for a failed message (null on success), with code, message, and description |
response.whatsapp.messages[].status | Per-message status (success or failed) |
response.whatsapp.messages[].data.sid | Unique SID of the message — log this for future tracking |
HTTP Status Codes
| Code | Description |
|---|---|
| 202 | Message accepted for delivery |
| 400 | Malformed request |
| 401 | Authentication failed |
| 402 | Plan limit exceeded |
| 403 | Access denied |
| 404 | Resource not found |
| 5xx | Server error — retry the request |