API Reference

WhatsApp Unofficial Gateway API

REST API untuk mengirim & menerima pesan WhatsApp dari sistem Anda sendiri. Semua endpoint relatif terhadap base URL berikut.

base url
https://wau.jasaonline.net

Butuh API key? Buat di portal setelah berlangganan.

Tiga langkah untuk mulai
1Buat API key di portalX-API-Key: jo_live_…
2Tautkan nomor lewat QRPOST /v1/devices/{id}/pair
3Kirim pesan pertamaPOST /v1/messages
Key disimpan sebagai hash argon2id

Autentikasi

Setiap permintaan menyertakan header X-API-Key berisi API key penuh Anda (format jo_live_<…>). Bukan bearer token.

curl
curl https://wau.jasaonline.net/v1/ping \
  -H "X-API-Key: jo_live_xxxxxxxx_xxxxxxxx…"
401 unauthorizedKey tidak valid atau dicabut
403 tenant_suspendedTenant ditangguhkan

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

60 req/min

Batas default per key. Terlampaui → 429 rate_limit_exceeded disertai header Retry-After: 60.

Idempotency-Key

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.

Serverhttps://wau.jasaonline.net

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:

NEWPAIRINGACTIVEDISCONNECTED | LOGGED_OUT | BANNED

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 · text
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:

202
{
  "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:

device_id kosong

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.

device_id: "<uuid>"

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

textimagevideoaudiodocumentstickerlocationcontactreactioneditdelete

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()):

node · express
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.

0s30s2m10m1h6h24hdead

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

message.received
{
  "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).

message.status
{
  "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
  }
}

Kode error

401unauthorizedAPI key hilang / tidak valid / dicabut
403tenant_suspendedTenant ditangguhkan
429rate_limit_exceededBatas laju terlampaui (lihat Retry-After)
400bad_requestBody / parameter tidak valid
404device_not_foundDevice tidak ada / bukan milik Anda
409device_not_activeDevice belum ACTIVE
503no_eligible_deviceTidak ada device aktif di bawah batas harian

Siap mulai?

Buat API key di portal, lalu pindai QR untuk menautkan nomor.