Jenis Event Webhook
Daftar semua event yang dikirim NgirimWA ke webhook URL Anda.
NgirimWA mengirim payload JSON ke webhook_url device Anda saat event-event berikut terjadi. Konfigurasi webhook URL ada di Dashboard → Perangkat → Edit.
Pembaruan 18 September 2026: (1) pengiriman webhook kini diulang 1× setelah 2 detik bila server Anda tidak merespons sama sekali (timeout 5 detik / koneksi putus) — respons HTTP 4xx/5xx tidak diulang; (2) setiap kiriman membawa header
X-NgirimWA-Delivery(UUID unik per percobaan pengiriman, untuk dedup) danX-NgirimWA-Event; (3) Anda bisa mengaktifkan tanda tangan HMAC (X-NgirimWA-Signature) dari Dashboard → Perangkat → Webhook agar server Anda bisa memverifikasi bahwa pengirimnya benar NgirimWA. Detail di bagian Keamanan: Verifikasi Tanda Tangan.
Semua event memakai HTTP POST dengan header:
Content-Type: application/json
User-Agent: NgirimWA-Webhook/1.0
X-NgirimWA-Event: message.received
X-NgirimWA-Delivery: 7f2c1d0e-3b4a-4c5d-9e8f-0a1b2c3d4e5f
X-NgirimWA-Signature: t=1758182400,v1=5f1c... (hanya bila secret diatur)| Header | Arti |
|---|---|
X-NgirimWA-Event | Nama event, sama dengan payload.event — praktis untuk routing tanpa parse body |
X-NgirimWA-Delivery | UUID unik per kiriman (percobaan ulang memakai UUID yang sama) — simpan untuk dedup |
X-NgirimWA-Signature | t=<unix detik>,v1=<hex HMAC-SHA256> — lihat verifikasi di bawah. Tidak ada bila secret belum diatur |
Kebijakan pengiriman ulang
- Timeout menunggu respons Anda: 5 detik.
- Bila tidak ada respons (timeout, koneksi ditolak, DNS gagal): 1× percobaan ulang setelah 2 detik, dengan body dan header yang identik (termasuk
X-NgirimWA-Deliverydan tanda tangan). - Bila server Anda menjawab HTTP apa pun (termasuk 4xx/5xx): tidak diulang — cek Riwayat Webhook di dashboard.
- Setelah dua percobaan gagal, event dicatat sebagai gagal di Riwayat Webhook dan tidak dikirim lagi.
Daftar Event
| Event | Trigger |
|---|---|
message.received | Pesan masuk dari kontak lain |
message.sent | Pesan keluar berhasil terkirim ke server WhatsApp (centang 1) |
message.delivered | Pesan keluar diterima device penerima (centang 2) |
message.read | Pesan keluar dibaca penerima (centang 2 biru) |
message.failed | Pesan keluar gagal dikirim |
device.connected | Device WhatsApp Anda baru saja online |
device.disconnected | Device WhatsApp Anda baru saja offline |
Read receipt hanya muncul bila penerima mengaktifkan "Read Receipts" di pengaturan privasi WhatsApp mereka.
message.received
Pesan masuk — text, media, button reply, list reply, dll.
{
"event": "message.received",
"timestamp": "2026-05-15T10:00:00.000Z",
"device": {
"id": "uuid-device",
"name": "Toko Saya",
"phone": "628111111111"
},
"message": {
"id": "3EB0ABC123",
"from": "628222222222",
"sender_name": "Budi",
"type": "text",
"content": "Halo, saya mau tanya",
"info": {},
"timestamp": 1747300800,
"raw": { /* objek message Baileys mentah */ }
},
"metadata": {
"is_group": false,
"group_id": null,
"is_lid_only": false
}
}Tipe message.type
| Tipe | Keterangan |
|---|---|
text | Pesan teks biasa |
image / video / audio / document / sticker | Pesan media |
button_reply | User menekan tombol format klasik — info.button_id, info.button_text |
interactive_reply | User menekan tombol interaktif/nativeFlow (kiriman /messages/buttons dan /messages/quick-reply) — info.button_id, info.button_text, info.interactive_type (mis. quick_reply) |
list_reply | User memilih item list klasik (kiriman /messages/list dan template list) — info.row_id, info.row_title, info.row_description |
interactive_list_reply | User memilih item list interaktif — info.row_id, info.row_title |
template_button_reply | User menekan tombol template — info.button_id, info.button_text |
location / contact / poll | Pesan khusus |
Pembaruan 27 Agu 2026: balasan tombol dari
POST /messages/buttonskini masuk sebagaiinteractive_reply(sebelumnyabutton_reply) karena pesan tombol dikirim dalam format interactive nativeFlow.info.button_idtetap berisibuttonIdyang Anda kirim, jadi cukup terima kedua tipe tersebut.
Isi content untuk balasan tombol & list
Untuk semua tipe balasan, message.content berisi teks yang dilihat dan diketuk user — label tombol atau judul baris list — persis seperti aslinya (spasi, huruf besar, dan tanda baca dipertahankan). ID yang Anda kirim selalu tersedia terpisah di info:
| Tipe | content | ID di info |
|---|---|---|
button_reply / interactive_reply / template_button_reply | Label tombol | info.button_id |
list_reply / interactive_list_reply | Judul baris (title) | info.row_id |
Bila teksnya tidak tersedia, content jatuh ke ID sebagai cadangan. Untuk logika integrasi, pakai info.row_id / info.button_id — bukan content — karena ID tidak berubah saat Anda mengganti teks tampilan.
Pembaruan 17 Sep 2026: pada
list_reply,contentkini berisi judul baris (mis.Cek Harga), sebelumnya berisirowId(mis.cek_harga).info.row_titledaninfo.row_descriptionkini terisi (sebelumnya selalunull).info.row_idtidak berubah. Jika integrasi Anda membacacontentuntuk mengenali pilihan list, pindah keinfo.row_id.
message.sent
Pesan keluar diterima server WhatsApp.
{
"event": "message.sent",
"timestamp": "2026-05-15T10:00:01.123Z",
"device": { "id": "...", "name": "...", "phone": "628111111111" },
"message": {
"id": "3EB0XYZ789",
"to": "628222222222",
"ack": 2
}
}message.delivered
{
"event": "message.delivered",
"timestamp": "...",
"device": { "id": "...", "name": "...", "phone": "..." },
"message": { "id": "3EB0XYZ789", "to": "628222222222", "ack": 3 }
}message.read
{
"event": "message.read",
"timestamp": "...",
"device": { "id": "...", "name": "...", "phone": "..." },
"message": { "id": "3EB0XYZ789", "to": "628222222222", "ack": 4 }
}message.failed
Pesan gagal terkirim. Field error.code mengikuti kode error Baileys/WhatsApp.
{
"event": "message.failed",
"timestamp": "...",
"device": { "id": "...", "name": "...", "phone": "..." },
"message": {
"id": "3EB0XYZ789",
"to": "628222222222",
"ack": "failed",
"error": {
"code": 408,
"message": "Recipient not reachable"
}
}
}device.connected
Device baru saja terhubung ke WhatsApp.
{
"event": "device.connected",
"timestamp": "2026-05-15T10:00:00.000Z",
"device": {
"id": "uuid-device",
"name": "Toko Saya",
"phone": "628111111111"
}
}device.disconnected
Device terputus. reason mengikuti kode disconnect Baileys:
loggedOut— di-logout dari ponselconnectionClosed— koneksi putus mendadakrestartRequired— perlu reconnecttimedOut— timeoutbadSession— sesi rusak, perlu scan ulang
{
"event": "device.disconnected",
"timestamp": "...",
"device": {
"id": "uuid-device",
"name": "Toko Saya",
"phone": "628111111111"
},
"reason": "loggedOut",
"code": 401
}Keamanan: Verifikasi Tanda Tangan
Tanpa tanda tangan, siapa pun yang tahu URL webhook Anda bisa mengirim payload palsu. Aktifkan secret di Dashboard → Perangkat → Webhook → Buat secret, simpan nilainya di server Anda, lalu verifikasi setiap kiriman:
- Ambil header
X-NgirimWA-Signature, pisahkant(unix detik) danv1(hex). - Hitung
HMAC-SHA256(secret, t + "." + rawBody)— rawBody adalah body persis seperti diterima (jangan di-JSON.parselalu di-stringify ulang). - Bandingkan dengan
v1memakai perbandingan timing-safe. - Tolak bila
|now − t| > 300detik (cegah replay).
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.NGIRIMWA_WEBHOOK_SECRET;
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-NgirimWA-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(401).end();
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex');
const given = Buffer.from(parts.v1 || '', 'hex');
if (given.length !== 32 || !crypto.timingSafeEqual(given, Buffer.from(expected, 'hex'))) {
return res.status(401).end();
}
const payload = JSON.parse(req.body);
// dedup: simpan req.get('X-NgirimWA-Delivery'), abaikan bila sudah pernah diproses
res.sendStatus(200);
// ... proses payload setelah membalas
});PHP
<?php
$secret = getenv('NGIRIMWA_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_NGIRIMWA_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts); // ['t' => '1758182400', 'v1' => '...']
$t = (int) ($parts['t'] ?? 0);
if ($t === 0 || abs(time() - $t) > 300) { http_response_code(401); exit; }
$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '')) { http_response_code(401); exit; }
http_response_code(200);
$payload = json_decode($raw, true);
// dedup dengan $_SERVER['HTTP_X_NGIRIMWA_DELIVERY'], lalu proses $payloadSetelah menekan Putar ulang di dashboard, secret lama langsung tidak berlaku — perbarui di server Anda sebelum menyimpan. Menghapus secret mengembalikan pengiriman tanpa tanda tangan.
Praktik Terbaik Konsumsi Webhook
Balas dengan HTTP 2xx secepatnya — webhook timeout di NgirimWA adalah 5 detik. Bila proses panjang, antrian-kan dulu lalu return 200.
Idempoten berdasarkan X-NgirimWA-Delivery (dan message.id) — percobaan ulang memakai UUID pengiriman yang sama, jadi simpan UUID yang sudah diproses dan abaikan duplikatnya.
Verifikasi X-NgirimWA-Signature bila Anda mengaktifkan secret — tolak kiriman yang tidak lolos verifikasi atau t-nya lebih dari 5 menit.
Filter berdasarkan event — handler Anda sebaiknya switch (payload.event) { ... } agar mudah dikembangkan saat event baru ditambahkan.
Pantau di Dashboard → Riwayat Webhook — lihat success rate dan response time per event. Failure rate tinggi biasanya berarti endpoint Anda timeout atau return 5xx.