Gateway WhatsApp untuk notifikasi transaksi: invoice, pembayaran, pengingat, dan OTP. Layanan ini bukan untuk broadcast atau promosi.
- Base URL:
https://bales.ai.id/api/v1 - Format: JSON (UTF-8). Semua waktu dalam ISO-8601 UTC (
2026-10-17T03:15:00Z). - Autentikasi: API key per aplikasi, dibuat di dashboard → Aplikasi & API key.
1. Autentikasi
Kirim API key di setiap request dengan salah satu header berikut:
Authorization: Bearer wan_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
atau
X-Api-Key: wan_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Aturan menyimpan key:
- Simpan key di environment/konfigurasi server aplikasi Anda (
.envatau opsi plugin), bukan di kode, repo, atau JavaScript browser. - Key hanya ditampilkan sekali saat dibuat. Kalau hilang atau bocor: Cabut key itu, lalu buat yang baru.
- Key bisa dibatasi ke nomor pengirim tertentu dan diberi tanggal kedaluwarsa.
Batas request: 120 request/menit per API key. Batas ini melindungi server. Kecepatan kirim WhatsApp sendiri diatur oleh antrian (lihat bagian 6).
2. Format error
Semua error memakai bentuk yang sama:
{
"error": {
"code": "validation_failed",
"message": "Nomor tujuan tidak valid. …",
"details": { "to": ["Nomor tujuan tidak valid. …"] }
}
}
| HTTP | code |
Arti |
|---|---|---|
| 401 | missing_api_key, invalid_api_key, api_key_revoked, api_key_expired |
Masalah API key |
| 402 | subscription_inactive |
Langganan/trial akun berakhir atau ditangguhkan. Perpanjang di menu Langganan. |
| 403 | application_inactive |
Aplikasi dinonaktifkan di dashboard |
| 403 | device_not_allowed |
Key tidak diizinkan memakai nomor tersebut |
| 404 | message_not_found, device_not_found |
Data tidak ditemukan (atau bukan milik aplikasi Anda) |
| 409 | idempotency_key_reused |
Idempotency-Key sudah dipakai dengan isi pesan berbeda |
| 422 | validation_failed, device_inactive |
Input tidak valid |
| 429 | rate_limited |
Terlalu banyak request, coba lagi sebentar |
| 429 | quota_exceeded |
Kuota pesan bulan ini habis (error.details.resets_at = waktu reset) |
| 503 | no_device_available |
Belum ada nomor WhatsApp aktif yang boleh dipakai |
3. Kirim pesan — POST /messages
Body
| Field | Wajib | Keterangan |
|---|---|---|
to |
ya | Nomor tujuan. 08…, 62…, dan +62… diubah otomatis ke 62…. Nomor luar negeri wajib diawali +kode negara (mis. +9665…). |
text |
ya, kecuali ada media/template |
Isi pesan, maksimal 4096 karakter. Kalau ada media, text menjadi caption. |
media.type |
dengan media | image (JPG/PNG/WebP, maks. 5 MB) atau document (PDF, maks. 16 MB) |
media.url |
dengan media | URL https publik ke file langsung, tanpa redirect |
media.filename |
tidak | Nama file yang terlihat penerima, mis. INV-123.pdf |
template.name |
dengan template | Khusus nomor WA Official. Nama template yang disetujui Meta (huruf kecil, angka, _). |
template.language |
tidak | Kode bahasa template, default id. Harus sama persis dengan di Meta (mis. id, en_US). |
template.params |
tidak | Isian {{1}}, {{2}}, … berurutan, mis. ["Budi", "Rp350.000"]. |
device_id |
tidak | ID nomor pengirim. Kalau kosong, nomor dipilih otomatis (untuk template: hanya nomor WA Official). |
reference |
tidak | Referensi Anda (mis. nomor invoice), maksimal 100 karakter. Bisa dicari di dashboard. |
expires_in |
tidak | Detik (60–259200). Pesan yang belum terkirim melewati batas ini menjadi failed dengan kode expired. Default 24 jam. Untuk OTP, pakai 300. |
sensitive |
tidak | true = angka 4+ digit disamarkan di log setelah terkirim. Kalau tidak diisi, OTP terdeteksi otomatis. |
metadata |
tidak | Objek berisi maksimal 20 pasangan string, disimpan bersama pesan |
send_at |
tidak | Kirim terjadwal. Waktu ISO-8601, mis. 2026-10-20T09:00:00+07:00. Minimal 1 menit dan maksimal 60 hari dari sekarang. Status pesan scheduled sampai waktunya, lalu masuk antrian. expires_in dihitung dari send_at. |
Header yang disarankan
Idempotency-Key: invoice-INV-123-paid
- Kalau request yang sama dikirim ulang (mis. karena timeout jaringan) dengan key yang sama dalam 24 jam, Anda akan menerima pesan yang sama (HTTP
200, headerIdempotent-Replayed: true). Pesan tidak dikirim dua kali. - Key yang sama dengan isi berbeda ditolak dengan HTTP
409. - Format key: maksimal 100 karakter, berisi huruf, angka, dan
- _ : .. Key juga boleh dikirim lewat field bodyidempotency_key.
Respons 202 Accepted
{
"data": {
"id": "01JABCDEF0123456789XYZABCD",
"status": "queued",
"to": "6281234567890",
"type": "text",
"device_id": "01J9ZYXWVUT987654321ABCDEF",
"reference": "INV-123",
"attempts": 0,
"error": null,
"created_at": "2026-10-17T03:15:00Z",
"sent_at": null,
"delivered_at": null,
"read_at": null,
"failed_at": null,
"expires_at": "2026-10-18T03:15:00Z"
}
}
Kalau nomor pengirim sedang terputus, respons berisi tambahan "warning": "…". Pesan tetap mengantri
dan dikirim otomatis begitu nomor tersambung kembali.
Batalkan pesan terjadwal — DELETE /messages/{id}
Hanya untuk pesan berstatus scheduled. Berhasil → status cancelled (tidak memotong kuota).
Kalau sudah masuk antrian atau terkirim → HTTP 409 dengan kode not_cancellable.
3b. Kirim banyak pesan sekaligus — POST /messages/batch
Untuk pesan transaksi ke banyak pelanggan Anda sendiri (mis. tagihan bulanan), bukan promosi.
Maksimal 100 pesan per request. Setiap pesan memakai field yang sama dengan POST /messages
(termasuk send_at), ditambah idempotency_key per pesan (opsional).
{
"messages": [
{ "to": "081234567890", "text": "Tagihan Mei Rp150.000", "reference": "INV-501", "idempotency_key": "inv-501" },
{ "to": "081298765432", "text": "Tagihan Mei Rp200.000", "reference": "INV-502", "idempotency_key": "inv-502" }
]
}
Respons 202 (atau 422 bila tidak ada satu pun yang diterima). Setiap pesan diproses sendiri:
yang salah tidak menggagalkan yang lain.
{
"data": [
{ "index": 0, "ok": true, "replayed": false, "data": { "id": "01JAB…", "status": "queued", "...": "..." } },
{ "index": 1, "ok": false, "error": { "code": "validation_failed", "message": "Nomor tujuan tidak valid. …" } }
],
"summary": { "total": 2, "accepted": 1, "failed": 1 }
}
Pesan tetap dikirim bertahap sesuai antrian anti-blokir tiap nomor, jadi 100 pesan tidak terkirim dalam satu detik. Itu disengaja supaya nomor Anda aman.
4. Cek status — GET /messages/{id}
Respons sama dengan data di atas. Arti status:
| Status | Arti |
|---|---|
scheduled |
Terjadwal, menunggu send_at (bisa dibatalkan) |
cancelled |
Pesan terjadwal yang dibatalkan |
queued |
Dalam antrian (menunggu giliran, jeda anti-blokir, nomor tersambung, atau kuota besok) |
sent |
Diterima server WhatsApp |
delivered |
Sampai di HP penerima |
read |
Dibaca (hanya muncul kalau penerima mengaktifkan centang biru) |
failed |
Gagal. Lihat error.code. |
Kode error pesan yang umum:
error.code |
Arti / tindakan |
|---|---|
recipient_not_on_whatsapp |
Nomor tujuan tidak terdaftar di WhatsApp. Perbaiki data nomor. |
expired |
Pesan lewat expires_in sebelum sempat dikirim |
media_invalid_type, media_too_large, media_unavailable, media_redirect, media_url_rejected |
Masalah file media |
whatsapp_error |
WhatsApp melaporkan pesan gagal |
provider_error, provider_unreachable |
Gangguan server; sudah dicoba ulang otomatis sampai 5 kali |
subscription_inactive |
Langganan berakhir/ditangguhkan saat pesan masih di antrian |
| outside_24h_window | WA Official: kontak belum chat dalam 24 jam terakhir — kirim memakai template |
| template_not_found | WA Official: template tidak ada / belum disetujui / jumlah isian tidak cocok |
| template_requires_official | template dikirim ke nomor WhatsApp biasa |
| meta_marketing_limit, recipient_opted_out, meta_payment_issue | WA Official: dibatasi Meta, penerima menolak promosi, atau pembayaran akun Meta bermasalah |
Daripada polling, sebaiknya pakai webhook (bagian 7).
WA Official (WhatsApp API resmi)
Nomor jenis WA Official memakai WhatsApp Cloud API resmi Meta. Endpoint, webhook, dan status sama dengan nomor biasa, dengan aturan Meta:
- Teks/media bebas hanya bila kontak chat ke nomor itu dalam 24 jam terakhir (mis. membalas pertanyaan, Bales AI).
- Di luar 24 jam (tagihan, pengingat, promosi, OTP) wajib
templateyang sudah disetujui Meta (menu Template WA di dashboard). - Tidak ada jeda anti-blokir; batasnya tier Meta (penerima unik per 24 jam). Biaya per pesan template ditagih Meta ke akun WhatsApp Business Anda.
- Grup tidak didukung.
{
"to": "081234567890",
"device_id": "01J…",
"template": { "name": "tagihan_spp", "language": "id", "params": ["Budi", "Oktober", "Rp350.000"] },
"reference": "INV-2410-0087"
}
5. Nomor pengirim
GET /devices
Menampilkan nomor yang boleh dipakai API key ini.
GET /devices/{id}/status
{
"data": {
"id": "01J9ZYXWVUT987654321ABCDEF",
"name": "Notifikasi RMM",
"status": "connected",
"connected": true,
"active": true,
"phone": "6281111111111",
"last_connected_at": "2026-10-17T00:00:00Z",
"last_disconnected_at": null,
"quota": { "daily_limit": 100, "sent_today": 12, "remaining_today": 88, "per_minute": 6 }
}
}
Nilai status yang mungkin: pending, connecting, connected, disconnected, refused.
GET /account
Status langganan & sisa kuota bulan ini (kuota direset tiap tanggal 1, WIB).
{
"data": {
"account": "Toko Berkah",
"plan": "basic",
"status": "active",
"active": true,
"ends_at": "2026-11-17T08:00:00Z",
"quota": { "monthly_limit": 3000, "used_this_month": 120, "remaining_this_month": 2880, "resets_at": "2026-10-31T17:00:00Z" }
}
}
monthly_limit bernilai null untuk akun tanpa batas. Pesan uji dari dashboard dan balasan otomatis ikut dihitung.
5b. Pesan masuk — GET /messages/inbound
Daftar chat yang masuk ke nomor yang boleh dipakai API key Anda, urut dari yang paling lama.
| Query | Keterangan |
|---|---|
after |
id pesan masuk terakhir yang sudah Anda proses (ambil dari next_after) |
since |
Waktu ISO-8601; dipakai bila after kosong |
device_id |
Hanya nomor tertentu |
from |
Hanya dari nomor pengirim tertentu |
limit |
1–100, default 50 |
{
"data": [
{ "id": "01JAC…", "device_id": "01J9…", "from": "6281234567890", "push_name": "Rina",
"type": "text", "text": "Kak, invoice saya sudah dibayar ya", "media": null,
"received_at": "2026-10-18T02:10:00Z" }
],
"next_after": "01JAC…"
}
next_after berisi nilai untuk halaman berikutnya, atau null bila sudah habis. Isi pesan dihapus
otomatis sesuai masa simpan (default 30 hari).
Chat di grup WhatsApp tidak termasuk di sini (grup hanya dilayani Bales AI, lihat dashboard → Bales AI).
5c. Penjualan dari iklan — POST /conversions/purchase
Untuk pelanggan yang datang lewat Link iklan WA (dashboard → Link iklan WA). Panggil saat terjadi
penjualan (mis. invoice lunas) supaya tercatat sebagai "Terjual" dan — bila link memakai Meta Pixel +
Conversions API — dikirim ke Meta sebagai event Purchase.
| Field | Wajib | Keterangan |
|---|---|---|
phone |
ya | Nomor WhatsApp pelanggan (0812… atau 62812…) |
value |
ya | Nilai penjualan, mis. 350000 |
currency |
— | Default IDR |
order_id |
— | ID pesanan Anda. Kiriman ulang dengan order_id sama tidak dihitung dua kali oleh Meta |
{ "data": { "duplicate": false, "click_id": "01JAD…", "link": "Iklan Umroh Desember", "clicked_at": "2026-10-18T01:00:00Z", "sent_to_meta": true } }
404 no_tracked_click = nomor ini tidak datang dari link iklan dalam 28 hari terakhir (aman diabaikan).
Kiriman ulang dengan order_id yang sama dibalas 200 dengan "duplicate": true dan tidak dihitung lagi.
5d. Alamat kompatibel gateway lain — /api/compat/send
Untuk plugin/aplikasi yang hanya mendukung Fonnte / Wablas / Wanotif / sejenisnya tetapi URL gateway-nya
bisa diganti. Alamat lengkap: https://bales.ai.id/api/compat/send (tanpa /v1).
- API key: header
Authorization: Bearer <key>atauAuthorization: <key>(tanpa Bearer), atau parametertoken/apikey/api_key/key. - Nomor:
target/phone/number/to(beberapa nomor dipisah koma, maks. 10). - Pesan:
message/text/body/msg. - Lampiran (opsional):
url/file/media_url— gambar, atau PDF bila alamatnya berakhiran.pdf. - Boleh JSON, form, atau GET (hindari GET bila bisa: API key ikut tercatat di log server).
{ "status": true, "detail": "Pesan masuk antrian Bales.ai", "id": ["01JAE…"], "failed": [] }
Gagal → { "status": false, "reason": "…" } dengan HTTP 4xx.
Integrasi siap pakai (plugin WordPress untuk WooCommerce/Sejoli/DonasiAja/ThemeFood/Fluent/Gravity/Caldera, modul WHMCS, script Google Form, RBFund) ada di dashboard → Integrasi.
5e. Cek nomor terdaftar di WhatsApp — POST /numbers/check
Untuk memvalidasi nomor saat pelanggan mengisi form/checkout, supaya notifikasi tidak dikirim ke nomor yang salah.
{ "numbers": ["081234567890", "6285700001111"] }
Respons:
{
"data": [
{ "input": "081234567890", "number": "6281234567890", "on_whatsapp": true },
{ "input": "6285700001111", "number": "6285700001111", "on_whatsapp": false }
],
"remaining_today": 198
}
- Maks. 10 nomor per permintaan dan 200 nomor per hari per akun (
429 check_limit_reachedbila lewat). Ini bukan alat menyaring daftar kontak massal. - Butuh minimal satu nomor pengirim yang tersambung (
409 no_connected_device). - Hasil disimpan sementara (terdaftar 30 hari, tidak terdaftar 3 hari). Pesan ke nomor yang diketahui tidak terdaftar langsung
faileddenganerror_coderecipient_not_on_whatsapptanpa menyentuh WhatsApp.
6. Antrian & aturan anti-blokir
- Satu nomor hanya mengirim satu pesan dalam satu waktu, dengan jeda acak antar pesan (default 4–12 detik) dan simulasi "sedang mengetik…".
- Ada batas per menit (default 6) dan per hari (default 300). Nomor baru memakai pemanasan: 50 pesan di hari pertama, naik 25 setiap hari.
- Kalau gagal sementara, pesan dicoba lagi setelah 30 detik, 2 menit, 10 menit, lalu 30 menit (maksimal 5 percobaan).
- Penerima yang sama selalu dikirimi dari nomor pengirim yang sama.
- Menunggu giliran tidak dihitung sebagai percobaan.
⚠️ Yang membuat nomor berisiko diblokir WhatsApp:
- Mengirim ke banyak nomor yang belum pernah berinteraksi dengan Anda
- Isi pesan identik berulang dalam jumlah besar
- Banyak nomor tujuan salah atau tidak terdaftar
- Link pemendek (bit.ly dan sejenisnya)
- Menaikkan batas kirim terlalu cepat
Layanan ini hanya untuk notifikasi yang memang ditunggu penerima.
7. Webhook ke aplikasi Anda
Aktifkan di dashboard → Aplikasi & API key → buka aplikasi → bagian Webhook. Isi URL https://…
dan pilih event yang diinginkan. Setelah disimpan, salin secret lewat tombol Webhook → Lihat secret webhook.
Event
| Event | Kapan |
|---|---|
message.sent |
Diterima server WhatsApp |
message.delivered |
Sampai di HP penerima |
message.read |
Dibaca |
message.failed |
Gagal (berisi error.code) |
message.received |
Ada chat masuk ke nomor Anda (lihat di bawah ke aplikasi mana dikirim) |
ping |
Tombol "Kirim webhook uji" di dashboard |
Request yang Anda terima
POST https://aplikasi-anda.com/wa-notifikasi/webhook
Content-Type: application/json
X-WAN-Event: message.delivered
X-WAN-Delivery: 01JABC… (unik per pengiriman; dipakai untuk mencegah proses dobel)
X-WAN-Timestamp: 1760670900
X-WAN-Signature: sha256=5f2b…
{
"id": "01JABC…",
"event": "message.delivered",
"created_at": "2026-10-17T03:15:05Z",
"data": { "id": "01JABCDEF…", "status": "delivered", "to": "6281234567890", "reference": "INV-123", "...": "..." }
}
Untuk message.received, data berisi:
device_id, from, push_name, type (jenis mentah WhatsApp, sama seperti sebelumnya: conversation,
extendedTextMessage, imageMessage, …), text (teks atau caption), in_reply_to_message_id, received_at,
ditambah field baru: inbound_id (ID pesan masuk, sama dengan id di API bagian 5b), content_type
(text, image, document, audio, video, sticker, location, contact, other), dan media
(mimetype, filename; isi file tidak ikut dikirim).
Ke aplikasi mana pesan masuk dikirim?
- Kalau di dashboard → Nomor WhatsApp → nomor itu → Pesan masuk ke aplikasi Anda dipilih sebuah
aplikasi (dan webhook-nya aktif untuk
message.received), semua chat masuk ke nomor itu dikirim ke aplikasi tersebut.in_reply_to_message_idterisi kalau aplikasi itu juga yang terakhir mengirim ke kontak. - Kalau tidak dipilih, chat dikirim ke aplikasi yang terakhir mengirim pesan ke kontak itu (maks. 30 hari).
Pesan masuk juga bisa dibaca lewat API (bagian 5b).
Verifikasi tanda tangan (WAJIB)
signature = "sha256=" + HMAC_SHA256(secret, X-WAN-Timestamp + "." + raw_body)
- Pakai body mentah apa adanya, sebelum di-decode JSON.
- Bandingkan dengan
hash_equals(perbandingan constant-time). - Tolak request kalau
X-WAN-Timestamplebih tua dari 5 menit. - Balas HTTP 2xx dalam 15 detik. Proses yang berat sebaiknya dikerjakan di antrian Anda sendiri.
Kalau tidak dibalas 2xx, webhook dicoba ulang setelah 10 detik, 1 menit, 5 menit, 30 menit, dan 2 jam
(maksimal 6 kali). Riwayat dan tombol Kirim ulang ada di dashboard. Webhook bisa datang lebih dari
sekali atau tidak berurutan, jadi pakai X-WAN-Delivery dan bandingkan status.
8. Contoh pemanggilan
curl
curl -X POST https://bales.ai.id/api/v1/messages \
-H "Authorization: Bearer $WA_NOTIFIKASI_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-INV-123-paid" \
-d '{
"to": "081234567890",
"text": "Pembayaran invoice INV-123 sebesar Rp150.000 sudah kami terima. Terima kasih 🙏",
"reference": "INV-123"
}'
curl https://bales.ai.id/api/v1/messages/01JABCDEF0123456789XYZABCD \
-H "Authorization: Bearer $WA_NOTIFIKASI_KEY"
Kirim PDF:
curl -X POST https://bales.ai.id/api/v1/messages \
-H "Authorization: Bearer $WA_NOTIFIKASI_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+62 812 3456 7890",
"text": "Invoice Anda terlampir.",
"media": { "type": "document", "url": "https://contoh.com/invoice/INV-123.pdf", "filename": "INV-123.pdf" },
"reference": "INV-123"
}'
Laravel
Salin clients/laravel/WaNotifikasi.php ke app/Services/WaNotifikasi.php, lalu tambahkan ke config/services.php:
'wa_notifikasi' => [
'url' => env('WA_NOTIFIKASI_URL', 'https://bales.ai.id'),
'key' => env('WA_NOTIFIKASI_KEY'),
'webhook_secret' => env('WA_NOTIFIKASI_WEBHOOK_SECRET'),
],
use App\Services\WaNotifikasi;
$result = app(WaNotifikasi::class)->sendText(
to: $order->customer_phone,
text: "Pembayaran {$order->number} sudah kami terima. Terima kasih!",
reference: $order->number,
idempotencyKey: "order-{$order->id}-paid",
);
$order->update(['wa_message_id' => $result['id']]);
Untuk OTP: ->sendText($phone, "Kode OTP Anda {$code}. Berlaku 5 menit.", expiresIn: 300).
Penerima webhook (route tanpa CSRF, mis. di routes/api.php):
Route::post('/wa-notifikasi/webhook', function (Illuminate\Http\Request $request) {
abort_unless(App\Services\WaNotifikasi::verifyWebhook($request), 401);
$event = $request->json('event');
$data = $request->json('data');
if (str_starts_with($event, 'message.') && isset($data['id'])) {
\App\Models\Order::where('wa_message_id', $data['id'])->update(['wa_status' => $data['status']]);
}
return response()->noContent();
});
PHP biasa / WordPress
Pasang plugin clients/wordpress/wa-notifikasi-client.zip (upload lewat
Plugins → Add New → Upload Plugin). Isi URL dan API key di Settings → WA Notifikasi.
// Di tema / plugin lain:
$result = wan_send_whatsapp('081234567890', 'Donasi Anda sebesar Rp100.000 sudah kami terima. Jazakallahu khairan.', [
'reference' => 'DON-' . $donation_id,
'idempotency_key' => 'donation-' . $donation_id . '-paid',
]);
if (is_wp_error($result)) {
error_log('WA gagal: ' . $result->get_error_message());
}
Tanpa plugin, cukup pakai wp_remote_post biasa:
$response = wp_remote_post('https://bales.ai.id/api/v1/messages', [
'timeout' => 15,
'headers' => [
'Authorization' => 'Bearer ' . WA_NOTIFIKASI_KEY, // definisikan di wp-config.php
'Content-Type' => 'application/json',
'Idempotency-Key' => 'donation-' . $donation_id . '-paid',
],
'body' => wp_json_encode(['to' => $phone, 'text' => $text]),
]);
Verifikasi webhook di PHP biasa:
$body = file_get_contents('php://input');
$ts = (int) ($_SERVER['HTTP_X_WAN_TIMESTAMP'] ?? 0);
$sig = $_SERVER['HTTP_X_WAN_SIGNATURE'] ?? '';
$ok = abs(time() - $ts) <= 300
&& hash_equals('sha256=' . hash_hmac('sha256', $ts . '.' . $body, WA_NOTIFIKASI_WEBHOOK_SECRET), $sig);
if (! $ok) { http_response_code(401); exit; }
$payload = json_decode($body, true);
http_response_code(204);