Dokumentasi API

Referensi lengkap REST API WA Client 1 — gateway WhatsApp multi-nomor dari PT Surya Digital Nusantara. Satu akun bisa mengelola beberapa nomor WhatsApp sekaligus, masing-masing dengan API key, riwayat chat, dan webhook sendiri yang terisolasi penuh dari nomor/akun lain.

Gambaran Umum

Base URL untuk seluruh endpoint di bawah ini:

https://pesan-client1.suryacloud.my.id

Setiap request dan response menggunakan JSON (content-type: application/json), kecuali endpoint upload media yang menerima multipart/form-data.

Model multi-nomor: satu akun dapat menghubungkan beberapa nomor WhatsApp. Setiap nomor punya API key (bisa lebih dari satu sekaligus, lihat bagian Autentikasi), riwayat percakapan sendiri, dan konfigurasi webhook sendiri — tidak ada endpoint yang mengembalikan data lintas-nomor atau lintas-akun.

Mulai Cepat

  1. Login dengan email & nomor WhatsApp aktif (verifikasi kode OTP) — akun dibuat otomatis saat pertama kali login.
  2. Di halaman Nomor Saya, klik + Tambah Nomor dan beri nama.
  3. Salin API key yang ditampilkan — hanya muncul sekali saat itu juga.
  4. Buka dashboard nomor tersebut, scan QR dari WhatsApp di HP (Perangkat Tertaut → Tautkan Perangkat).
  5. Panggil endpoint di bawah menggunakan API key nomor tersebut.

Autentikasi

