WhatsApp Unofficial Gateway API
REST API untuk mengirim & menerima pesan WhatsApp dari sistem Anda sendiri. Semua endpoint relatif terhadap base URL berikut.
https://wau.jasaonline.netButuh API key? Buat di portal setelah berlangganan.
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.
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.netPerangkat
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 JID 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 = HMAC-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). Lima kegagalan beruntun membuka circuit breaker ~60 detik.
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
{
"event": "message.received",
"data": {
"message_id": "…",
"wa_message_id": "…",
"device_id": "…",
"chat_jid": "6281…@s.whatsapp.net",
"sender_jid": "6281…@s.whatsapp.net",
"is_group": false,
"type": "text",
"body": { "text": "isi pesan" },
"media": { "media_id": "…", "url": "/v1/media/…" }
}
}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).
{
"event": "message.status",
"data": {
"message_id": "…",
"wa_message_id": "…",
"to": "6281…@s.whatsapp.net",
"status": "delivered", // sent | delivered | read | played | failed | expired
"status_detail": "…" // ada saat failed/expired
}
}