Webhooks
POST ke URL kamu setelah email terkirim, bounce, atau gagal.
Webhook opsional. Tanpa URL, kirim SMTP dan API tetap 202, log dashboard, dan GET /emails/{id} jalan seperti biasa.
POST /emails bukan hasil Google. Respons 202 = pesan diterima. Hasil MX (terkirim / bounce / gagal) datang belakangan lewat webhook, atau kamu polling status.
Pasang URL di dashboard → Pengaturan. Satu URL per akun. Event v1: email.delivered, email.bounced, email.failed. SMTP dan HTTP API sama.
Tes ping dan kirim ulang event tidak memotong credit.
Payload
Satu POST JSON. Lapisan luar = event. Isi data = email + penerima yang baru final.
{
"id": "01JEXAMPLEEVENT",
"type": "email.delivered",
"created_at": "2026-09-09T07:00:00Z",
"data": {
"email_id": "01JEXAMPLEMSG",
"recipient_id": "01JEXAMPLERCPT",
"to": "user@gmail.com",
"from": "noreply@toko.id",
"subject": "Kode OTP",
"smtp_code": 250,
"smtp_response": "2.0.0 OK"
}
}| Field | Keterangan |
|---|---|
id | ID event ini. Retry dan “kirim ulang” memakai id yang sama — dedup di aplikasi kamu pakai ini, bukan waktu POST |
type | Apa yang terjadi. Lihat Tipe |
created_at | Waktu event dibuat (UTC, RFC3339), bukan waktu POST retry |
data | Detail penerima. Field yang kosong tidak dikirim (misalnya tes ping hampir tidak punya email_id) |
data
| Field | Keterangan |
|---|---|
email_id | ID pesan. Sama dengan id di GET /emails/{id} dan log dashboard. Satu pesan bisa punya beberapa penerima → beberapa webhook, email_id sama |
recipient_id | ID baris penerima itu. Bedakan dua alamat di satu pesan (To + BCC, dll.) |
to | Alamat yang baru selesai dikirim / bounce / gagal |
from | Envelope From (MAIL FROM), bukan wajib sama dengan header From di isi email |
subject | Subjek pesan, dipotong kalau panjang |
smtp_code | Kode angka dari MX tujuan (contoh 250 terkirim, 550 bounce). Hilang kalau tidak ada kode |
smtp_response | Teks jawaban MX, dipotong ~500 karakter. Berguna untuk log error |
test | true hanya di webhook.test. Event email biasa tidak punya field ini |
Tes ping dari Pengaturan: type = webhook.test, data.test = true, data.subject = Mailnation webhook ping. Tidak memotong credit.
Tipe
Satu POST = satu penerima, setelah statusnya final. Bukan saat API jawab 202.
type | Artinya |
|---|---|
email.delivered | MX tujuan terima pesan (biasanya SMTP 2xx). Masuk antrian mereka, bukan jaminan inbox |
email.bounced | Ditolak permanen (alamat tidak ada, blokir, kebijakan). Credit penerima ini dikembalikan |
email.failed | Gagal kirim setelah retry habis (timeout, server tujuan down, dll.). Bukan bounce tegas |
webhook.test | Tes ping dari Pengaturan. Bukan hasil email. data.test = true. Tidak memotong credit |
Local rate-limit (defer internal, misalnya pool Gmail) bukan event pelanggan. Kami coba kirim lagi; webhook belum ditembak.
Signature
Setiap POST memakai header:
Content-Type: application/json
User-Agent: Mailnation-Webhook/1.0
Mailnation-Signature: t=<unix>,v1=<hex>v1 = HMAC-SHA256(secret, {t}.{raw_body}) dalam hex. Secret tampil sekali saat simpan pertama atau putar rahasia (whsec_…). Tolak request jika t lebih dari ±5 menit.
import crypto from "node:crypto";
export function verifyMailnation(secret, header, rawBody) {
const parts = Object.fromEntries(
String(header || "")
.split(",")
.map((p) => p.trim().split("=")),
);
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return false;
const skew = Math.abs(Date.now() / 1000 - Number(t));
if (!Number.isFinite(skew) || skew > 300) return false;
const mac = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(v1, "hex"));
} catch {
return false;
}
}Pakai body mentah (jangan JSON.stringify ulang). Aplikasi kamu harus idempotent lewat id.
Retry
Kami POST sampai 7 kali: langsung, lalu 5 detik / 5 menit / 30 menit / 2 jam / 5 jam / 10 jam. HTTP 2xx = sukses. Timeout 5 detik. Tidak mengikuti redirect. HTTPS wajib.
Log request/response disimpan 7 hari, body dipotong 2 KB.
Dashboard API
JWT customer (login dashboard), bukan password SMTP.
| Method | Path | Keterangan |
|---|---|---|
GET | /v1/webhook | Endpoint akun (webhook: null jika belum ada) |
PUT | /v1/webhook | Simpan URL, event, enabled. Secret tampil sekali saat buat |
DELETE | /v1/webhook | Hapus URL + log |
POST | /v1/webhook/rotate-secret | Secret baru, tampil sekali |
POST | /v1/webhook/ping | Tes POST webhook.test (cooldown 15 detik) |
GET | /v1/webhook/deliveries | Riwayat. Query message_id, limit |
POST | /v1/webhook/deliveries/{id}/replay | Kirim ulang body yang sama |
URL harus https, host publik, bukan localhost / IP privat / *.mailnation.id.