Sending WhatsApp messages from my own application (API)
WACO exposes a small API so your own application — attendance, billing, queueing, admissions — can send WhatsApp messages through your official number. Two endpoints, one auth header. You never touch a Meta dashboard.
nomor, template, isi, teks). They are stable and documented in the OpenAPI spec, so generated SDKs match them exactly — we would rather keep them consistent than rename them and break every integration built so far.Authentication
Create an API key on the Developer page. Keys look like
waco_… and are shown only once — store it safely. Send it with every request:
Authorization: Bearer waco_YOUR_KEY
Base URL: https://waco.id. A revoked key, or a tenant suspended for non-payment,
stops working immediately.
1. List the templates you can use
curl https://waco.id/api/v1/template \ -H "Authorization: Bearer waco_YOUR_KEY"
The response contains only templates Meta has already approved:
{
"template": [
{ "nama": "absensi_hadir", "bahasa": "id", "kategori": "UTILITY",
"isi": "Hello {{1}}, {{2}} arrived at {{3}}.",
"jumlah_nilai": 3, "tombol": [] }
]
}
jumlah_nilai tells you how many values you must supply to fill
{{1}}, {{2}} and so on.
2. Send a notification
curl -X POST https://waco.id/api/v1/kirim \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"nomor": "628123456789",
"nama": "Ms Ani",
"template": "absensi_hadir",
"isi": ["Budi", "Budi", "07:15"]
}'
Fields: nomor (E.164 without +, required), template (required),
isi (array of variable values — the count must match jumlah_nilai),
nama (optional, used for contact naming). A template with an image header
must carry gambar_url (public https, JPEG/PNG ≤5 MB) — Meta does not store the sample
image, so every send names the image again; a PDF-header template uses dokumen_url
(+ optional dokumen_nama). A successful response:
{ "status": "terkirim", "message_id": "wamid.HBg…", "percakapan_id": 81 }
message_id is the WhatsApp wamid — store it, because pesan_id in the
message.status webhook carries the same id. That is how you track
sent → delivered → read per message. percakapan_id only appears when your
number has an inbox; API-only accounts do not get it.
Node.js example
const res = await fetch('https://waco.id/api/v1/kirim', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.WACO_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
nomor: '628123456789',
template: 'absensi_hadir',
isi: ['Budi', 'Budi', '07:15'],
}),
});
const data = await res.json();
PHP example
$ch = curl_init('https://waco.id/api/v1/kirim');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WACO_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'nomor' => '628123456789',
'template' => 'absensi_hadir',
'isi' => ['Budi', 'Budi', '07:15'],
]),
]);
$data = json_decode(curl_exec($ch), true);
Replying in a conversation (no template)
If the customer messaged you within the last 24 hours, you may reply with free-form text without a template — ideal for interactive flows: receive the reply through a webhook, then answer here.
curl -X POST https://waco.id/api/v1/balas \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"nomor": "628123456789",
"teks": "Understood — we have received the registration."
}'
Response: { "status": "terkirim", "message_id": "wamid…" }. Text limit 4096
characters.
Only valid inside the 24-hour window. Outside it Meta rejects the message
(error 131047) and you must use /api/v1/kirim with a template. Why that rule exists
is explained in the template guide.
Sending media (image, document, video, audio)
Send files by public URL — Meta downloads them, so you never upload to us first. Good for lab results, PDF invoices or photos. Like replies, this works only inside the 24-hour window.
curl -X POST https://waco.id/api/v1/kirim-media \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"nomor": "628123456789",
"tipe": "document",
"url": "https://your-app.com/invoice/INV-001.pdf",
"filename": "Invoice-INV-001.pdf",
"caption": "Registration invoice"
}'
tipe: image, document, video or
audio. url must be HTTPS and publicly reachable. caption
is optional (not for audio); filename applies to documents only. Size limits follow
WhatsApp's own (for example documents 100 MB, images 5 MB).
Creating templates through the API
Templates can also be created from your application, without opening the dashboard:
curl -X POST https://waco.id/api/v1/template \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"nama": "order_confirmation",
"bahasa": "en",
"kategori": "UTILITY",
"isi": "Hello {{1}}, we have received order {{2}}.",
"contoh": ["Budi", "INV-123"]
}'
A successful response (201): { "id": "…", "status": "PENDING", … }.
Important: success means submitted, not ready to use. Every template
is reviewed by Meta first — usually minutes to a few hours, and it can be rejected. Poll until
it reaches APPROVED:
curl "https://waco.id/api/v1/template?status=semua" \ -H "Authorization: Bearer waco_YOUR_KEY"
With ?status=semua, each template carries a status field
(PENDING / APPROVED / REJECTED) plus
alasan_tolak (Meta's rejection reason) when rejected. Once approved, send it through
/kirim as usual.
Other fields: bahasa (defaults to id), footer
(≤60 chars), tombol (quick replies, max 3), tombol_url
({"teks":"…","url":"https://…"}), and for the AUTHENTICATION category an
otp object — Meta composes that body itself, don't write your own. The rule that
causes most rejections is checked upfront for you: the number of contoh examples
must equal the number of variables in isi.
Deleting: DELETE /api/v1/template/template_name. Deleted names are quarantined by
Meta for 30 days and cannot be reused immediately.
Image templates (media broadcasts)
A template message can carry an image above the text (image header) — right for poster promos or reminders with a photo. Three ways, all available today:
- Dashboard: the Template page → upload the image in the header field; the Blast page → pick a template marked 🖼️ and upload the image once for every recipient.
- Creation API: add
gambar_urltoPOST /api/v1/template— WACO downloads the image, uploads the sample to Meta and submits an image-header template. - Send API: once APPROVED, send through
POST /api/v1/kirimwith thegambar_urlyou actually want shown (it may differ from the sample). For mass image broadcasts from the API usePOST /api/v1/blastwithgambar_url— the template is created for you.
curl -X POST https://waco.id/api/v1/template \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"nama": "september_promo",
"bahasa": "en",
"kategori": "MARKETING",
"isi": "Hi {{1}}, our September promo is open. See the poster above!",
"contoh": ["Budi"],
"gambar_url": "https://cdn.yourshop.com/poster-september.jpg"
}'
# once APPROVED:
curl -X POST https://waco.id/api/v1/kirim \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"nomor": "628123456789",
"template": "september_promo",
"isi": ["Budi"],
"gambar_url": "https://cdn.yourshop.com/poster-september.jpg"
}'
The image must be JPEG/PNG ≤5 MB at a publicly downloadable https URL. Free-form image messages
inside the 24-hour window still go through /api/v1/kirim-media with a
caption; outside that window the only route is an image-header template as above. PDF and
video headers cannot be created through the API yet — use the dashboard.
Choosing the sending number (multi-branch)
Have more than one number (one per clinic branch, say)? Add dari to choose which
number sends. Without it, your first number is used. Works on /kirim,
/balas and /kirim-media:
curl -X POST https://waco.id/api/v1/kirim \
-H "Authorization: Bearer waco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"dari": "628158825011",
"nomor": "628123456789",
"template": "absensi_hadir",
"isi": ["Budi", "Budi", "07:15"]
}'
dari is matched against your branch display numbers. If it matches none, the
answer is 400 — so a typo in the sender is caught rather than silently sent from
another branch.
Error codes
401— API key not recognised (wrong header, or the key was revoked).403— Service suspended (invoice unpaid).429— Too many requests (limit 120 sends/minute per key). Retry shortly.400—nomor/templatemissing, invalid number, or the number ofisivalues does not match the template.404— Template not found on your number.409— WhatsApp not connected yet, or the template is not approved by Meta.502— Meta rejected the send; the reason is in thedetailfield.
To receive replies and delivery statuses in your application, see Receiving replies & statuses over webhooks.