Endpoint pengiriman pesan (/api/send/*, /api/groups, /api/connection/*, /api/webhooks/*, /media/*) diautentikasi dengan API key per-nomor, dikirim lewat header x-api-key (atau parameter query ?key= jika header tidak memungkinkan, mis. embed <img>).

x-api-key: API_KEY_NOMOR_ANDA
Bisa punya lebih dari satu key: satu nomor bisa punya beberapa API key aktif sekaligus — kelola di halaman API Keys pada dashboard nomor tersebut. Berguna untuk memberi key terpisah ke tiap integrasi/sistem yang memakai nomor yang sama, supaya satu key bisa dicabut kapan saja tanpa mematikan integrasi lain, dan supaya jelas key mana yang dipakai oleh sistem yang mana. Semua key aktif pada satu nomor punya akses penuh yang sama ke nomor itu — tidak ada perbedaan hak akses antar key.
Penting: API key hanya ditampilkan penuh satu kali — saat nomor dibuat, atau saat Anda membuat key baru di halaman API Keys. Setelahnya hanya beberapa karakter awal yang terlihat. Simpan key di tempat aman (secret manager / env var), bukan di kode sumber. Key yang hilang tidak bisa ditampilkan ulang — buat key baru dan cabut yang lama.
Header, bukan query param, kecuali untuk media: gunakan x-api-key di setiap pemanggilan API dari server/skrip Anda. ?key= hanya dimaksudkan untuk kasus embed media (<img>/<video> src) yang tidak bisa mengirim header — nilai di query string berisiko tercatat di access log atau bocor lewat header Referer, jadi jangan pakai bentuk ini di request server-to-server biasa.

Login dashboard (halaman web) menggunakan email & nomor WhatsApp aktif (verifikasi kode OTP), terpisah sepenuhnya dari API key — API key tidak pernah digunakan untuk login ke dashboard, dan sebaliknya.

POST/api/send/text🔑 API key

Mengirim pesan teks ke nomor personal atau grup.

FieldTipeKeterangan
tostringNomor tujuan (mis. 628123456789) atau JID grup (...@g.us).
messagestringIsi pesan teks.
idempotencyKeystringOpsional. Kirim ID unik milik Anda sendiri (mis. ID transaksi/notifikasi di sistem Anda) — kalau request dengan idempotencyKey yang sama dikirim ulang (misal karena timeout lalu retry), WA Client 1 tidak akan mengirim pesan dua kali, cukup mengembalikan hasil pengiriman yang pertama. Sangat disarankan dipakai untuk sistem otomatis (reminder, notifikasi terjadwal, dsb.) yang mungkin melakukan retry.
Waktu respons & timeout: pengiriman normal biasanya selesai dalam hitungan detik, tapi bisa lebih lambat kalau WhatsApp sedang lambat merespons di sisi mereka. Kami membatasi waktu tunggu respons di server kami sendiri ke ~20 detik — kalau tercapai, Anda akan menerima 504 (bukan diam tanpa respons). Proses pengiriman tetap berjalan di belakang layar walau responsnya sudah 504. Set timeout client Anda ke minimal 30 detik, dan kalau menerima 504: kirim ulang request yang sama dengan idempotencyKey yang sama persis — Anda tidak akan pernah mengirim pesan kedua, dan tidak perlu menebak apakah pesan pertama terkirim.

Saat memeriksa ulang, ada dua kemungkinan jawaban: kalau pengiriman sudah selesai, Anda langsung mendapat hasil akhirnya (200 berisi messageId, atau error yang sebenarnya). Kalau masih diproses, Anda mendapat 504 lagi tapi seketika — bukan setelah menunggu 20 detik lagi — beserta header retry-after yang menunjukkan kapan sebaiknya diperiksa lagi. Jadi memeriksa berulang kali aman dan tidak membuat sistem Anda menggantung.
curl -X POST https://pesan-client1.suryacloud.my.id/api/send/text \
  -H "x-api-key: API_KEY_ANDA" \
  -H "content-type: application/json" \
  -d '{"to":"628123456789","message":"Halo dari WA Client 1","idempotencyKey":"reminder-2026-08-16-ainun"}'
const res = await fetch('https://pesan-client1.suryacloud.my.id/api/send/text', {
  method: 'POST',
  headers: { 'x-api-key': 'API_KEY_ANDA', 'content-type': 'application/json' },
  body: JSON.stringify({ to: '628123456789', message: 'Halo dari WA Client 1' }),
});
import requests

requests.post(
    'https://pesan-client1.suryacloud.my.id/api/send/text',
    headers={'x-api-key': 'API_KEY_ANDA'},
    json={'to': '628123456789', 'message': 'Halo dari WA Client 1'},
)

Response 200

{ "success": true, "messageId": "3EB0..." }
POST/api/send/media🔑 API key

Mengirim gambar, video, dokumen, atau audio. Terima file langsung (multipart/form-data) atau URL media yang sudah publik.

FieldTipeKeterangan
tostringNomor atau JID grup tujuan.
filefileUpload langsung (opsional jika mediaUrl diisi).
mediaUrlstringURL http(s) ke file media (opsional jika file diisi).
typestringimage / video / document / audio — wajib jika pakai mediaUrl, opsional (auto-deteksi dari mimetype) jika upload file.
captionstringOpsional, teks penyerta.
fileNamestringOpsional, nama file untuk dokumen.
idempotencyKeystringOpsional, sama seperti pada /api/send/text di atas — mencegah kiriman ganda kalau request yang sama di-retry.
Waktu respons & perilaku 504 sama seperti /api/send/text — lihat catatan di bagian di atas.
curl -X POST https://pesan-client1.suryacloud.my.id/api/send/media \
  -H "x-api-key: API_KEY_ANDA" \
  -F "to=628123456789" \
  -F "file=@/path/ke/gambar.jpg" \
  -F "caption=Contoh gambar"
const form = new FormData();
form.append('to', '628123456789');
form.append('mediaUrl', 'https://contoh.com/gambar.jpg');
form.append('type', 'image');
await fetch('https://pesan-client1.suryacloud.my.id/api/send/media', {
  method: 'POST', headers: { 'x-api-key': 'API_KEY_ANDA' }, body: form,
});
GET/api/groups🔑 API key

Daftar grup WhatsApp yang diikuti nomor ini.

FieldTipeKeterangan
limitnumberOpsional, query param. Jumlah maksimum grup per halaman (default 100, maksimum 500).
offsetnumberOpsional, query param. Lewati sekian grup pertama (default 0) — dipakai bersama limit untuk paginasi.
{ "groups": [ { "id": "1234-5678@g.us", "subject": "Tim Support", "participants": 12 } ], "total": 34, "limit": 100, "offset": 0 }
GET/api/connection/status🔑 API key

Status koneksi WhatsApp nomor ini, termasuk QR code kalau sedang menunggu discan. Dipakai untuk membangun halaman "Hubungkan WhatsApp" sendiri di sistem Anda (mis. panel admin sekolah) tanpa perlu membuka dashboard WA Client 1 — panggil endpoint ini setiap ~3 detik selama proses pairing untuk menampilkan QR terbaru secara realtime, sama seperti yang dilakukan dashboard kami sendiri.

qr sudah berupa data:image/png;base64,... — tinggal dipasang langsung ke <img src>, tidak perlu diolah lagi. Bernilai null kalau tidak sedang menunggu scan (misal sudah open, atau baru connecting).
Jangan panggil dari JavaScript browser: endpoint ini (dan semua endpoint 🔑 API key lainnya) harus dipanggil dari backend Anda sendiri, bukan langsung dari halaman admin di browser — API key yang ditaruh di kode sisi client bisa diambil siapa saja yang membuka DevTools. Backend Anda yang memanggil WA Client 1, lalu meneruskan hasilnya (QR + status) ke halaman admin Anda lewat mekanisme Anda sendiri (polling ke endpoint Anda sendiri, misalnya).
FieldTipeKeterangan
statestringconnecting / qr / open / close.
qrstring|nullData URL QR code, hanya terisi saat state = qr.
connectedNumberstring|nullNomor HP yang tersambung, kalau state = open.
staleRescanNeededbooleantrue kalau watchdog otomatis kami yang menghentikan koneksi ini (lihat FAQ) — perlu /api/connection/logout lalu scan ulang, bukan sekadar /restart.
autoRetryGivenUpbooleantrue kalau percobaan sambung-ulang otomatis sudah menyerah — perlu /api/connection/restart manual.
pairingStoppedbooleantrue kalau QR tidak discan selama beberapa kali masa berlaku QR berturut-turut (±14 menit) dan sistem berhenti menampilkan QR baru terus-menerus. Sesi tidak dihapus — cukup panggil /api/connection/restart saat siap scan, dan QR baru akan muncul di /api/connection/status. Status ini bertahan walau server di-restart. Event webhook device.disconnected menyertakan reason: "pairing_timeout" dan needsQrScan: true saat ini terjadi.
curl https://pesan-client1.suryacloud.my.id/api/connection/status \
  -H "x-api-key: API_KEY_ANDA"
const res = await fetch('https://pesan-client1.suryacloud.my.id/api/connection/status', {
  headers: { 'x-api-key': 'API_KEY_ANDA' },
});
const { state, qr, connectedNumber } = await res.json();

Response 200

{ "state": "qr", "qr": "data:image/png;base64,...", "connectedNumber": null, "lastDisconnectCode": null, "reconnectAttempts": 0, "autoRetryGivenUp": false, "staleRescanNeeded": false, "pairingStopped": false }
POST/api/connection/restart🔑 API key

Sambung ulang pakai sesi yang sama (tidak perlu scan QR) — dipakai kalau koneksi terlihat macet, sama seperti tombol Restart Koneksi di dashboard. Selesai dalam beberapa detik di belakang layar — poll /api/connection/status setelahnya untuk melihat hasilnya, respons endpoint ini sendiri tidak menunggu proses reconnect selesai.

POST/api/connection/logout🔑 API key

Logout penuh — menghapus sesi tersimpan, sama seperti tombol Logout & Scan Ulang di dashboard. Setelah ini /api/connection/status akan mulai mengembalikan QR baru untuk discan lagi. Gunakan hanya saat benar-benar perlu re-pairing (mis. staleRescanNeeded bernilai true) — jangan dipanggil rutin, karena ini memutus koneksi yang sedang aktif.

{ "success": true }

Webhook

Dikelola dari halaman Webhook di dashboard nomor Anda, atau lewat API key (lihat di bawah) — misalnya untuk mendaftarkan webhook otomatis begitu integrasi Anda sendiri berhasil terhubung, tanpa langkah manual di dashboard. Satu nomor bisa punya lebih dari satu tujuan webhook sekaligus — misalnya satu ke Google Apps Script untuk otomasi, satu lagi ke server OTP login — setiap event dikirim ke semua endpoint yang aktif secara paralel dan independen. Tiap endpoint punya URL dan secret HMAC-nya sendiri, jadi merotasi atau menghapus satu integrasi tidak memengaruhi yang lain.

Kelola Webhook Lewat API

MethodPathKeterangan
GET/api/webhooksDaftar semua endpoint webhook nomor ini.
POST/api/webhooksDaftarkan endpoint baru. Body: { "label": "...", "url": "https://..." }. Response 201 berisi secret HMAC — simpan, dipakai untuk verifikasi signature (lihat bagian Event & Signature di bawah).
PATCH/api/webhooks/:idUbah label / url / enabled sebagian (kirim hanya field yang mau diubah).
POST/api/webhooks/:id/rotate-secretTerbitkan secret HMAC baru untuk endpoint ini (yang lama langsung tidak berlaku).
POST/api/webhooks/:id/testKirim satu payload uji coba bertanda tangan ke endpoint ini sekarang juga, untuk memastikan verifikasi signature di sisi Anda benar sebelum mengandalkannya untuk event sungguhan.
DELETE/api/webhooks/:idHapus endpoint webhook.
URL webhook divalidasi sama seperti dari dashboard (tidak boleh mengarah ke alamat privat/internal — lihat proteksi SSRF) — URL yang mengarah ke jaringan lokal/internal akan ditolak dengan 400.
curl -X POST https://pesan-client1.suryacloud.my.id/api/webhooks \
  -H "x-api-key: API_KEY_ANDA" \
  -H "content-type: application/json" \
  -d '{"label":"Sistem Internal","url":"https://contoh.com/webhook/whatsapp"}'

Response 201

{ "endpoint": { "id": "...", "label": "Sistem Internal", "url": "https://contoh.com/webhook/whatsapp", "secret": "...", "enabled": true } }

Event yang Dikirim

EventKapan Terjadi
message.incomingAda pesan masuk (personal atau grup).
message.statusStatus pesan keluar berubah (sent / delivered / read / failed).
identity.mergedKontak yang sebelumnya dikenali lewat @lid berhasil diketahui nomor HP aslinya (lihat penjelasan LID di bawah) — pakai ini untuk menggabungkan kontak/thread lama Anda.
device.connectedNomor berhasil terhubung ke WhatsApp.
device.disconnectedKoneksi terputus (cek field needsQrScan).

Setiap payload otomatis disisipi numberId dan phoneNumber milik nomor WA Client1 Anda sendiri (bukan lawan bicara) — jadi satu URL webhook bisa dipakai untuk beberapa nomor sekaligus dan tetap bisa dibedakan asalnya. Untuk identitas pengirim/lawan bicara, selalu pakai chatId/from, bukan phoneNumber di level atas ini.

{
  "event": "message.incoming",
  "chatId": "628123456789@s.whatsapp.net",
  "from": "628123456789@s.whatsapp.net",
  "isGroup": false,
  "isFromMe": false,
  "type": "text",
  "text": "Halo, ada info?",
  "numberId": "b3f1...",
  "phoneNumber": "6281234567890"
}
Penting — identitas pengirim bisa berupa LID, bukan nomor HP. Untuk sebagian kontak (fitur privasi WhatsApp terbaru), chatId/from pada message.incoming bisa berisi identitas WhatsApp internal berformat <angka-acak>@lid alih-alih <nomor-hp>@s.whatsapp.net — biasanya untuk kontak yang belum pernah mengirim lewat nomor HP aslinya dan belum tersimpan di address book nomor WA Client1 Anda. Ini bukan bug pengiriman: WhatsApp memang tidak selalu membocorkan nomor HP asli kontak seperti itu di level protokol, bahkan aplikasi WhatsApp resmi si pengirim pun menampilkannya sebagai kontak terpisah sampai nomornya "ketemu".

WA Client1 otomatis meresolusi @lid → nomor HP asli begitu nomornya diketahui (lewat sinkronisasi kontak, atau kontak tsb membagikan nomornya), lalu memakai nomor asli itu untuk seluruh pesan berikutnya. Kalau resolusi ini terjadi setelah Anda sudah menerima satu/lebih message.incoming dengan chatId ber-@lid, Anda akan menerima event identity.merged berisi oldChatId (yang lama, @lid) dan newChatId (nomor HP asli) — pakai ini untuk menggabungkan kontak/thread yang sempat tercatat terpisah di sisi Anda. Jangan asumsikan chatId/from selalu berformat @s.whatsapp.net, dan jangan pakai field phoneNumber di level atas sebagai pengganti — field itu identitas nomor WA Client1 Anda sendiri, bukan lawan bicara.

Contoh payload message.status — perhatikan nama field-nya chatId, bukan to:

{
  "event": "message.status",
  "messageId": "3EB0...",
  "chatId": "628123456789@s.whatsapp.net",
  "status": "delivered",
  "timestamp": 1786450000,
  "numberId": "b3f1...",
  "phoneNumber": "6281234567890"
}

Contoh payload identity.merged:

{
  "event": "identity.merged",
  "oldChatId": "44294803030187@lid",
  "newChatId": "628123456789@s.whatsapp.net",
  "numberId": "b3f1...",
  "phoneNumber": "6281234567890"
}

Verifikasi Signature

Setiap request webhook menyertakan header x-webhook-signature berisi HMAC-SHA256 dari body mentah, ditandatangani dengan secret milik endpoint tujuan itu sendiri (beda dari API key, dan beda per endpoint kalau Anda punya lebih dari satu — lihat halaman Webhook di dashboard).

const crypto = require('crypto');

function isValid(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
import hmac, hashlib

def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)
Selalu verifikasi signature sebelum memproses payload — jangan percaya isi numberId/phoneNumber di body tanpa verifikasi terlebih dahulu.

Verifikasi Kepemilikan Nomor WhatsApp

Pola siap-pakai untuk kasus yang sangat umum: sistem Anda punya form pendaftaran dengan kolom nomor WhatsApp, dan Anda perlu membuktikan pengisinya benar-benar menguasai nomor itu — sebelum mengandalkannya untuk kirim info penting (undangan interview, status pesanan, kode akses, dsb). Pola di bawah ini dipakai secara produksi oleh salah satu integrasi kami dan terbukti jauh lebih aman untuk reputasi nomor Anda dibanding OTP biasa.

Kenapa bukan OTP biasa

OTP klasik (sistem Anda kirim kode duluan ke nomor yang baru didaftarkan) terlihat aman, tapi punya risiko tersembunyi kalau volume pendaftaran cukup ramai: nomor WA Client 1 Anda jadi pihak yang memulai kontak ke ribuan nomor baru yang belum pernah membalas apa pun. Pola itu — banyak pesan keluar, nyaris nol balasan — adalah salah satu sinyal terkuat yang dipakai sistem anti-spam WhatsApp. Ini bukan teori: salah satu nomor produksi di gateway ini pernah di-logout paksa oleh WhatsApp (kode 401, perlu scan ulang) setelah mengirim pesan massal ke banyak penerima sekaligus. Integrasi tersebut lalu memilih pola terbalik di bawah ini supaya pola berisiko yang sama tidak berulang setiap kali ada pendaftar baru.

Aturan praktis: nomor WA Client 1 Anda sebaiknya tidak pernah menjadi pihak yang memulai percakapan ke alamat yang belum pernah membalas — baik untuk OTP maupun broadcast. Selalu ada pola yang membuat lawan bicara mengirim duluan.

Pola yang direkomendasikan: OTP terbalik

Alih-alih Anda mengirim kode ke pengguna, pengguna yang mengirim kode ke Anda:

  1. Sistem Anda membuat token acak sekali-pakai per pengguna (contoh: SBWVERIF-a1b2c3d4e5 — prefiks tetap yang mudah Anda kenali lewat regex, plus bagian acak yang cukup panjang untuk tidak bisa ditebak).
  2. Tampilkan tombol/link wa.me yang sudah terisi otomatis dengan token itu, mengarah ke nomor WA Client 1 Anda sendiri: https://wa.me/<nomor-anda>?text=<token>. Pengguna tinggal tekan Kirim di WhatsApp — tidak perlu mengetik apa pun.
  3. Daftarkan webhook untuk event message.incoming. Saat pesan masuk cocok pola token Anda, itu bukti pengirimnya menguasai sesi WhatsApp aktif di baliknya.
  4. Tandai token itu terverifikasi, lalu balas pengguna (bukan mengirim duluan) untuk konfirmasi — lihat contoh kode di bawah.
Karena Anda hanya membalas percakapan yang pengguna mulai sendiri, nomor Anda tidak pernah "menyapa" alamat asing secara massal — jauh lebih aman untuk reputasinya, walau volume pendaftaran sedang tinggi sekalipun.
// POST /webhook-anda, setelah signature diverifikasi (lihat bagian Webhook di atas)
const POLA_TOKEN = /^PREFIKS-[a-f0-9]{10}$/i;

if (payload.event === 'message.incoming' && !payload.isFromMe && !payload.isGroup) {
  const teks = (payload.text || '').trim();
  if (POLA_TOKEN.test(teks)) {
    const baris = await cariToken(teks.toUpperCase());
    if (baris && baris.status === 'pending') {
      await tandaiTerverifikasi(baris.id, payload.from);
      // balas via POST /api/send/text ke pengguna, JANGAN ke identitas mentah kalau @lid (lihat di bawah)
    }
  }
}
// index.php / router Anda, setelah signature diverifikasi
$polaToken = '/^PREFIKS-[a-f0-9]{10}$/i';

if ($payload['event'] === 'message.incoming' && empty($payload['isFromMe']) && empty($payload['isGroup'])) {
    $teks = trim($payload['text'] ?? '');
    if (preg_match($polaToken, $teks)) {
        $baris = $model->cariToken(strtoupper($teks));
        if ($baris && $baris['status'] === 'pending') {
            $model->tandaiTerverifikasi($baris['id'], $payload['from']);
            // balas via POST /api/send/text — JANGAN ke identitas mentah kalau @lid, lihat di bawah
        }
    }
}

Nomor pengirim bisa berupa LID — pisahkan "sah" dari "tahu nomornya"

Konsep @lid sudah dijelaskan di bagian Webhook Events — untuk kasus verifikasi kepemilikan nomor ini, konsekuensinya perlu ditangani dengan hati-hati karena dua hal yang terasa mirip sebenarnya berbeda:

  • Apakah verifikasinya sah? Ya, selalu — token yang benar dari sesi WhatsApp aktif tetap bukti sah kepemilikan, LID atau bukan. Jangan tolak verifikasi hanya karena identitasnya LID.
  • Apakah Anda tahu nomor HP-nya? Belum tentu. Kalau identitasnya @lid, Anda tidak punya nomor HP asli saat itu juga — jangan coba menebaknya dari angka di depan @lid (itu ID internal acak, bukan nomor telepon, dan mengirim balasan ke situ tidak akan pernah sampai).
Jangan timpa data nomor yang sudah ada dengan tebakan dari LID. Kalau profil pengguna sudah punya nomor HP tersimpan (dari form pendaftaran, misalnya), biarkan itu apa adanya saat verifikasi lewat LID — cukup tandai terverifikasi, jangan sentuh kolom nomornya. Balas konfirmasi ke nomor yang sudah tersimpan itu, bukan ke identitas LID mentah.

WA Client 1 otomatis mencoba resolusi @lid → nomor asli di belakang layar, dan mengirim event identity.merged begitu berhasil — kadang seketika, kadang jam/hari kemudian, kadang tidak pernah (tergantung pengaturan privasi pengguna itu). Manfaatkan event ini: begitu diterima, cocokkan newChatId dengan nomor yang tersimpan di profil pengguna terkait. Kalau cocok, tidak ada yang perlu dilakukan. Kalau berbeda, jangan otomatis mengubah/mencabut status terverifikasi pengguna yang mungkin sudah lanjut memakai sistem Anda — cukup catat untuk ditinjau (log, tiket admin, dsb). Perubahan status secara diam-diam dan asinkron (bisa saja berhari-hari setelah verifikasi asli) lebih berisiko mengganggu pengguna yang sah dibanding manfaat mengejar akurasi sempurna.

Nomor pengirim beda dari yang diisi di form — jangan timpa diam-diam

Ini kasus yang gampang terlewat: token benar, pengirimnya jelas nomor HP asli (bukan LID) — tapi berbeda dari nomor yang pengguna ketik sendiri saat mendaftar. Godaannya adalah langsung menganggap "yang mengirim kode pasti nomor yang benar" dan menimpa data lama. Jangan lakukan itu secara otomatis — pengguna tidak pernah diberi tahu profilnya "berubah", dan kalau tokennya pernah bocor (screenshot yang tidak sengaja dibagikan, dsb), siapa pun bisa mengklaim nomor siapa pun.

Pola yang lebih aman:

  1. Normalisasi kedua nomor ke format yang sama (kode negara konsisten, tanpa spasi/tanda hubung) sebelum dibandingkan — jangan bandingkan string mentah.
  2. Kalau sama: lanjutkan seperti biasa, tandai terverifikasi.
  3. Kalau beda: jangan langsung menimpa atau langsung menolak. Simpan sebagai status antara ("menunggu konfirmasi"), balas ke nomor yang baru saja mengirim (nomor itu baru saja terbukti aktif) dengan penjelasan singkat + arahkan ke halaman web Anda untuk mengonfirmasi secara sadar — tombol "Ya, ini nomor saya" / "Bukan, saya kirim ulang dari nomor terdaftar".
  4. Hanya setelah pengguna sendiri yang menekan konfirmasi di web, baru data nomor benar-benar diperbarui dan status jadi terverifikasi penuh.
Intinya: sistem boleh mendeteksi ketidakcocokan dan menyarankan perbaikan, tapi keputusan mengubah data identitas pengguna sebaiknya selalu lewat konfirmasi sadar pengguna itu sendiri di kanal yang sudah terautentikasi (sesi login web Anda) — bukan diam-diam disimpulkan dari satu pesan WhatsApp.

Ringkasan praktik baik

JanganLakukan sebagai gantinya
Kirim OTP duluan ke setiap pendaftar baru secara massalPola terbalik: pengguna kirim token ke Anda lewat wa.me, Anda hanya membalas
Menebak nomor HP dari angka di depan @lidTerima sebagai verifikasi sah, tapi biarkan kolom nomor apa adanya sampai identitas asli diketahui (lewat identity.merged atau input pengguna sendiri)
Menimpa nomor tersimpan begitu saja saat pengirim ≠ nomor formTandai "menunggu konfirmasi", minta pengguna mengonfirmasi sadar lewat web sebelum data berubah
Mencabut status terverifikasi otomatis saat identity.merged datang belakangan dan ternyata bedaCatat untuk ditinjau manual — pengguna itu mungkin sudah lanjut memakai sistem Anda dengan status itu

Rate Limit

Endpoint /api/send/* dibatasi 20 pesan per menit per nomor (bisa berbeda per deployment) untuk mengurangi risiko deteksi otomatisasi oleh WhatsApp. Response melebihi batas mengembalikan status 429 dengan header retry-after.

Kode Error

StatusArti
400Parameter request tidak valid atau kurang.
401API key tidak ada / tidak valid, atau sesi login tidak valid.
404Resource tidak ditemukan (termasuk mengakses nomor yang bukan milik Anda).
409Konflik — mis. batas maksimum jumlah nomor per akun tercapai.
429Melebihi rate limit.
503Sesi WhatsApp nomor ini belum/tidak terhubung saat ini. Coba lagi setelah beberapa saat.
504Pengiriman belum selesai dalam batas waktu respons kami (lihat bagian Kirim Teks) — proses tetap berjalan di server, bukan berarti gagal.
500Kesalahan tak terduga di server.
{ "error": "pesan penjelasan singkat", "requestId": "3f2a1c9e-..." }
Setiap response (berhasil maupun error) membawa header x-request-id, dan setiap response error menyertakan requestId yang sama di body. Sertakan nilai ini saat melaporkan masalah ke kami — jauh lebih cepat kami telusuri di log dibanding hanya waktu & deskripsi masalah.
Response 429/504 bisa menyertakan header retry-after (dalam detik) yang menunjukkan kapan sebaiknya Anda mencoba lagi.