Untuk aplikasi bisnis di Indonesia, notifikasi email punya masalah mendasar: banyak yang tidak pernah dibuka. Pelanggan toko, staf gudang, dan pengelola koperasi umumnya tidak memantau inbox. WhatsApp beda cerita, pesannya hampir pasti terbaca dalam hitungan menit.
Konsekuensinya, kebutuhan โkirim notifikasi ke pelangganโ di sini hampir selalu berarti WhatsApp, bukan email. Tulisan ini soal membangunnya supaya tidak rapuh.
Dua Jalur, Dua Konsekuensi
WhatsApp Cloud API resmi dari Meta. Legal dan stabil, tapi butuh verifikasi bisnis, nomor khusus yang tidak dipakai di aplikasi WhatsApp biasa, dan template pesan yang harus disetujui lebih dulu. Untuk pesan yang dimulai oleh bisnis di luar jendela 24 jam, hanya template yang boleh dikirim.
Gateway pihak ketiga tidak resmi seperti Fonnte dan sejenisnya. Jauh lebih cepat dipasang, tidak perlu persetujuan template, dan bisa memakai nomor biasa. Konsekuensinya, nomor berisiko diblokir Meta kalau pola pengirimannya dianggap spam, dan layanannya bisa berubah sewaktu-waktu di luar kendali Anda.
Untuk sistem internal dan notifikasi operasional dengan volume wajar, jalur kedua sering cukup dan jauh lebih murah. Untuk aplikasi yang mengirim ke ribuan pelanggan asing, jalur resmi lebih aman dalam jangka panjang.
Apa pun pilihannya, bungkus di balik satu antarmuka sendiri. Suatu hari Anda mungkin pindah penyedia, dan tidak ada yang ingin mencari-cari pemanggilan API yang tersebar di lima puluh berkas.
// src/notifications/whatsapp.js
export interface WhatsAppProvider {
send(to: string, message: string): Promise<{ id: string }>;
}
// Implementasi Fonnte, Cloud API, atau mock untuk pengujian
// semuanya memenuhi antarmuka yang sama.
Jangan Kirim di Dalam Request
Godaan pertama adalah memanggil API WhatsApp langsung di dalam handler HTTP. Jangan.
Pengiriman bisa memakan beberapa detik, bisa gagal karena jaringan, dan bisa kena rate limit. Kalau itu terjadi di dalam request pembuatan order, pelanggan melihat error padahal ordernya sebenarnya berhasil dibuat.
Pisahkan menjadi dua langkah. Simpan pesan ke basis data dengan status PENDING, lalu biarkan pekerja terpisah yang mengirim.
await prisma.waMessage.create({
data: {
to: normalizePhone(customer.phone),
body: renderTemplate('order_paid', { name: customer.name, id: order.id }),
status: 'PENDING',
attempts: 0,
},
});
Cara ini memberi tiga hal sekaligus: request tetap cepat, pesan tidak hilang kalau proses mati, dan Anda punya riwayat lengkap apa yang pernah dikirim ke siapa.
Normalisasi Nomor Lebih Dulu
Nomor telepon yang masuk dari formulir akan berantakan. Orang menulis 0895..., +62 895..., 62895..., dengan tanda hubung, dengan spasi, kadang dengan huruf O menggantikan angka nol.
Gateway umumnya menuntut format internasional tanpa tanda plus. Normalisasi sekali di satu tempat:
export function normalizePhone(raw) {
const digits = String(raw).replace(/\D/g, '');
if (digits.startsWith('62')) return digits;
if (digits.startsWith('0')) return '62' + digits.slice(1);
if (digits.startsWith('8')) return '62' + digits;
return null; // biarkan pemanggil memutuskan
}
Kembalikan null untuk yang tidak dikenali, jangan menebak. Nomor yang salah tebak berarti pesan pelanggan A terkirim ke orang asing, dan itu masalah privasi, bukan sekadar bug.
Simpan versi ternormalisasi di basis data, bukan versi mentahnya. Kalau tidak, Anda akan punya tiga baris pelanggan yang sebenarnya orang yang sama.
Antrean, Percobaan Ulang, dan Batas
Pekerja pengirim perlu tiga perilaku.
Batasi laju. Mengirim lima ratus pesan dalam semenit adalah cara tercepat membuat nomor diblokir. Beri jeda antar pengiriman, dan beri jeda lebih panjang untuk blast.
Coba ulang dengan mundur bertahap. Kegagalan jaringan biasanya sementara. Coba lagi setelah satu menit, lalu lima, lalu tiga puluh. Tapi bedakan jenis kegagalannya.
Berhenti pada kegagalan permanen. Nomor tidak terdaftar di WhatsApp tidak akan berubah dengan mencoba lagi seribu kali. Tandai FAILED dan berhenti.
const PERMANENT = ['invalid_number', 'not_registered', 'blocked'];
async function deliver(message) {
try {
const res = await provider.send(message.to, message.body);
await markSent(message.id, res.id);
} catch (err) {
const permanent = PERMANENT.includes(err.code);
const exhausted = message.attempts + 1 >= 5;
await prisma.waMessage.update({
where: { id: message.id },
data: {
status: permanent || exhausted ? 'FAILED' : 'PENDING',
attempts: { increment: 1 },
lastError: err.code ?? String(err).slice(0, 200),
nextAttemptAt: permanent ? null : backoff(message.attempts),
},
});
}
}
Menyimpan lastError terlihat sepele, tapi inilah yang membuat Anda bisa menjawab pertanyaan โkenapa Bu Siti tidak dapat notifikasiโ tanpa menebak.
Isi Pesan Menentukan Apakah Dibaca
Notifikasi WhatsApp muncul di ruang yang sama dengan percakapan keluarga. Pesan yang terlalu panjang atau terasa seperti robot akan diabaikan, bahkan diblokir.
Beberapa hal yang membuat perbedaan besar:
- Sebut nama penerima di awal
- Letakkan informasi terpenting di baris pertama, karena itu yang terlihat di notifikasi tanpa membuka aplikasi
- Satu pesan satu tujuan, jangan menggabungkan tagihan dan promo
- Sertakan cara menindaklanjuti, misalnya nomor order atau tautan lacak
- Tutup dengan identitas bisnis yang jelas
Pesan yang berguna terbaca seperti ini:
Halo Pak Andi, pembayaran order #INV-2291 sudah kami terima.
Total: Rp 450.000
Estimasi kirim: 2 hari kerja
Lacak: https://contoh.id/t/INV-2291
Terima kasih, Toko Sejahtera
Simpan template di basis data atau berkas konfigurasi, jangan tersebar sebagai string di dalam kode. Suatu saat pemilik bisnis akan minta ubah kalimatnya, dan itu tidak seharusnya butuh deploy.
Hormati Hak Menolak
Ini sering dilewatkan di aplikasi internal, padahal penting. Sediakan cara pelanggan berhenti menerima pesan pemasaran, dan patuhi itu.
Pisahkan dua jenis pesan. Transaksional seperti konfirmasi pembayaran atau status pengiriman umumnya diharapkan penerima. Promosional butuh persetujuan dan harus bisa dihentikan.
Satu kolom marketingOptOut pada tabel pelanggan, ditambah pemeriksaan di pekerja pengirim, sudah cukup untuk memulai. Yang penting jangan mencampur keduanya dalam satu saluran, karena satu keluhan spam bisa membuat nomor bisnis diblokir dan semua notifikasi transaksional ikut mati.
Perhatikan Webhook Status
Kebanyakan gateway bisa mengirim balik status pengiriman: terkirim, sampai, dibaca, atau gagal. Manfaatkan itu untuk melengkapi catatan Anda.
Yang paling berguna bukan status โdibacaโ, melainkan pola kegagalan. Kalau tiba-tiba banyak pesan berstatus gagal dalam waktu berdekatan, kemungkinan besar nomor pengirim sedang bermasalah, dan Anda ingin tahu itu sebelum pelanggan yang memberi tahu.
Penutup
Notifikasi WhatsApp terasa sepele sampai dipakai sungguhan. Yang membuatnya andal bukan API-nya, melainkan hal-hal di sekitarnya: nomor yang dinormalisasi, pesan yang diantrekan, kegagalan yang dibedakan, riwayat yang tersimpan, dan isi pesan yang menghormati ruang pribadi penerimanya.