Autentikasi
Setiap permintaan menyertakan header X-API-Key berisi API key penuh Anda (format jo_live_<…>). Bukan bearer token.
curl https://wau.jasaonline.net/v1/ping \-H "X-API-Key: jo_live_xxxxxxxx_xxxxxxxx…"
Key disimpan sebagai hash (argon2id) — simpan baik-baik saat dibuat, tidak dapat ditampilkan ulang oleh gateway (portal menyimpan salinan terenkripsi untuk Anda).
Rate limit & idempotensi
Batas default per key. Terlampaui → 429 rate_limit_exceeded disertai header Retry-After: 60.
Sertakan header unik per operasi untuk mencegah duplikasi saat retry. Respons yang diputar ulang membawa Idempotent-Replay: true.
Resep penggunaan
Empat alur umum yang bisa langsung ditiru. Setiap cuplikan bisa disalin dan dijalankan setelah Anda mengganti X-API-Key dan nilai contoh lainnya.
1. Kirim pesan pertama
Buat device, mulai pairing, pindai QR, lalu kirim pesan teks.
# 1. Buat deviceDEVICE=$(curl -s -X POST https://wau.jasaonline.net/v1/devices \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"label":"CS Utama"}' | jq -r '.id')# 2. Buka sesi pairingcurl -s -X POST https://wau.jasaonline.net/v1/devices/$DEVICE/pair -H "X-API-Key: jo_live_…"# 3. Tampilkan QR (buka URL ini di peramban, pindai dalam ~20 detik)open https://wau.jasaonline.net/v1/devices/$DEVICE/pair/qr.png# 4. Tunggu status ACTIVE, lalu kirim pesancurl -s -X POST https://wau.jasaonline.net/v1/messages \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"to":"6281234567890","type":"text","text":"Halo dari gateway"}'
2. Terima webhook
Daftarkan URL, lalu verifikasi tanda tangan di endpoint Anda sebelum membalas 200 OK.
# Daftarkan endpoint penerimacurl -s -X POST https://wau.jasaonline.net/v1/webhooks \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"url":"https://server-anda.com/webhook","events":["message.received","message.status"]}'
Di server Anda (contoh Express — lihat bagian Webhook untuk bahasa lain):
app.use("/webhook", express.json({verify: (req, _res, buf) => { req.rawBody = buf; }}));app.post("/webhook", (req, res) => {if (!verify(req, process.env.JO_WEBHOOK_SECRET)) return res.sendStatus(401);res.sendStatus(200); // balas duluenqueue(req.body); // kerjakan belakangan});
3. Retry idempoten
Kirim dengan Idempotency-Key unik. Kalau koneksi putus atau dapat 5xx, ulangi permintaan dengan key yang sama — gateway akan mengembalikan respons asli, bukan menjalankan ulang.
IDEMPOTENCY=$(uuidgen)for i in 1 2 3; doSTATUS=$(curl -s -o /tmp/wa_resp.json -w "%{http_code}" -X POST https://wau.jasaonline.net/v1/messages \-H "X-API-Key: jo_live_…" \-H "Idempotency-Key: $IDEMPOTENCY" \-H "Content-Type: application/json" \-d '{"to":"6281234567890","type":"text","text":"Pesan penting"}')[ "$STATUS" -lt 500 ] && breaksleep $((2 ** i))donecat /tmp/wa_resp.json
Key idempotensi diingat selama 10 menit (METHOD+PATH+body). Respons ulang membawa header Idempotent-Replay: true.
4. Kelola grup
Gabung ke grup lewat kode undangan, tambah anggota, dan ubah pengaturan dasar.
# Gabung lewat kode undangancurl -s -X POST https://wau.jasaonline.net/v1/groups/join \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"code":"AbCdEfGhIjK"}'# Ambil JID grup dari info grup (atau dari pesan masuk)JID="1203630…@g.us"# Tambah anggotacurl -s -X POST https://wau.jasaonline.net/v1/groups/$JID/participants \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"action":"add","jids":["6281234567890@s.whatsapp.net"]}'# Ubah nama & topikcurl -s -X PATCH https://wau.jasaonline.net/v1/groups/$JID \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"name":"Tim Support","topic":"Internal"}'
Referensi endpoint
Seluruh endpoint, lengkap dengan contoh payload dan contoh balasan — dan bisa dijalankan langsung dari halaman ini. Tekan Authorize, tempel API key Anda, lalu buka satu endpoint dan tekan Try it out.
Yang dijalankan di sini adalah permintaan SUNGGUHAN ke akun Anda: pesan benar-benar terkirim, webhook benar-benar didaftarkan, perangkat benar-benar terhapus. Operasi yang mengubah data ditandai peringatan di dalam panelnya.
https://wau.jasaonline.netMemeriksa…Perangkat
Satu device = satu nomor WhatsApp yang tertaut. NEW → PAIRING → ACTIVE.
Pesan
Kirim pesan. 202 berarti diterima antrean, BUKAN terkirim — hasilnya datang lewat webhook message.status.
Media
Unggah berkas — dan kalau `to` diisi, sekalian kirim. Tanpa `to`, endpoint ini hanya menyimpan dan mengembalikan media_id yang bisa dipakai berkali-kali lewat POST /v1/messages (satu unggah, banyak penerima).
Webhook
Endpoint penerima event, riwayat kiriman, dan kirim ulang.
Grup
Gabung, kelola anggota, dan ubah pengaturan grup.
Chat
Operasi pada satu percakapan.
Kontak
Cek nomor, blokir, dan profil kontak.
Audit & utilitas
Jejak audit tenant dan endpoint pemeriksaan.
Permintaan dilewatkan proxy milik situs ini, bukan dipanggil peramban langsung — CSP situs menutup koneksi ke host lain dan gateway tidak memasang header CORS. Proxy hanya meneruskan (method, path) yang ada di halaman ini, dan API key Anda tidak pernah dicatat.
Perangkat & pairing
Sebuah "device" = satu nomor WhatsApp yang tertaut. Siklus statusnya:
Buat device, mulai pairing, lalu pindai QR (PNG di /pair/qr.png, atau stream SSE di /pair/stream). QR berotasi ~20 detik.
Setelah QR dipindai, status tetap PAIRING sekitar 25 detik (proses upload prekey) sebelum berubah ke ACTIVE. Jangan timeout lebih cepat. Tidak ada endpoint status terpisah — baca status via GET /v1/devices/{id}.
Kirim pesan
curl -X POST https://wau.jasaonline.net/v1/messages \-H "X-API-Key: jo_live_…" \-H "Content-Type: application/json" \-d '{"to": "6281234567890","type": "text","text": "Halo dari gateway"}'
Nomor to boleh berupa digit polos (otomatis ditambah @s.whatsapp.net) atau JIDJabber ID — pengenal unik untuk kontak atau grup di WhatsApp, biasanya berbentuk 6281…@s.whatsapp.net atau 120363…@g.us. grup …@g.us. Sukses → 202 Accepted:
{"message_id": "…","wa_message_id": "…","status": "queued","device_id": "…"}
Nomor pengirim & round-robin
Anda tidak mengirim daftar nomor pengirim — tidak ada field from. Pemilihan device diatur oleh satu field opsional device_id:
Gateway memilih otomatis. Untuk chat pribadi, percakapan baru disebar (round-robin) ke device aktif yang paling lama tak dipakai, lalu percakapan itu menempel ke device tersebut agar penerima selalu melihat nomor yang sama. Untuk grup, dipilih device yang menjadi anggota grup itu.
Paksa satu device tertentu (harus berstatus ACTIVE). Ambil id dari GET /v1/devices. Respons & webhook selalu menyertakan device_id yang benar-benar dipakai.
202 bukan berarti terkirim. Gateway hanya memvalidasi bahwa to dan type terisi. Tipe tak dikenal, atau media tanpa media_id, tetap menerima 202 — kegagalannya muncul asinkron lewat webhook message.status (status failed + status_detail).
Tipe yang didukung
Media memakai media_id (unggah dulu, lihat bagian Media) + caption. Reaction & delete memakai target_id. Lintas-tipe: reply_to, mentions, ttl, dan callback_url per-pesan.
Tipe buttons saat ini di-render sebagai teks bernomor (WhatsApp membatasi tombol untuk akun tidak resmi), bukan tombol interaktif — jangan mengandalkannya sebagai tombol.
Media
Unggah via multipart/form-data (field file) atau JSON {"url": "…"}. Sertakan to (dan caption opsional) kalau berkasnya mau langsung dikirim — balasannya membawa media_id sekaligus message_id. Tanpa to, endpoint ini hanya menyimpan.
curl -X POST https://wau.jasaonline.net/v1/media -H "X-API-Key: jo_live_…" -F "file=@foto.jpg" -F "to=6281234567890" -F "caption=Struk pesanan #1023"# → 202 { "media_id": "…", "message_id": "…", "status": "queued", "device_id": "…" }
Webhook
Daftarkan URL untuk menerima event. secret hanya dikembalikan sekali (saat create / rotate-secret). Event yang tersedia: message.received & message.status.
Verifikasi tanda tanganSANGAT DISARANKAN
Setiap kiriman selalu membawa header X-JO-Signature, X-JO-Timestamp, dan X-JO-Event-Id— Anda tidak perlu mengaktifkan apa pun. Memverifikasinya adalah pilihan Anda: endpoint yang melewatinya tetap menerima event seperti biasa. Yang ditanggung kalau melewatinya juga jelas: siapa pun yang mengetahui URL endpoint Anda bisa mengarang "pesan masuk" yang terlihat asli, karena tanda tangan inilah satu-satunya yang membedakan kiriman kami dari kiriman orang lain.
Tanda tangan = HMACHash-based Message Authentication Code — tanda tangan kriptografi untuk memastikan payload berasal dari gateway.-SHA256 atas timestamp + "." + rawBody, memakai secret endpoint:
import crypto from "node:crypto";// Umur kiriman yang masih diterima. Tanpa batas ini, satu kiriman lama yang// terekam bisa diputar ulang kapan saja — tanda tangannya tetap sah selamanya.const MAX_SKEW_SECONDS = 300;function verify(req, secret) {const ts = req.headers["x-jo-timestamp"];const sig = req.headers["x-jo-signature"]; // "sha256=<hex>"if (!ts || !sig) return false;if (Math.abs(Date.now() / 1000 - Number(ts)) > MAX_SKEW_SECONDS) return false;const expected ="sha256=" +crypto.createHmac("sha256", secret).update(ts + "." + req.rawBody) // rawBody = bytes mentah, bukan JSON re-serialize.digest("hex");const a = Buffer.from(sig), b = Buffer.from(expected);// timingSafeEqual MELEMPAR kalau panjang keduanya beda. Header ngawur harus// berakhir sebagai "tidak valid", bukan sebagai 500 di server Anda.return a.length === b.length && crypto.timingSafeEqual(a, b);}
Bagian yang paling sering salah bukan HMAC-nya, melainkan rawBody: tanda tangan dihitung atas byte yang benar-benar dikirim, sedangkan JSON.stringify(req.body) menghasilkan susunan spasi dan urutan kunci yang bisa berbeda. Simpan byte mentahnya sebelum parser menyentuhnya — contohnya Express, tapi urusannya sama di kerangka mana pun (Spring: byte[] di @RequestBody; Laravel: $request->getContent()):
app.use("/webhook", express.json({verify: (req, _res, buf) => { req.rawBody = buf; }, // byte asli, sebelum di-parse}));app.post("/webhook", (req, res) => {if (!verify(req, process.env.JO_WEBHOOK_SECRET)) return res.sendStatus(401);// Balas dulu, kerjakan belakangan: gateway menunggu balasan Anda, dan// pekerjaan berat di sini berubah jadi timeout lalu retry.res.sendStatus(200);enqueue(req.body);});
X-JO-Event-Id adalah kunci idempotensi milik peristiwanya, bukan milik percobaan pengiriman. Ia tetap sama pada tiap retry maupun saat kiriman dikirim ulang dari portal — simpan id yang sudah pernah diproses dan abaikan duplikatnya, jangan bertumpu pada "kami hanya mengirim sekali".
Balas 2xx untuk menandai sukses. Jika gagal, gateway mengulang dengan jadwal berikut, lalu berhenti (dead).
Circuit breaker melindungi endpoint Anda dari banjir retry. Setiap kali gateway menerima respons bukan 2xx, penghitung kegagalan naik. Setelah 5 kegagalan beruntun, breaker membuka dan semua pengiriman ke endpoint itu dijeda selama ~60 detik — event tetap masuk antrean internal, tapi tidak dikirim. Begitu jeda selesai, breaker masuk ke mode half-open: satu kiriman percobaan dilepas; kalau sukses, breaker closed kembali dan aliran normal pulih. Kalau gagal, breaker membuka lagi selama 60 detik berikutnya.
Dari sisi Anda, endpoint sebaiknya selalu membalas 2xxsecepat mungkin (idealnya < 5 detik) dan mengerjakan payload di background. Gateway membaca status dead dari riwayat kiriman di GET /v1/webhooks/{id}/deliveries.
Callback per-pesan (callback_url pada POST /v1/messages) ditandatangani dengan secret callback tenant, berbeda dari secret endpoint di atas. Secret itu hanya bisa dilihat & dirotasi dari portal.
Payload webhook
media.url adalah path relatif — gabungkan dengan base URL untuk mengunduhnya (https://wau.jasaonline.net/v1/media/…). Bentuk body berbeda per tipe (mis. image → caption/mime; location → latitude/longitude).
Kode error
FAQ & troubleshooting
Pesan status 'queued' lama sekali, kenapa?▼
202 Accepted hanya berarti gateway menerima permintaan. Status akhir (sent/delivered/read/failed) datang lewat webhook message.status. Kalau tidak ada webhook, cek device masih ACTIVE dan webhook endpoint membalas 2xx.
Device sudah ACTIVE tapi tidak bisa kirim▼
Kemungkinan device mencapai daily_cap, atau grup tidak punya device yang bisa mengelolanya (no_device_in_group). Cek response 503 no_eligible_device / no_active_device, atau cek batas harian di portal.
Webhook tidak dipanggil sama sekali▼
Pastikan: (1) URL bisa diakses publik, (2) endpoint membalas HTTP 2xx, (3) webhook is_active true, (4) circuit breaker tidak sedang open. Riwayat kiriman ada di GET /v1/webhooks/{id}/deliveries.
Tanda tangan webhook tidak cocok▼
Hampir selalu karena rawBody berbeda dari byte yang dikirim gateway. Jangan pakai JSON.stringify(req.body). Simpan byte mentah sebelum parser JSON. Lihat contoh verifikasi di bagian Webhook.
Bisa membatasi API key per IP?▼
Saat ini tidak. API key berlaku global untuk tenant. Anda bisa membuat beberapa key dan mencabutnya per key dari portal jika diperlukan rotasi.
Bagaimana migrasi dari sandbox ke production?▼
Sandbox dan production adalah tenant terpisah. Device dan API key sandbox tidak otomatis pindah. Setelah testing, buat device & API key baru di production, lalu ubah base URL client Anda.
Changelog
- Rilis stabil REST API WhatsApp Unofficial Gateway.
- Endpoint perangkat, pesan, media, webhook, grup, chat, kontak, audit, dan utilitas.
- Autentikasi X-API-Key dengan hash argon2id.
- Webhook dengan tanda tangan HMAC-SHA256, retry schedule, dan circuit breaker.
- Idempotensi via Idempotency-Key header.
Ada saran atau menemukan kesalahan docs? Hubungi support.
