Guides/For developers

Sending WhatsApp messages from my own application (API)

Authenticate, fetch your templates, send — with curl, Node and PHP examples. 8 min read

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.

Field names in this API are Indonesian (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:

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

When is a template required? Only when you start a conversation with someone who has not replied within 24 hours. If they just messaged you, reply freely without a template. More in the template guide.

To receive replies and delivery statuses in your application, see Receiving replies & statuses over webhooks.

Still stuck after following this guide? Contact us from the dashboard — attach a screenshot if you have one, it speeds everything up.