MailnationDocs

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"
  }
}
FieldKeterangan
idID event ini. Retry dan “kirim ulang” memakai id yang sama — dedup di aplikasi kamu pakai ini, bukan waktu POST
typeApa yang terjadi. Lihat Tipe
created_atWaktu event dibuat (UTC, RFC3339), bukan waktu POST retry
dataDetail penerima. Field yang kosong tidak dikirim (misalnya tes ping hampir tidak punya email_id)

data

FieldKeterangan
email_idID pesan. Sama dengan id di GET /emails/{id} dan log dashboard. Satu pesan bisa punya beberapa penerima → beberapa webhook, email_id sama
recipient_idID baris penerima itu. Bedakan dua alamat di satu pesan (To + BCC, dll.)
toAlamat yang baru selesai dikirim / bounce / gagal
fromEnvelope From (MAIL FROM), bukan wajib sama dengan header From di isi email
subjectSubjek pesan, dipotong kalau panjang
smtp_codeKode angka dari MX tujuan (contoh 250 terkirim, 550 bounce). Hilang kalau tidak ada kode
smtp_responseTeks jawaban MX, dipotong ~500 karakter. Berguna untuk log error
testtrue 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.

typeArtinya
email.deliveredMX tujuan terima pesan (biasanya SMTP 2xx). Masuk antrian mereka, bukan jaminan inbox
email.bouncedDitolak permanen (alamat tidak ada, blokir, kebijakan). Credit penerima ini dikembalikan
email.failedGagal kirim setelah retry habis (timeout, server tujuan down, dll.). Bukan bounce tegas
webhook.testTes 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.

MethodPathKeterangan
GET/v1/webhookEndpoint akun (webhook: null jika belum ada)
PUT/v1/webhookSimpan URL, event, enabled. Secret tampil sekali saat buat
DELETE/v1/webhookHapus URL + log
POST/v1/webhook/rotate-secretSecret baru, tampil sekali
POST/v1/webhook/pingTes POST webhook.test (cooldown 15 detik)
GET/v1/webhook/deliveriesRiwayat. Query message_id, limit
POST/v1/webhook/deliveries/{id}/replayKirim ulang body yang sama

URL harus https, host publik, bukan localhost / IP privat / *.mailnation.id.

On this page