From access token to delivered message in five minutes, with cURL, Python, and JavaScript examples.
You need three things:
402 plan_feature_locked; upgrade under Settings → Billing.All requests go to:
https://v2.ghala.io/api/v2
with your token in the Authorization header. That prefix is the API version, and v2 is the current one.
Reconnecting the number issues a new token and invalidates the old one. If a working integration suddenly returns
401, that is almost always why.
The cheapest way to prove authentication works is a call that messages nobody:
curl "https://v2.ghala.io/api/v2/templates?limit=1" \
-H "Authorization: Bearer $ACCESS_TOKEN"
A 200 means your token is good and the plan is right. A 401 means the token is wrong or stale; a 402 means the plan does not include API access.
WhatsApp only allows free-form messages within 24 hours of the customer's last message to you. Outside that window, only an approved template can be sent.
So before your first test: message the number from a real WhatsApp handset. That opens the window and lets the text example below succeed. Skip this and you get:
{ "code": "outside_messaging_window", "message": "..." }
with status 409. That is the API telling you the rule up front rather than letting Meta reject the send later.
Phone numbers use full international format without + (e.g. 255712345678).
curl -X POST https://v2.ghala.io/api/v2/messages \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "255712345678",
"type": "text",
"text": "Habari! Hii ni ujumbe wa majaribio."
}'
import os, requests
res = requests.post(
"https://v2.ghala.io/api/v2/messages",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
json={
"to": "255712345678",
"type": "text",
"text": "Habari! Hii ni ujumbe wa majaribio.",
},
)
print(res.json())
const res = await fetch("https://v2.ghala.io/api/v2/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "255712345678",
type: "text",
text: "Habari! Hii ni ujumbe wa majaribio.",
}),
});
console.log(await res.json());
A successful send returns the recorded message:
{
"id": "01JZ8Q4R2K7N3M5P9V1X6T0B2C",
"direction": "OUTBOUND",
"message_type": "text",
"content": "Habari! Hii ni ujumbe wa majaribio.",
"status": "sent",
"wa_message_id": "wamid.HBgM...",
"created_at": "2026-08-05T09:14:22Z"
}
Send it to your own WhatsApp number first; it should land on your phone within seconds. There is no sandbox and no test mode, so every send is a real message to a real handset.
Sending through the API stands the AI agent down for that customer, exactly as replying from the dashboard inbox does. Otherwise the assistant would answer alongside you and the customer would hear two voices.
The agent resumes on its own after the number's takeover window, or immediately if the customer texts BOT. Worth knowing before you wire the API into something that sends often: every send is a handover.
Templates are the only thing you can send outside the 24-hour window, so they are how you start a conversation, send a reminder, or follow up on an order.
Ask the API which ones you may send:
curl "https://v2.ghala.io/api/v2/templates?sendable=true" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Then send one:
curl -X POST https://v2.ghala.io/api/v2/messages \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "255712345678",
"type": "template",
"template_name": "hello_world",
"template_language": "en_US"
}'
import os, requests
res = requests.post(
"https://v2.ghala.io/api/v2/messages",
headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
json={
"to": "255712345678",
"type": "template",
"template_name": "hello_world",
"template_language": "en_US",
},
)
print(res.json())
const res = await fetch("https://v2.ghala.io/api/v2/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "255712345678",
type: "template",
template_name: "hello_world",
template_language: "en_US",
}),
});
console.log(await res.json());
Create and submit templates under Templates in the dashboard; Meta approval usually takes minutes to a few hours.
For a template with variables, pass template_components in Meta's own component format — Templates and Media has worked examples. A 502 back means WhatsApp refused it, usually because the template is not approved or the name and language do not match.
The same endpoint sends media and interactive menus. type picks the shape:
{
"to": "255712345678",
"type": "interactive",
"interactive": {
"type": "buttons",
"body": "Oda yako #1042 iko tayari. Ungependa vipi kuipata?",
"buttons": [
{ "id": "delivery", "title": "Niletewe" },
{ "id": "pickup", "title": "Nitakuja kuchukua" }
]
}
}
The id you set comes back to you when the customer taps, so make it something you can act on. Full list of types in the API Reference.
Add an Idempotency-Key header to any send your code might retry, and every integration retries eventually:
curl -X POST https://v2.ghala.io/api/v2/messages \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: order-1042-confirmation" \
-H "Content-Type: application/json" \
-d '{
"to": "255712345678",
"type": "text",
"text": "Asante! Oda yako #1042 imethibitishwa."
}'
Run it twice and the customer still receives one message: the second call replays the first response with Idempotency-Replayed: true in the headers, and nothing reaches WhatsApp.
Derive the key from the thing you are messaging about, not from a random value, so a retry after a crash reuses the same key. That is also how you recover when a request times out and you do not know whether it landed — see Errors and Retries.
sent only means WhatsApp accepted the message. Delivery and read receipts arrive asynchronously, and so do your customers' replies. That is the second half of any integration:
→ Receive events with webhooks to get replies and status updates in real time.
For every endpoint, parameter, and error code, see the API Reference.