Lacak status email secara real-time butuh lebih dari sekadar cek API secara polling. Artikel ini menjelaskan arsitektur event-driven yang proper untuk terima dan proses email event dari webhook, termasuk endpoint idempotent, queue processing, duplicate handling, dan korelasi data internal.
Email Anda terkirim. Log menunjukkan HTTP 200. Tapi apa yang terjadi setelahnya? Email itu sampai ke inbox? Dibuka? Diklik? Atau langsung bounce tanpa Anda ketahui?
Kalau Anda hanya bergantung pada SMTP response, Anda buta terhadap semua itu. Email marketing dan email transaksional adalah dua hal yang berbeda. Yang kedua butuh accountability: Anda perlu tahu apakah email sampai, kapan dibuka, dan kapan gagal. Tanpa data itu, Anda tidak bisa improve dan tidak bisa debug.
Di sinilah webhook email jadi critical infrastructure.
Daftar Isi
Kenapa Polling Tidak Cukup untuk Lacak Status Email
Polling adalah cara lama. Anda jalanin script tiap 30 detik, cek ke API provider, dapat status, simpan ke database. Ini bekerja, tapi ada tiga masalah fundamental.
Pertama, latency. Anda tidak tahu kapan status berubah. Mungkin email sudah delivered 20 detik lalu, tapi polling Anda baru jalan 25 detik kemudian. Untuk use case yang butuh respons real-time, ini tidak bisa diterima.
Kedua, rate limit. Provider email seperti SendGrid, Mailgun, atau KIRIM.EMAIL punya batas request per detik atau per menit. Kalau aplikasi Anda punya ribuan user yang bersamaan, polling akan cepat kena rate limit dan Anda tidak dapat data sama sekali.
Ketiga, missing events. Provider tidak menyimpan semua event selamanya. Biasanya 30 hari. Kalau polling gagal karena server down atau network issue, event yang terjadi saat itu hilang.
Webhook menyelesaikan ketiganya. Provider mendorong event ke endpoint Anda saat itu juga, tidak perlu tanya-tanya.
Setup Webhook Endpoint yang Idempotent
Hal pertama yang sering salah: developer treat webhook endpoint seperti API endpoint biasa. Endpoint biasa mengembalikan data kalau dipanggil. Webhook endpoint harus bisa menerima panggilan yang sama berkali-kali dan tetap memberikan response yang benar.
Email provider biasanya retry webhook kalau endpoint mereka timeout atau dapat response non-2xx. Retry ini bisa terjadi 3-5 kali dengan interval yang meningkat. Artinya satu event email bisa mengenai endpoint Anda beberapa kali dalam hitungan menit.
Kalau endpoint Anda memproses setiap request tanpa deduplication, Anda akan dapat duplicate event. User Anda bisa dapat notifikasi tiga kali. Analytics Anda salah. Laporan Anda kacau.
Solusinya: idempotent endpoint. Setiap event punya UUID unik. Sebelum proses, cek apakah UUID itu sudah ada di database. Kalau sudah ada, return 200 OK tapi jangan proses lagi.
Contoh sederhana di Node.js:
app.post('/webhook/email', async (req, res) => {
const eventId = req.body.event_id;
// Cek apakah event sudah diproses
const exists = await db.query(
'SELECT id FROM email_events WHERE event_id = $1',
[eventId]
);
if (exists.rows.length > 0) {
return res.status(200).json({ received: true, duplicate: true });
}
// Proses event baru
await db.query(
'INSERT INTO email_events (event_id, event_type, timestamp, metadata) VALUES ($1, $2, $3, $4)',
[eventId, req.body.event_type, req.body.timestamp, JSON.stringify(req.body)]
);
await processEmailEvent(req.body);
return res.status(200).json({ received: true });
});
Pattern yang sama berlaku untuk PHP, Python, Go, atau bahasa apapun yang Anda pakai. Kuncinya adalah check-before-insert dengan event_id yang uniqueness.
Event Email Apa Saja yang Bisa Anda Terima
Tidak semua provider kirim event yang sama. Tapi ada subset standar yang didukung oleh mayoritas provider email modern.
sent adalah event pertama. Email sudah berhasil dikirim dari server provider ke server tujuan. Ini bukan guarantee bahwa email sampai inbox, hanya berarti server tujuan menerima email.
delivered artinya server tujuan sudah menerima email dan memberitahu provider. Di banyak kasus, delivered adalah indikasi terkuat bahwa email sampai. Tapi delivered bukan guarantee inbox, email tetap bisa masuk spam setelahnya.
bounce terjadi ketika email tidak bisa dikirim ke tujuan. Hard bounce adalah alamat email yang tidak valid secara permanen, misalnya domain mati atau user tidak ada. Soft bounce adalah masalah sementara, misalnya mailbox penuh atau server tujuan timeout.
spam event dikirim ketika email masuk ke folder spam di sisi penerima. Provider yang punya inbox placement data bisa memberikan informasi ini lewat webhook. KIRIM.EMAIL memberikan spam event kalau email terdeteksi masuk folder spam.
open dan click adalah engagement event. Open berarti email dibuka, click berarti ada link di dalam email yang diklik. Kedua event ini menggunakan tracking pixel atau link redirect. Tanpa tracking, Anda tidak dapat data ini.
unsubscribe dan complaint adalah opt-out event. Complaint terjadi ketika user melapor spam di sisi klien, ini sangat penting untuk reputasi sending domain Anda. Untuk memahami bagaimana complaint email bisa merusak reputasi domain, Anda bisa baca cara setup list-unsubscribe header sebagai langkah preventif.
Kenapa Anda Perlu Message Queue untuk Process Webhook
Mendapat webhook itu mudah. Memprosesnya dengan benar itu susah.
Webhooks datang secara asynchronous dan bisa datang bersamaan dalam jumlah besar. Kalau Anda proses secara synchronous di dalam request handler, satu event yang lambat akan menahan seluruh queue. Request timeout akan terjadi dan provider akan retry, menambah beban.
Queue adalah jawabannya. Pisahkan penerimaan webhook dari pemrosesan event. Webhook endpoint hanya terima request, validasi event_id, masukkan ke queue, dan return 200 OK secepat mungkin. Pemrosesan aktual happen di background worker.
Contoh arsitektur dengan Redis dan Bull:
// Webhook receiver — cepat, tidak blocking
app.post('/webhook/email', async (req, res) => {
const eventId = req.body.event_id;
// Minimal validation
if (!eventId || !req.body.event_type) {
return res.status(400).json({ error: 'invalid payload' });
}
// Masukkan ke queue, jangan proses langsung
await emailQueue.add('process-event', req.body, {
jobId: eventId,
attempts: 3,
backoff: { type: 'exponential', delay: 1000 }
});
return res.status(200).json({ received: true });
});
// Queue worker — proses di background
emailQueue.process('process-event', async (job) => {
const event = job.data;
await processEmailEvent(event);
});
Keuntungan pendekatan ini: throughput tinggi, retry otomatis kalau processing gagal, dan decoupling antara receive dan process layer. Kalau worker Anda down, jobs tetap di queue dan akan diproses ketika worker kembali online. Untuk panduan lengkap soal queue di Laravel, lihat tutorial kirim email Laravel 11.
Cara Handle Duplicate Events dan Race Conditions
Even dengan idempotent endpoint dan queue, duplikat masih bisa terjadi dari dua sumber: provider retry dan queue retry. Kedua hal ini saling orthogonal.
Provider retry adalah ketika email provider Push webhook ke endpoint Anda, endpoint tidak respond 2xx, provider retry. Ini biasanya hanya terjadi 1-3 kali dengan delay exponential.
Queue retry adalah ketika processing di dalam worker gagal dan queue system otomatis retry dengan delay exponential.
Kombinasi keduanya bisa menghasilkan empat-lima kali duplikat untuk satu event. Dengan idempotent check di database, ini tidak jadi masalah untuk data integrity. Tapi performance overhead tetap ada.
Untuk high-volume sistem, ada teknik tambahan: distributed lock. Sebelum proses event, acquire lock dengan event_id sebagai key. Kalau lock gagal didapat, artinya event sedang diproses di worker lain. Skip.
Contoh dengan Redis:
const lockKey = `lock:event:${eventId}`;
const lockAcquired = await redis.set(lockKey, '1', 'NX', 'EX', 60);
if (!lockAcquired) {
// Event sedang diproses di tempat lain
return;
}
try {
await processEmailEvent(event);
} finally {
await redis.del(lockKey);
}
Dengan distributed lock, duplicate processing jadi tidak mungkin. Overhead yang ditambahkan hanya satu Redis call per event, yang secara performance sangat murah.
Korelasikan Email Event dengan Data Internal Anda
Event webhook memberi Anda data tentang apa yang terjadi di sisi provider. Tapi untuk aplikasi Anda, data itu meaningless tanpa konteks internal.
Contoh: Anda kirim email invoice ke user. Anda tahu dari webhook bahwa email itu delivered. Tapi Anda juga perlu tahu invoice_id mana yang ada di email itu, user_id mana yang harus dapat notifikasi, dan apakah status invoice harus berubah.
Correlation ID adalah solusi. Buat UUID unik untuk setiap email yang Anda kirim, masukkan ke header email atau metadata SMTP, lalu ikutkan di webhook payload oleh provider. KIRIM.EMAIL webhook menyertakan message_id yang bisa Anda pakai sebagai correlation key. Baca juga cara cek email bisa dikirim atau tidak sebagai precondition sebelum melacak status.
Dengan cara ini, alurnya jadi circular: Anda kirim email dengan invoice_id di metadata, dapat webhook delivered dengan message_id, lalu update database Anda:
// Kirim email dengan korelasi
await sendEmail({
to: user.email,
subject: `Invoice #${invoice.id}`,
metadata: {
invoice_id: invoice.id,
user_id: user.id,
message_id: uuidv4()
}
});
// Terima webhook delivered
app.post('/webhook/email', async (req, res) => {
const { message_id, event_type } = req.body;
if (event_type === 'delivered') {
await db.query(
'UPDATE invoices SET status = $1, delivered_at = $2 WHERE message_id = $3',
['delivered', new Date(), message_id]
);
// Notify dashboard, update analytics, dll
}
});
Tanpa korelasi, webhook delivered hanya jadi event tanpa aksi. Dengan korelasi, Anda punya sistem tracking yang actionable.
Kalau masalahnya ada di infrastruktur webhook yang tidak bisa Anda kontrol sendiri, KIRIM.EMAIL Dev menyediakan webhook endpoint yang sudah siap pakai dengan retry policy bawaan dan dashboard untuk monitor status email per event. Cukup konfigurasi URL endpoint Anda di dashboard dan semua event akan push secara otomatis.
FAQ
Apa bedanya webhook email dengan polling API untuk cek status email?
Webhook adalah provider yang mendorong data ke Anda secara real-time ketika status berubah. Polling adalah aplikasi Anda yang secara periodik menarik data dari provider. Webhook lebih efisien untuk high volume karena tidak ada rate limit issue dan tidak ada delay. Polling lebih simpel untuk low volume tapi tidak scalable dan ada latency inherent.
Bagaimana cara terima webhook email di aplikasi web saya?
Anda perlu endpoint HTTP POST yang publicly accessible. Endpoint harus bisa handle request yang datang secara asynchronous. Endpoint harus idempotent karena provider retry ketika tidak dapat response 2xx. Untuk testing lokal, gunakan tools seperti ngrok atau bore untuk expose localhost ke internet. KIRIM.EMAIL menyediakan webhook configuration di dashboard tanpa perlu coding infrastruktur khusus.
Kenapa webhook email bisa gagal terkirim?
Provider webhook bisa gagal karena endpoint Anda tidak respond dalam timeout (umumnya 10-30 detik), endpoint Anda respond dengan error code, network issue antara provider dan server Anda, atau server Anda overloaded dan tidak bisa accept connection. Provider email berkualitas punya retry mechanism dengan exponential backoff untuk menangani failure sementara. KIRIM.EMAIL retry webhook hingga 5 kali dengan interval yang meningkat.
Bagaimana cara pastikan webhook tidak terduplikasi di sistem saya?
Pakai event_id atau message_id unik yang datang di webhook payload sebagai idempotency key. Sebelum proses, cek di database apakah event_id itu sudah ada. Kalau sudah ada, skip processing tapi tetap return 200 OK agar provider tidak retry. Untuk sistem dengan banyak worker, gunakan distributed lock dengan Redis untuk pastikan satu event hanya diproses satu worker pada satu waktu.
Apakah saya harus pakai queue untuk process webhook? Kenapa tidak langsung proses?
Queue memberikan reliability dan scalability yang tidak bisa diberikan processing langsung. Dengan queue, kalau worker Anda down, jobs tidak hilang dan diproses ketika worker kembali. Queue juga memungkinkan retry otomatis dan parallel processing. Untuk aplikasi dengan volume rendah dan tidak ada requirement real-time, processing langsung masih acceptable. Tapi untuk production system dengan volume tinggi, queue adalah keharusan.
Bagaimana cara test webhook email tanpa kirim email beneran?
Gunakan webhook testing tools seperti RequestBin, Webhook.site, atau ngrok. Buat endpoint sementara di layanan tersebut, konfigurasi sebagai webhook URL di provider email, lalu kirim test email. Anda bisa lihat raw payload yang dikirim provider dan test apakah endpoint Anda handle dengan benar. Untuk testing idempotency dan retry logic, Anda bisa replay request manual berkali-kali ke endpoint Anda.
Apa itu email event correlation ID dan kenapa penting?
Correlation ID adalah identifier unik yang Anda masukkan ke metadata email saat kirim, yang kemudian muncul lagi di webhook payload ketika event terjadi. Dengan correlation ID, Anda bisa link event webhook ke record internal Anda. Tanpa ini, Anda hanya tahu bahwa email delivered tapi tidak tahu invoice atau user mana yang terkait. KIRIM.EMAIL webhook sudah menyertakan message_id yang bisa Anda pakai untuk korelasi ini.
Hasbi Putra adalah Head of Marketing di KIRIM.EMAIL, email delivery infrastructure untuk developer dan tim IT di Indonesia. KIRIM.EMAIL mengirim lebih dari 11 juta email per hari dengan server yang sepenuhnya berlokasi di Indonesia.
