Complete API documentation for Ghala. Authentication, endpoints, examples, and error handling.
The Ghala API is plain HTTPS + JSON on top of the official WhatsApp Cloud API. With it you can:
Base URL:
https://api.ghala.io/api/v1
Request examples come in cURL, Python, and JavaScript; pick your language once and every example on the page follows. Every block has a copy button, and Copy for AI (top of the page) copies this whole reference as markdown, ready to paste into your AI assistant. No SDK required.
All requests require an API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Create and rotate keys in Dashboard → Developer → Credentials.
Every response shares one shape:
{ "success": true, "data": { } }
List endpoints paginate with page and limit query parameters and return:
{
"success": true,
"data": [ ],
"pagination": { "page": 1, "limit": 20, "total": 143 }
}
| Tier | Requests/min |
|---|---|
| Starter | 60 |
| Growth | 300 |
| Enterprise | 1000+ |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. On 429, back off until the reset timestamp; don't retry in a tight loop.
WhatsApp only allows free-form messages within 24 hours of the customer's last inbound message. Outside that window, sends fail with OUTSIDE_SESSION_WINDOW; use a template message instead. This is a WhatsApp platform rule, not a Ghala limit, and it's the most common first-integration surprise.
POST /messages
curl -X POST https://api.ghala.io/api/v1/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255712345678",
"type": "text",
"text": { "body": "Hello from Ghala!" }
}'
import requests
resp = requests.post(
"https://api.ghala.io/api/v1/messages",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"to": "255712345678",
"type": "text",
"text": {"body": "Hello from Ghala!"},
},
)
print(resp.json())
const resp = await fetch("https://api.ghala.io/api/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "255712345678",
type: "text",
text: { body: "Hello from Ghala!" },
}),
});
console.log(await resp.json());
Response:
{
"success": true,
"data": { "message_id": "wamid.HBgM...", "status": "sent" }
}
to is full international format without +. sent means WhatsApp accepted the message; delivery and read receipts arrive later as webhook events.
POST /messages
curl -X POST https://api.ghala.io/api/v1/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255712345678",
"type": "template",
"template": {
"name": "order_update",
"language": { "code": "en" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "John" },
{ "type": "text", "text": "1042" }
]
}
]
}
}'
import requests
resp = requests.post(
"https://api.ghala.io/api/v1/messages",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"to": "255712345678",
"type": "template",
"template": {
"name": "order_update",
"language": {"code": "en"},
"components": [
{
"type": "body",
"parameters": [
{"type": "text", "text": "John"},
{"type": "text", "text": "1042"},
],
}
],
},
},
)
print(resp.json())
const resp = await fetch("https://api.ghala.io/api/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "255712345678",
type: "template",
template: {
name: "order_update",
language: { code: "en" },
components: [
{
type: "body",
parameters: [
{ type: "text", text: "John" },
{ type: "text", text: "1042" },
],
},
],
},
}),
});
console.log(await resp.json());
Parameters fill the template's {{1}}, {{2}} placeholders in order. The template must be approved and the language.code must match one of its submitted languages.
POST /messages
curl -X POST https://api.ghala.io/api/v1/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255712345678",
"type": "image",
"image": {
"url": "https://example.com/image.jpg",
"caption": "Check out our new product!"
}
}'
import requests
resp = requests.post(
"https://api.ghala.io/api/v1/messages",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"to": "255712345678",
"type": "image",
"image": {
"url": "https://example.com/image.jpg",
"caption": "Check out our new product!",
},
},
)
print(resp.json())
const resp = await fetch("https://api.ghala.io/api/v1/messages", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "255712345678",
type: "image",
image: {
url: "https://example.com/image.jpg",
caption: "Check out our new product!",
},
}),
});
console.log(await resp.json());
type can be image, video, audio, or document with a matching object. Media URLs must be public HTTPS; documents also accept a filename.
| Status | Meaning |
|---|---|
sent |
Accepted by WhatsApp |
delivered |
Reached the recipient's device |
read |
Opened by the recipient |
failed |
Not delivered; the webhook event includes the reason |
GET /contacts
Query parameters: page, limit (max 100), tag, search (name or phone).
GET /contacts/{phone} · PUT /contacts/{phone} · DELETE /contacts/{phone}
POST /contacts
curl -X POST https://api.ghala.io/api/v1/contacts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "255712345678",
"name": "John Doe",
"email": "john@example.com",
"tags": ["customer", "vip"],
"custom_fields": { "company": "Acme Inc" }
}'
import requests
resp = requests.post(
"https://api.ghala.io/api/v1/contacts",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"phone": "255712345678",
"name": "John Doe",
"email": "john@example.com",
"tags": ["customer", "vip"],
"custom_fields": {"company": "Acme Inc"},
},
)
print(resp.json())
const resp = await fetch("https://api.ghala.io/api/v1/contacts", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
phone: "255712345678",
name: "John Doe",
email: "john@example.com",
tags: ["customer", "vip"],
custom_fields: { company: "Acme Inc" },
}),
});
console.log(await resp.json());
Tags and custom fields are the raw material for campaign segments; keep them consistent.
GET /templates · GET /templates/{name}
Each template includes its Meta review status: PENDING, APPROVED, or REJECTED. Only approved templates can be sent.
POST /templates
curl -X POST https://api.ghala.io/api/v1/templates \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_confirmation",
"category": "UTILITY",
"language": "en",
"components": [
{ "type": "BODY", "text": "Hi {{1}}, your order #{{2}} is confirmed!" }
]
}'
import requests
resp = requests.post(
"https://api.ghala.io/api/v1/templates",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"name": "order_confirmation",
"category": "UTILITY",
"language": "en",
"components": [
{"type": "BODY", "text": "Hi {{1}}, your order #{{2}} is confirmed!"}
],
},
)
print(resp.json())
const resp = await fetch("https://api.ghala.io/api/v1/templates", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "order_confirmation",
category: "UTILITY",
language: "en",
components: [
{ type: "BODY", text: "Hi {{1}}, your order #{{2}} is confirmed!" },
],
}),
});
console.log(await resp.json());
name: lowercase letters, numbers, and underscores onlycategory: UTILITY (transactional), MARKETING, or AUTHENTICATION; Meta reviews against the category, and miscategorized templates get rejectedSee Message Template Best Practices for what gets approved on the first try.
Ghala points your number's webhooks at your own HTTPS endpoint, and events arrive in the standard WhatsApp Cloud API format: incoming messages, delivery updates, and read receipts, pushed in real time.
Configure it in Dashboard → Developer → Webhooks: enter your callback URL and verify token, and Ghala runs Meta's verification handshake against your endpoint immediately.
The full walkthrough (handshake code, payload examples, retry semantics) is in the Webhooks Setup Guide.
GET /analytics/messages
Query parameters:
start_date, end_date: ISO datesgranularity: hour, day, week, or monthReturns sent, delivered, read, and failed counts per bucket.
{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Phone number format is invalid",
"details": "Expected format: country code + number"
}
}
| Code | Status | Description |
|---|---|---|
UNAUTHORIZED |
401 | Missing or invalid API key |
INVALID_PHONE |
400 | Bad phone format; international, no + |
OUTSIDE_SESSION_WINDOW |
400 | Free-form message outside the 24-hour window; send a template |
TEMPLATE_NOT_FOUND |
404 | Template doesn't exist or isn't approved |
INSUFFICIENT_CREDITS |
402 | Top up required |
RATE_LIMITED |
429 | Too many requests; honor X-RateLimit-Reset |
Errors WhatsApp reports asynchronously (blocked recipients, expired sessions, policy rejections) arrive as failed statuses on your webhook, with Meta's error detail attached.
Retry guidance: 5xx and 429 are safe to retry with exponential backoff. 4xx errors are yours to fix; retrying the same request won't change the answer.