Dokumentasi API

Kirim notifikasi WhatsApp dari aplikasi Anda lewat REST API. Base URL: https://bales.ai.id/api/v1

1 Daftar & sambungkan nomor Buat akun, lalu di dashboard tambah nomor WhatsApp dan scan QR.
2 Buat API key Menu Aplikasi & API key → buat aplikasi → salin key (hanya tampil sekali).
3 Kirim pesan pertama POST /messages dengan header Authorization: Bearer …

Gateway WhatsApp untuk notifikasi transaksi: invoice, pembayaran, pengingat, dan OTP. Layanan ini bukan untuk broadcast atau promosi.


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:

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

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:

{
  "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).

{ "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
}

6. Antrian & aturan anti-blokir

⚠️ Yang membuat nomor berisiko diblokir WhatsApp:

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?

  1. 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_id terisi kalau aplikasi itu juga yang terakhir mengirim ke kontak.
  2. 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)
  1. Pakai body mentah apa adanya, sebelum di-decode JSON.
  2. Bandingkan dengan hash_equals (perbandingan constant-time).
  3. Tolak request kalau X-WAN-Timestamp lebih tua dari 5 menit.
  4. 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);
Chat WA