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.
Reconnecting the number issues a new token and invalidates the old one. If a working integration suddenly returns
401, that's almost always why.
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'll get:
{ "code": "outside_messaging_window", "message": "..." }
with status 409. That's 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.
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're how you start a conversation, send a reminder, or follow up on an order.
Create and submit templates under Templates in the dashboard; Meta approval usually takes minutes to a few hours. Then:
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());
For a template with variables, pass template_components in Meta's own component format. A 502 back means WhatsApp refused it, usually because the template isn't approved or the name and language don't match.
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're messaging about, not from a random value, so a retry after a crash reuses the same key.
sent only means WhatsApp accepted the message. Delivery and read receipts arrive asynchronously, and so do your customers' replies. That's 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.