Notifikasi Transaksional
Kirim pengingat tagihan, jatuh tempo, dan konfirmasi pembayaran lewat API, lalu pastikan pesannya sampai — tanpa webhook.
Kirim pesan di WhatsApp saat sesuatu terjadi seperti tagihan terbit, jatuh tempo, pembayaran berhasil. Anda ingin tahu pesan mana yang sampai dan mana yang gagal? Cukup API saja. Webhook opsional.
Alurnya
Kirim dengan ref_id = ID transaksi Anda dan expires_in sesuai jenis pesan.
Simpan tracking_id dari respons bersama data transaksi.
Cek status secara berkala (cron) untuk pesan yang belum final — banyak sekaligus.
Tangani yang gagal sesuai error.code: kirim ulang, beralih ke email/SMS, atau tandai kontak.
1. Pilih expires_in yang masuk akal
expires_in = berapa lama NgirimWA boleh menahan pesan bila perangkat sedang offline. Lewat dari itu pesan tidak dikirim dan statusnya failed / expired — lebih baik daripada pengingat yang datang terlambat. Bila perangkat tersambung lagi sebelum batas itu, pesan yang tertahan dikirim bertahap (±2,5 detik antar pesan).
Apa artinya "perangkat offline"? Nomor WhatsApp Anda terhubung ke NgirimWA sebagai perangkat tertaut (seperti WhatsApp Web), jadi pesan tetap terkirim walau HP Anda mati atau tanpa internet (selama HP tidak offline lebih dari ±14 hari). Pesan baru tertahan bila sesi perangkat di NgirimWA terputus — mis. pemeliharaan/restart server, gangguan jaringan, atau perangkat di-logout dari HP.
| Jenis pesan | Saran expires_in |
|---|---|
| Kode OTP / verifikasi | 300 (5 menit) |
| Pembayaran berhasil / pesanan dikirim | 3600 (1 jam, default) |
| Pengingat jatuh tempo hari ini | 14400 (4 jam) |
| Pengingat H-3 / H-1, info umum | 86400 (24 jam, maksimum) |
2. Kirim dan simpan tracking_id
Siapkan tabel (atau kolom) di database Anda:
CREATE TABLE wa_notifications (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
invoice_id VARCHAR(64) NOT NULL, -- dipakai sebagai ref_id
phone VARCHAR(20) NOT NULL,
tracking_id CHAR(36) NULL,
status VARCHAR(16) NOT NULL DEFAULT 'queued',
error_code VARCHAR(40) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
checked_at DATETIME NULL
);PHP
<?php
/** $percobaan: naikkan saat SENGAJA mengirim ulang, agar Idempotency-Key berbeda. */
function kirimWa(string $phone, string $text, string $refId, int $expiresIn = 3600, int $percobaan = 1): ?array
{
$ch = curl_init('https://dash.ngirimwa.com/api/v1/messages/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('NGIRIMWA_API_KEY'),
'Content-Type: application/json',
// Satu key per pesan: bila request ini diulang (timeout jaringan), tidak terkirim dobel.
'Idempotency-Key: ' . $refId . '-' . $percobaan . '-' . md5($phone . $text),
],
CURLOPT_POSTFIELDS => json_encode([
'to' => $phone,
'message' => $text,
'ref_id' => $refId,
'expires_in' => $expiresIn,
]),
]);
$body = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http === 200) {
return $body['data']; // tracking_id, ref_id, status, expires_at
}
// Ditolak langsung — pesan tidak masuk antrean. Lihat details.code.
error_log('NgirimWA ' . $http . ' ' . ($body['details']['code'] ?? '') . ': ' . ($body['message'] ?? ''));
return null;
}
$data = kirimWa('081234567890', "Pembayaran INV-2026-0042 diterima. Terima kasih!", 'INV-2026-0042');
if ($data) {
$db->prepare('INSERT INTO wa_notifications (invoice_id, phone, tracking_id) VALUES (?, ?, ?)')
->execute(['INV-2026-0042', '081234567890', $data['tracking_id']]);
}Node.js
import crypto from 'node:crypto';
async function kirimWa(phone, text, refId, expiresIn = 3600, percobaan = 1) {
const res = await fetch('https://dash.ngirimwa.com/api/v1/messages/send', {
method: 'POST',
headers: {
'x-api-key': process.env.NGIRIMWA_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': `${refId}-${percobaan}-${crypto.createHash('md5').update(phone + text).digest('hex')}`,
},
body: JSON.stringify({ to: phone, message: text, ref_id: refId, expires_in: expiresIn }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.details?.code}: ${body.message}`);
return body.data; // { tracking_id, ref_id, status, expires_at, job_id }
}Python
import hashlib, os, requests
def kirim_wa(phone: str, text: str, ref_id: str, expires_in: int = 3600, percobaan: int = 1) -> dict:
res = requests.post(
"https://dash.ngirimwa.com/api/v1/messages/send",
headers={
"x-api-key": os.environ["NGIRIMWA_API_KEY"],
"Idempotency-Key": f"{ref_id}-{percobaan}-{hashlib.md5((phone + text).encode()).hexdigest()}",
},
json={"to": phone, "message": text, "ref_id": ref_id, "expires_in": expires_in},
timeout=30,
)
body = res.json()
if res.status_code != 200:
raise RuntimeError(f"{res.status_code} {body.get('details', {}).get('code')}: {body.get('message')}")
return body["data"]3. Cek status lewat cron
Jalankan tiap 5–15 menit. Ambil notifikasi yang belum final, cek 100 sekaligus, simpan hasilnya.
<?php
// cron: */10 * * * * php cek-status-wa.php
$rows = $db->query(
"SELECT id, tracking_id FROM wa_notifications
WHERE status IN ('queued', 'sent') AND tracking_id IS NOT NULL
AND created_at > NOW() - INTERVAL 3 DAY
ORDER BY id LIMIT 500"
)->fetchAll(PDO::FETCH_ASSOC);
foreach (array_chunk($rows, 100) as $chunk) {
$ids = implode(',', array_column($chunk, 'tracking_id'));
$ch = curl_init('https://dash.ngirimwa.com/api/v1/messages?tracking_ids=' . $ids);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('NGIRIMWA_API_KEY')],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($body['data'] ?? [] as $msg) {
$db->prepare('UPDATE wa_notifications SET status = ?, error_code = ?, checked_at = NOW() WHERE tracking_id = ?')
->execute([$msg['status'], $msg['error']['code'] ?? null, $msg['tracking_id']]);
if ($msg['status'] === 'failed') {
tanganiGagal($msg); // langkah 4
}
}
}Pesan
sentbisa berubah menjadideliveredberjam-jam kemudian (HP penerima mati). Batasi pengecekan, mis. hanya pesan 3 hari terakhir. Data status tersimpan 30 hari.
4. Tangani yang gagal
Tentukan tindakan berdasarkan error.code (bukan teks message):
<?php
function tanganiGagal(array $msg): void
{
switch ($msg['error']['code']) {
case 'expired':
case 'device_logged_out':
case 'device_not_connected':
// Masalah di perangkat WhatsApp Anda — pesan belum pernah sampai.
// Beri tahu admin untuk menyambungkan perangkat; kirim lewat email bila mendesak.
kirimEmailCadangan($msg['ref_id']);
break;
case 'not_on_whatsapp':
// Nomor tidak punya WhatsApp — jangan kirim ulang; tandai kontak.
tandaiNomorTanpaWa($msg['to']);
break;
case 'recipient_restricted':
case 'device_restricted':
// WhatsApp sedang membatasi — JANGAN kirim ulang beruntun (memperburuk pembatasan).
break;
default:
// send_timeout, recipient_session_error, wa_rejected, internal_error, kode baru:
// aman dikirim ulang SEKALI beberapa menit kemudian: ref_id sama, $percobaan = 2
// (Idempotency-Key baru — key lama akan memutar ulang respons pertama).
jadwalkanKirimUlang($msg['ref_id']);
}
}Daftar lengkap kode dan artinya: Kode gagal kirim.
Alternatif: webhook (status seketika)
Bila server Anda bisa menerima webhook, Anda tidak perlu cron. Event message.sent / message.delivered / message.read / message.failed membawa tracking_id dan ref_id yang sama, jadi bisa langsung meng-update baris yang sama:
<?php
$payload = json_decode(file_get_contents('php://input'), true);
http_response_code(200); // balas cepat
if (str_starts_with($payload['event'] ?? '', 'message.') && !empty($payload['message']['tracking_id'])) {
$m = $payload['message'];
// Hanya maju: queued < sent < delivered < read; failed hanya menimpa queued/sent.
$db->prepare(
"UPDATE wa_notifications SET status = ?, error_code = ?
WHERE tracking_id = ?
AND FIELD(status, 'queued', 'sent', 'delivered', 'read') < FIELD(?, 'queued', 'sent', 'delivered', 'read')
OR (tracking_id = ? AND ? = 'failed' AND status IN ('queued', 'sent'))"
)->execute([$m['status'], $m['error']['code'] ?? null, $m['tracking_id'], $m['status'], $m['tracking_id'], $m['status']]);
}Verifikasi tanda tangan dan buang kiriman ganda tetap disarankan — lihat Webhook. Webhook dan cron juga bisa dipakai bersamaan: webhook untuk kecepatan, cron harian ?status=failed&since=… sebagai jaring pengaman.
Untuk tim admin / CS
Tidak semua orang di tim Anda perlu membuka database. Di Dashboard → Perangkat → Riwayat Pesan, cari nomor tagihan (ref_id) untuk melihat apakah pengingatnya terkirim, diterima, dibaca, atau gagal beserta alasannya — data pesan API tersimpan 30 hari.
Checklist
- Setiap kirim membawa
ref_id(ID transaksi Anda) danexpires_inyang sesuai. -
tracking_iddisimpan di database Anda. - Ada cron (atau webhook) yang memperbarui status.
- Pesan
failedditangani pererror.code, termasuk kode yang belum dikenal. -
Idempotency-Keydipakai di setiap request kirim; key baru saat sengaja mengirim ulang.