Guides/For developers

Sending WhatsApp messages from my own application (API)

Authenticate, fetch your templates, send, with curl, Node and PHP examples. 9 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.

Restrict IPs (optional). On the Developer page each key has a Restrict IPs link: enter your server addresses or CIDR ranges (103.10.20.30, 10.0.0.0/24, IPv6 works too). Once set, the key is accepted only from those addresses; anywhere else gets 401 as if the key were unknown, and the Developer page records the last rejected IP with an Allow this IP button. Leave it empty for keys used by claude.ai or ChatGPT over MCP, since their addresses change.

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.