Panduan
Referensi API v1
Base URL https://wardia.id/v1. Semua endpoint butuh header Authorization: Bearer ardw_live_xxx. Semua respons JSON.
Error codes
Format error seragam:
{ "error": "invalid_api_key", "message": "Invalid, expired, or revoked API key." }
| Code | HTTP | Kapan muncul |
|---|---|---|
invalid_api_key | 401 | Header auth kosong, key salah/kedaluwarsa/dicabut |
account_inactive | 403 | Akun suspended atau cancelled |
device_limit_reached | 402 | Sudah di batas max_devices paketmu |
quota_exceeded | 402 | Kuota pesan bulanan habis |
device_not_found | 404 | device_uuid tidak ada di akunmu |
device_disconnected | 409 | Device belum/tidak connected |
already_connected | 409 | Minta QR padahal device sudah tersambung |
idempotency_key_required | 422 | POST /messages tanpa header Idempotency-Key |
rate_limited | 429 | Melebihi batas request per menit paketmu di POST /messages |
interactive_not_supported | 422 | Kirim button/list ke device jalur hemat — fitur ini butuh engine=meta_cloud |
interactive_body_required | 422 | Button/list tanpa body (teks di atas pilihan) |
invalid_phone | 422 | Nomor tujuan < 8 digit setelah dibersihkan |
invalid_meta_credentials | 422 | Kredensial Meta gagal diverifikasi ke Graph API |
not_meta_cloud_device | 422 | Rotasi token Meta dipanggil ke device jalur hemat |
no_recipients_targeted | 422 | Broadcast tanpa tags maupun contact_ids |
no_recipients_matched | 422 | Target broadcast tidak cocok kontak mana pun |
broadcast_not_paused | 422 | Resume broadcast yang statusnya bukan paused |
device_not_connected | 409 | Resume broadcast tapi device belum tersambung lagi |
Error validasi field biasa (field wajib kosong, format salah) balik 422 dengan bentuk Laravel standar {"message": "...", "errors": {"field": ["..."]}} — bukan format {error, message} di atas.
Devices
POST /v1/devices — buat device
Jalur hemat (default, pairing lewat QR):
{ "label": "toko-sari-admin" }
→ 201 { "device_uuid": "388d0f3a-...", "status": "provisioned" }
Jalur WhatsApp Cloud API resmi Meta — pakai WABA sendiri, tanpa QR:
{ "engine": "meta_cloud",
"label": "toko-sari-admin",
"meta_phone_number_id": "123456789012345",
"meta_access_token": "EAAxxxxxxxxxxxx" }
→ 201 { "device_uuid": "...", "status": "connected",
"phone_number": "+62 812-9000-8646", "verified_name": "Toko Sari" }
Kredensial diverifikasi live ke Graph API sebelum device dibuat — gagal verifikasi berarti device tidak jadi dibuat (422 invalid_meta_credentials), bukan dibuat dengan status rusak. Satu meta_phone_number_id hanya boleh dipasang di satu device.
GET /v1/devices · GET /v1/devices/{uuid}
→ 200 { "data": [ { "device_uuid", "label", "phone_number", "engine",
"status", "connected_at", "last_health_at" } ] }
status: provisioned · qr_pending · connected · disconnected · banned.
GET /v1/devices/{uuid}/qr — QR pairing
Hanya untuk device jalur hemat. 202 = QR belum siap (polling lagi), 200 = qr berisi data URL PNG base64 berumur ~90 detik, 409 already_connected = sudah tersambung.
PATCH /v1/devices/{uuid}/meta-credentials — rotasi token
{ "meta_access_token": "EAA-token-baru" }
Token baru diverifikasi ulang ke Graph API sebelum disimpan.
DELETE /v1/devices/{uuid} — logout & lepas device
→ 200 { "device_uuid": "...", "status": "disconnected" }
Messages
POST /v1/messages — kirim pesan
Header Idempotency-Key wajib. Kirim ulang dengan key sama akan mengembalikan pesan yang sudah ada, bukan mengirim dobel.
{ "device_uuid": "388d0f3a-...",
"to": "6281290008646",
"type": "text",
"body": "Halo, pesanan Anda sudah dikirim.",
"media_url": null }
→ 202 { "id": 42, "status": "queued", "wa_message_id": null,
"direction": "out", "type": "text", "created_at": "..." }
type: text (default) · image · document · audio · video. Untuk media, isi media_url dengan URL yang bisa kami unduh — body jadi caption. to boleh format apa saja (0812…, +62 812…); kami bersihkan ke digit lalu validasi minimal 8 digit.
Rate limit
Endpoint ini dibatasi per menit per akun sesuai paketmu (Starter 10/menit, Business 20, Pro 40). Setiap respons membawa sisa jatahmu:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
Lewat batas → 429 plus header Retry-After berisi detik sampai jendela berikutnya:
HTTP/1.1 429 Too Many Requests
Retry-After: 41
{ "error": "rate_limited",
"message": "Rate limit of 10 requests per minute exceeded for this plan. Retry in 41s." }
Jendelanya 60 detik dan tidak bergulir mundur — tunggu Retry-After, jangan retry ketat. Request yang kena 429 tidak membuat pesan dan tidak memotong kuota bulanan, jadi aman dikirim ulang. Kalau butuh throughput lebih tinggi dari paketmu, naikkan paket atau pakai POST /v1/broadcasts — jalur broadcast memang dirancang untuk volume, lengkap dengan jeda acak anti-banned.
Endpoint baca (GET /messages/{id}, /usage, /contacts) tidak kena batas ini — polling status sesering yang kamu butuh.
Button & list message
Kirim pilihan yang bisa di-tap pelanggan, bukan menyuruh mereka mengetik. Hanya untuk device engine=meta_cloud — device jalur hemat ditolak 422 interactive_not_supported. WhatsApp membatasi tombol di klien non-official, dan memaksakannya justru menaikkan risiko banned, jadi kami tidak menawarkannya di sana.
Tombol balasan (maksimum 3):
{ "device_uuid": "388d0f3a-...",
"to": "6281290008646",
"type": "button",
"body": "Pesanan #1042 sudah sampai?",
"interactive": {
"footer": "Toko Sari",
"buttons": [
{ "id": "yes", "title": "Sudah" },
{ "id": "no", "title": "Belum" }
]
} }
Daftar pilihan (maksimum 10 section, 10 baris per section):
{ "device_uuid": "388d0f3a-...",
"to": "6281290008646",
"type": "list",
"body": "Mau lihat produk yang mana?",
"interactive": {
"button_text": "Lihat katalog",
"sections": [
{ "title": "Skincare",
"rows": [
{ "id": "sku-1", "title": "Serum", "description": "30ml" },
{ "id": "sku-2", "title": "Toner" }
] }
]
} }
body wajib — itu teks yang muncul di atas pilihan. interactive.header dan interactive.footer opsional.
| Field | Batas |
|---|---|
buttons | 1–3 tombol; title maks 20 karakter, id maks 256 |
sections | 1–10 section; title section maks 24 karakter |
rows | 1–10 per section; title maks 24, description maks 72 |
header / footer | maks 60 karakter |
Batas-batas itu batas WhatsApp; kami tolak 422 di sini supaya kamu dapat pesan error yang jelas, bukan error mentah dari Meta setelah pesan telanjur diantrikan.
Saat pelanggan menekan pilihan
Tap tombol/list masuk ke webhook message.received sebagai pesan teks biasa berisi judul yang dipilih (mis. "Sudah"). Artinya rule auto-reply keyword-mu langsung bekerja tanpa kode tambahan — perlakukan sama seperti pelanggan mengetik teks itu. Id pilihan (yes/sku-1) ikut tersimpan di sisi kami, tapi belum ikut dikirim di payload webhook — kalau kamu butuh membedakan dua tombol berjudul sama, beri judul yang berbeda dulu.
GET /v1/messages/{id} — cek status
Bentuk sama dengan respons di atas, dengan status terbaru: queued → sent → delivered → read, atau failed. wa_message_id terisi begitu terkirim.
Contacts
POST /v1/contacts — buat/update kontak
{ "phone": "081290008646", "name": "Budi", "email": null,
"tags": ["vip", "reseller"] }
→ 201 { "id": 9, "phone": "6281290008646" } # 200 kalau kontak sudah ada
Upsert berdasarkan nomor — panggil berkali-kali aman, tidak bikin duplikat. Tag yang belum ada dibuat otomatis.
GET /v1/contacts — list kontak
Query opsional: ?tag=vip, ?per_page=50 (default 50, maksimum 200). Respons adalah paginator Laravel standar (data, current_page, last_page, total) dan tiap item menyertakan relasi tags.
Broadcasts
POST /v1/broadcasts — buat broadcast
{ "device_uuid": "388d0f3a-...",
"name": "Promo Agustus",
"message_template": "Halo {nama}, ada {promo|diskon|penawaran} spesial buat kamu!",
"tags": ["vip"],
"delay_min_seconds": 5,
"delay_max_seconds": 20,
"scheduled_at": null }
→ 201 { "id": 12, "status": "draft", "total_recipients": 50 }
- Target:
tags(nama tag) ataucontact_ids— wajib salah satu, dan harus cocok minimal 1 kontak. message_templatemendukung spintax{a|b|c}(dipilih acak per penerima) dan variabel{nama}.- Tanpa
scheduled_at→ langsung jalan lewat antrian (respons tidak menunggu selesai). Denganscheduled_at→status=scheduled, dijalankan penjadwal. - Jeda acak antar pesan adalah guardrail anti-banned. Default 5–20 detik kalau tidak diisi.
GET /v1/broadcasts/{id} — status & laporan
→ 200 { "id": 12, "name": "Promo Agustus", "status": "running",
"total_recipients": 50, "sent_count": 30, "failed_count": 2,
"recipients_by_status": { "pending": 18, "sent": 30, "failed": 2 },
"scheduled_at": null, "started_at": "...", "completed_at": null }
POST /v1/broadcasts/{id}/resume — lanjutkan yang auto-pause
Kalau device terputus di tengah broadcast, broadcast otomatis paused dan penerima sisanya tetap pending. Resume hanya jalan kalau statusnya paused dan device sudah tersambung lagi. Hitungan total_recipients dan started_at tidak direset.
Usage
GET /v1/usage — sisa kuota bulan berjalan
→ 200 { "period": "2026-07", "messages_sent": 1230, "messages_quota": 5000,
"messages_remaining": 3770, "api_calls": 1450 }
messages_remaining: null berarti tanpa batas (paket tanpa kuota).
Catatan praktis
Idempotency-Keyitu wajib, beda dari banyak API lain yang menjadikannya opsional. Generate per pesan logis ({sumber}-{id_internal}), bukan per percobaan HTTP.- Dua batas berbeda berlaku bersamaan.
429 rate_limited= batas request per menit dari paketmu (kontrak komersial).402 quota_exceeded= kuota pesan bulanan habis. Di jalur broadcast ada lapis ketiga: guardrail anti-banned per device (throttle sesuai umur nomor + jeda acak) yang menahan pengiriman, bukan menolaknya. - Broadcast di device Meta Cloud secara kebijakan Meta hanya boleh pakai template yang sudah disetujui. Endpoint broadcast belum menolak free-text untuk device Meta di sisi kami — kegagalannya baru muncul saat pengiriman ke Graph API.