Appearance
Webhooks
Terima notifikasi real-time di server-mu saat ada pesan WhatsApp masuk ke device. Setiap request dari WAQ ditandatangani HMAC supaya kamu bisa pastikan asalnya asli.
Daftar & kelola endpoint
bash
# Daftarkan endpoint (JWT dashboard)
curl -X POST https://waq.karyawah.id/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "url": "https://server-kamu.com/wa-webhook", "events": ["message"] }'Respons berisi secret — simpan, dipakai untuk verifikasi signature. events kosong ([]) = terima semua event.
URL harus publik
URL internal/privat (localhost, IP LAN, dst) ditolak 400 — WAQ memvalidasi URL saat didaftarkan DAN saat kirim (anti-SSRF).
bash
# List endpoint terdaftar
curl https://waq.karyawah.id/v1/webhooks -H "Authorization: Bearer $TOKEN"
# Hapus endpoint
curl -X DELETE https://waq.karyawah.id/v1/webhooks/<id> -H "Authorization: Bearer $TOKEN"Event yang tersedia
| Event | Kapan dikirim | Status |
|---|---|---|
message | Ada pesan WhatsApp masuk ke device-mu (bukan pesan yang kamu kirim sendiri) | ✅ Aktif |
message.sent / delivered / read / failed | Perubahan status pesan keluar | ⏳ Belum di-relay sebagai webhook — status tetap ter-update dan bisa dibaca via GET /v1/messages/:id (polling) |
device.disconnected | Device putus dari WhatsApp | ⏳ Belum tersedia — cek via POST /v1/devices/:id/sync |
Kenapa ditandai jujur?
Kami hanya mendokumentasikan yang benar-benar jalan. Event yang masih ⏳ ada di roadmap — untuk sekarang gunakan polling endpoint terkait.
Payload event message
json
{
"event": "message",
"session_id": "…",
"device_id": "628xxx@s.whatsapp.net",
"timestamp": 1789300000,
"payload": {
"id": "…",
"chat_id": "628987654321@s.whatsapp.net",
"from": "628987654321@s.whatsapp.net",
"type": "text",
"body": "Halo, saya mau tanya…"
}
}Balas 200 secepat mungkin (timeout WAQ: 10 detik) — proses berat lakukan async setelah merespons.
Verifikasi signature (WAJIB di server-mu)
Setiap request membawa header:
X-Signature: sha256=<hex HMAC-SHA256 dari raw body, key = secret endpoint-mu>Verifikasi dengan constant-time compare atas raw body mentah (jangan re-serialize JSON — byte bisa beda):
js
const crypto = require('crypto');
// PENTING: pakai raw body, bukan hasil JSON.parse
app.post('/wa-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const expected =
'sha256=' +
crypto.createHmac('sha256', process.env.WAQ_WEBHOOK_SECRET)
.update(req.body) // Buffer mentah
.digest('hex');
const given = req.get('X-Signature') || '';
const a = Buffer.from(expected);
const b = Buffer.from(given);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body);
// …proses event…
res.sendStatus(200);
});php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('WAQ_WEBHOOK_SECRET'));
$given = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($raw, true);
// …proses event…
http_response_code(200);python
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/wa-webhook")
def wa_webhook():
expected = "sha256=" + hmac.new(
os.environ["WAQ_WEBHOOK_SECRET"].encode(),
request.get_data(), # raw body
hashlib.sha256,
).hexdigest()
given = request.headers.get("X-Signature", "")
if not hmac.compare_digest(expected, given):
abort(401)
event = request.get_json()
# …proses event…
return "", 200Perilaku pengiriman
- Timeout: 10 detik per request.
- Retry otomatis: ⏳ belum ada — kalau endpoint-mu gagal merespons, event itu terlewat dan
failure_countendpoint bertambah. Pastikan endpoint-mu selalu balas cepat; retry policy ada di roadmap. - Dedup di sisimu: gunakan
payload.idsebagai idempotency key — pada kondisi tertentu event yang sama bisa terkirim lebih dari sekali.