Webhook status pengiriman email: cara menerima, memverifikasi, dan memproses event

Cara memakai webhook untuk melacak status email: jenis event, verifikasi tanda tangan, idempotensi, dan kenapa diterima tidak sama dengan terkirim.

Tim CapiMailTerbit 3 menit baca
Daftar isi
  1. Status email itu berlapis
  2. Jenis event webhook
  3. Selalu verifikasi tanda tangan
  4. Praktik terbaik pemrosesan event
  5. Uji webhook dengan test mode

Setelah API email mengembalikan respons sukses, perjalanan email baru dimulai. Email masih harus diproses, diserahkan ke server penerima, lalu diterima atau ditolak. Webhook adalah cara paling efisien untuk mengetahui apa yang terjadi, tanpa harus bertanya berulang kali ke API.

Status email itu berlapis

Satu kesalahan umum adalah menganggap email "terkirim" begitu API membalas. Di CapiMail, status pesan bergerak melalui beberapa tahap:

StatusArti
queuedTersimpan dengan aman di antrean.
processingSedang diproses untuk diserahkan ke penyedia.
provider_acceptedPenyedia pengiriman menerima email, belum tentu sampai.
deliveredServer penerima mengonfirmasi menerima email.
soft_bounced / hard_bouncedDitolak sementara atau permanen.
complainedPenerima menandai email sebagai spam.
failedGagal dikirim.

Jenis event webhook

  • email.sent: email diserahkan ke penyedia pengiriman.
  • email.delivered: server penerima menerima email.
  • email.bounced: email ditolak server penerima.
  • email.complained: penerima melaporkan email sebagai spam.
  • email.failed: email gagal dikirim.

Selalu verifikasi tanda tangan

Endpoint webhook Anda adalah URL publik. Siapa pun bisa mengirim permintaan palsu ke sana. Karena itu setiap webhook CapiMail ditandatangani, dan Anda harus memverifikasi tanda tangannya sebelum mempercayai isinya. Setiap permintaan membawa header X-CapiMail-Signature berformat t=<detik-unix>,v1=<hex>. Nilai v1 adalah HMAC-SHA256 dari string <t>.<body-mentah> dengan signing secret endpoint Anda.

Contoh route handler Next.js yang memverifikasi lalu memproses event· ts
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.CAPIMAIL_WEBHOOK_SECRET!;
const TOLERANCE_SECONDS = 300;

function isValidSignature(header: string | null, rawBody: string): boolean {
  if (!header) return false;
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isFinite(t)) return false;
  // Tolak permintaan lama untuk mencegah replay.
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

export async function POST(request: Request) {
  const rawBody = await request.text(); // body mentah, jangan di-parse dulu
  if (!isValidSignature(request.headers.get("x-capimail-signature"), rawBody)) {
    return new Response("invalid signature", { status: 401 });
  }
  const event = JSON.parse(rawBody); // { id, type, created_at, data: { message_id } }

  // Idempotensi: x-capimail-event-id sama dengan event.id. Abaikan yang sudah diproses.
  // Proses cepat, lalu balas 2xx. Kerja berat masukkan ke antrean.
  await handleEmailEvent(event);
  return new Response(null, { status: 204 });
}

Saat Anda merotasi secret, lebih dari satu secret bisa berlaku sementara waktu. Terima permintaan jika tanda tangannya cocok dengan salah satu secret yang masih aktif. Payload hanya berisi message_id, tanpa alamat penerima atau isi email, jadi ambil detail lain lewat API bila perlu.

Praktik terbaik pemrosesan event

  1. Balas cepat. Kembalikan status 2xx secepat mungkin, lalu lakukan pekerjaan berat secara asinkron.
  2. Siap menerima duplikat. Respons non-2xx dan timeout dicoba ulang dengan exponential backoff (15 detik, 30 detik, 60 detik, dan seterusnya, maksimal satu jam, hingga 8 percobaan). Simpan ID event yang sudah diproses dan abaikan pengulangan.
  3. Jangan bergantung pada urutan. Event bisa tiba tidak berurutan. Jangan menurunkan status delivered hanya karena event yang lebih lama datang belakangan.
  4. Tindak lanjuti bounce dan complaint. Tandai alamat bermasalah di database Anda dan minta pengguna memperbaruinya.
  5. Simpan secret di server. Secret webhook sama sensitifnya dengan API key.

Uji webhook dengan test mode

Dengan kunci cm_test_, CapiMail mengirim webhook yang ditandatangani untuk setiap skenario simulasi, misalnya alamat bounce@ untuk hard bounce dan complaint@ untuk complaint. Anda bisa menguji seluruh alur pemrosesan event tanpa mengirim email sungguhan. Lihat dokumentasi untuk detail payload.

Pertanyaan yang sering diajukan

Apa beda status provider_accepted dan delivered?

provider_accepted berarti penyedia pengiriman sudah menerima email untuk dikirim. delivered berarti server penerima mengonfirmasi menerima email. Hanya delivered yang menandakan email sampai ke server penerima.

Kenapa webhook saya menerima event yang sama dua kali?

Pengiriman webhook bisa diulang jika endpoint lambat atau gagal merespons. Rancang pemrosesan secara idempoten dengan menyimpan ID event yang sudah diproses.

Bagaimana cara memverifikasi webhook CapiMail?

Setiap webhook membawa header X-CapiMail-Signature berformat t=<detik-unix>,v1=<hex>. Hitung ulang HMAC-SHA256 dari string <t>.<body-mentah> dengan signing secret endpoint, bandingkan secara constant-time, dan tolak jika t lebih dari 300 detik dari waktu sekarang.

Semua artikel