Skip to content

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 secretsimpan, 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

EventKapan dikirimStatus
messageAda pesan WhatsApp masuk ke device-mu (bukan pesan yang kamu kirim sendiri)✅ Aktif
message.sent / delivered / read / failedPerubahan status pesan keluarBelum di-relay sebagai webhook — status tetap ter-update dan bisa dibaca via GET /v1/messages/:id (polling)
device.disconnectedDevice 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 "", 200

Perilaku pengiriman

  • Timeout: 10 detik per request.
  • Retry otomatis: ⏳ belum ada — kalau endpoint-mu gagal merespons, event itu terlewat dan failure_count endpoint bertambah. Pastikan endpoint-mu selalu balas cepat; retry policy ada di roadmap.
  • Dedup di sisimu: gunakan payload.id sebagai idempotency key — pada kondisi tertentu event yang sama bisa terkirim lebih dari sekali.