NgirimWA Docs
Webhook

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) dan X-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)
HeaderArti
X-NgirimWA-EventNama event, sama dengan payload.event — praktis untuk routing tanpa parse body
X-NgirimWA-DeliveryUUID unik per kiriman (percobaan ulang memakai UUID yang sama) — simpan untuk dedup
X-NgirimWA-Signaturet=<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-Delivery dan 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

EventTrigger
message.receivedPesan masuk dari kontak lain
message.sentPesan keluar berhasil terkirim ke server WhatsApp (centang 1)
message.deliveredPesan keluar diterima device penerima (centang 2)
message.readPesan keluar dibaca penerima (centang 2 biru)
message.failedPesan keluar gagal dikirim
device.connectedDevice WhatsApp Anda baru saja online
device.disconnectedDevice 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

TipeKeterangan
textPesan teks biasa
image / video / audio / document / stickerPesan media
button_replyUser menekan tombol format klasik — info.button_id, info.button_text
interactive_replyUser menekan tombol interaktif/nativeFlow (kiriman /messages/buttons dan /messages/quick-reply) — info.button_id, info.button_text, info.interactive_type (mis. quick_reply)
list_replyUser memilih item list klasik (kiriman /messages/list dan template list) — info.row_id, info.row_title, info.row_description
interactive_list_replyUser memilih item list interaktif — info.row_id, info.row_title
template_button_replyUser menekan tombol template — info.button_id, info.button_text
location / contact / pollPesan khusus

Pembaruan 27 Agu 2026: balasan tombol dari POST /messages/buttons kini masuk sebagai interactive_reply (sebelumnya button_reply) karena pesan tombol dikirim dalam format interactive nativeFlow. info.button_id tetap berisi buttonId yang 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:

TipecontentID di info
button_reply / interactive_reply / template_button_replyLabel tombolinfo.button_id
list_reply / interactive_list_replyJudul 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, content kini berisi judul baris (mis. Cek Harga), sebelumnya berisi rowId (mis. cek_harga). info.row_title dan info.row_description kini terisi (sebelumnya selalu null). info.row_id tidak berubah. Jika integrasi Anda membaca content untuk mengenali pilihan list, pindah ke info.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 ponsel
  • connectionClosed — koneksi putus mendadak
  • restartRequired — perlu reconnect
  • timedOut — timeout
  • badSession — 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:

  1. Ambil header X-NgirimWA-Signature, pisahkan t (unix detik) dan v1 (hex).
  2. Hitung HMAC-SHA256(secret, t + "." + rawBody) — rawBody adalah body persis seperti diterima (jangan di-JSON.parse lalu di-stringify ulang).
  3. Bandingkan dengan v1 memakai perbandingan timing-safe.
  4. Tolak bila |now − t| > 300 detik (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 $payload

Setelah 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.

On this page