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." }
CodeHTTPKapan muncul
invalid_api_key401Header auth kosong, key salah/kedaluwarsa/dicabut
account_inactive403Akun suspended atau cancelled
device_limit_reached402Sudah di batas max_devices paketmu
quota_exceeded402Kuota pesan bulanan habis
device_not_found404device_uuid tidak ada di akunmu
device_disconnected409Device belum/tidak connected
already_connected409Minta QR padahal device sudah tersambung
idempotency_key_required422POST /messages tanpa header Idempotency-Key
rate_limited429Melebihi batas request per menit paketmu di POST /messages
interactive_not_supported422Kirim button/list ke device jalur hemat — fitur ini butuh engine=meta_cloud
interactive_body_required422Button/list tanpa body (teks di atas pilihan)
invalid_phone422Nomor tujuan < 8 digit setelah dibersihkan
invalid_meta_credentials422Kredensial Meta gagal diverifikasi ke Graph API
not_meta_cloud_device422Rotasi token Meta dipanggil ke device jalur hemat
no_recipients_targeted422Broadcast tanpa tags maupun contact_ids
no_recipients_matched422Target broadcast tidak cocok kontak mana pun
broadcast_not_paused422Resume broadcast yang statusnya bukan paused
device_not_connected409Resume 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.

FieldBatas
buttons1–3 tombol; title maks 20 karakter, id maks 256
sections1–10 section; title section maks 24 karakter
rows1–10 per section; title maks 24, description maks 72
header / footermaks 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: queuedsentdeliveredread, 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) atau contact_ids — wajib salah satu, dan harus cocok minimal 1 kontak.
  • message_template mendukung spintax {a|b|c} (dipilih acak per penerima) dan variabel {nama}.
  • Tanpa scheduled_at → langsung jalan lewat antrian (respons tidak menunggu selesai). Dengan scheduled_atstatus=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-Key itu 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.