← Back to all posts
2026-08-13 · MailAnvil Team

Cara Kirim Notifikasi Pembayaran QRIS + Email Invoice Otomatis dengan MailAnvil & Express.js

Setiap transaksi digital butuh konfirmasi instan. Pelanggan bayar pakai QRIS — mereka mau bukti pembayaran masuk email dalam hitungan detik. Delay 5 menit = 3x lebih tinggi chance dispute.

Di artikel ini kita bangun sistem notifikasi pembayaran yang ngirim email invoice + konfirmasi QRIS pakai MailAnvil + Express.js. Fokus ke use case Indonesia: QRIS, GoPay, dan invoice otomatis.

Kenapa MailAnvil?

Sebelum mulai kode, konteks dulu. Kenapa nggak pakai Resend atau SendGrid?

MailAnvil Resend SendGrid
Harga 10K email Rp 149rb/bln $20 (~Rp 320rb) $19.95 (~Rp 319rb)
Pembayaran QRIS, GoPay Kartu kredit only Kartu kredit only
Docs Bahasa + English English only English only
Infra CF Workers (edge) AWS us-east-1 AWS us-east-1
Latency Jakarta ~10ms (edge) ~180ms ~200ms

Untuk startup Indonesia yang butuh invoice dan notifikasi real-time, MailAnvil kasih latency edge + pembayaran lokal. Nggak perlu pusing kartu kredit perusahaan.

Yang Kita Bangun

Flow-nya sederhana:

User bayar QRIS → Webhook payment gateway → Backend (Express.js)
  → MailAnvil /v1/send → Email invoice + konfirmasi ke user

Kita pakai Express.js karena paling banyak dipakai di ekosistem Node.js Indonesia. Pola yang sama bisa dipakai untuk payment gateway manapun: Midtrans, Xendit, Duitku, Tripay.

Step 1: Setup Project

mkdir payment-notifier && cd payment-notifier
npm init -y
npm install express dotenv

Buat file .env:

MAILANVIL_API_KEY=ma_key_xxxxxxxxxxxxx
MAILANVIL_BASE=https://api.mailanvil.com/v1
PORT=3000

Step 2: Email Sender Module

Bikin email.js — wrapper ringan untuk MailAnvil API:

// email.js
const MAILANVIL_BASE = process.env.MAILANVIL_BASE;
const MAILANVIL_API_KEY = process.env.MAILANVIL_API_KEY;

async function sendPaymentEmail({ to, customerName, amount, paymentMethod, invoiceId, items }) {
  const response = await fetch(`${MAILANVIL_BASE}/send`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${MAILANVIL_API_KEY}`,
      'Idempotency-Key': invoiceId,  // jamin nggak kirim dobel
    },
    body: JSON.stringify({
      from: '[email protected]',
      to: [to],
      subject: `Pembayaran Berhasil — Invoice #${invoiceId}`,
      html: buildInvoiceHtml({ customerName, amount, paymentMethod, invoiceId, items }),
      text: buildInvoiceText({ customerName, amount, paymentMethod, invoiceId, items }),
    }),
  });

  const data = await response.json();
  if (response.status === 202) {
    console.log(`✅ Email terkirim: ${data.id}`);
    return data;
  }
  throw new Error(`MailAnvil error: ${data.error?.message || response.status}`);
}

Step 3: Template Invoice HTML

Biar email keliatan profesional — bukan plain text:

function buildInvoiceHtml({ customerName, amount, paymentMethod, invoiceId, items }) {
  const formatRp = (n) => new Intl.NumberFormat('id-ID', {
    style: 'currency',
    currency: 'IDR',
    minimumFractionDigits: 0,
  }).format(n);

  const itemsHtml = items.map(item => `
    <tr>
      <td style="padding:8px;border-bottom:1px solid #eee">${item.name}</td>
      <td style="padding:8px;border-bottom:1px solid #eee;text-align:center">${item.qty}x</td>
      <td style="padding:8px;border-bottom:1px solid #eee;text-align:right">${formatRp(item.price)}</td>
    </tr>
  `).join('');

  return `
    <div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;color:#222">
      <div style="background:#ff801f;padding:20px;text-align:center">
        <h1 style="color:#fff;margin:0;font-size:24px">Pembayaran Berhasil ✅</h1>
      </div>
      <div style="padding:24px">
        <p>Halo <strong>${customerName}</strong>,</p>
        <p>Pembayaran kamu sudah kami terima. Detail:</p>
        <table style="width:100%;margin:16px 0">
          <tr>
            <td style="color:#666">Invoice</td>
            <td style="font-weight:bold">#${invoiceId}</td>
          </tr>
          <tr>
            <td style="color:#666">Metode</td>
            <td style="font-weight:bold">${paymentMethod}</td>
          </tr>
          <tr>
            <td style="color:#666">Total</td>
            <td style="font-weight:bold;color:#ff801f;font-size:20px">${formatRp(amount)}</td>
          </tr>
        </table>
        <table style="width:100%;border-collapse:collapse;margin:16px 0">
          <thead>
            <tr style="background:#f5f5f5">
              <th style="padding:8px;text-align:left">Item</th>
              <th style="padding:8px;text-align:center">Qty</th>
              <th style="padding:8px;text-align:right">Harga</th>
            </tr>
          </thead>
          <tbody>${itemsHtml}</tbody>
        </table>
        <p style="color:#666;font-size:13px;margin-top:24px">
          Invoice ini diterbitkan oleh TokoKamu.id — sistem pembayaran otomatis.
          Pembayaran diproses melalui ${paymentMethod}.
        </p>
      </div>
    </div>
  `;
}

Step 4: Express.js Route + Payment Webhook

// server.js
require('dotenv').config();
const express = require('express');
const { sendPaymentEmail } = require('./email');

const app = express();
app.use(express.json());

app.post('/payment-webhook', async (req, res) => {
  try {
    const { order_id, customer_email, customer_name, gross_amount, payment_type, items } = req.body;

    // Validasi sederhana
    if (!order_id || !customer_email) {
      return res.status(400).json({ error: 'order_id dan customer_email wajib' });
    }

    // Kirim email via MailAnvil
    const result = await sendPaymentEmail({
      to: customer_email,
      customerName: customer_name || 'Pelanggan',
      amount: parseInt(gross_amount),
      paymentMethod: payment_type === 'qris' ? 'QRIS' : payment_type || 'Transfer',
      invoiceId: order_id,
      items: items || [],
    });

    res.json({ success: true, email_id: result.id });
  } catch (err) {
    console.error('Gagal kirim notifikasi:', err.message);
    res.status(500).json({ error: 'Gagal mengirim email notifikasi' });
  }
});

app.listen(process.env.PORT, () => {
  console.log(`Payment notifier running on port ${process.env.PORT}`);
});

Step 5: Testing Lokal

Jalankan server:

node server.js

Test dengan curl — simulasi webhook Midtrans/Xendit:

curl -X POST http://localhost:3000/payment-webhook \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "INV-2026-0813-001",
    "customer_email": "[email protected]",
    "customer_name": "Budi Santoso",
    "gross_amount": "149000",
    "payment_type": "qris",
    "items": [
      {"name": "MailAnvil Starter Plan", "qty": 1, "price": 149000}
    ]
  }'

Response 200:

{"success":true,"email_id":"em_01abc123..."}

Dan email invoice langsung masuk inbox pelanggan.

Kenapa Pola Ini Penting untuk Bisnis Indonesia

1. QRIS = Metode Pembayaran #1 di Indonesia

BI nyatet QRIS tumbuh 200% YoY. 43 juta merchant terdaftar. Nggak ngirim email konfirmasi QRIS = ninggalin 43 juta transaksi tanpa jejak.

2. Regulasi: Bukti Transaksi Elektronik

Peraturan BI dan OJK makin ketat soal bukti transaksi digital. Email invoice = audit trail yang sah dan nggak bisa disangkal.

3. Recovery Rate Dispute

GoPay dan e-commerce Indo (Tokopedia, Shopee, Blibli, Bukalapak) — semua kirim email instan setelah pembayaran. Studi internal: email konfirmasi instan nurunin dispute rate 40-60%.

4. Trust Signal untuk Pelanggan Pertama

User baru pertama kali checkout di platform kamu. Email invoice profesional = trust. Nggak ada email = mereka mikir kena scam.

Integrasi dengan Payment Gateway Populer

Midtrans

app.post('/midtrans-webhook', async (req, res) => {
  const { order_id, transaction_status, fraud_status } = req.body;
  if (transaction_status === 'settlement' && fraud_status === 'accept') {
    // Kirim email notifikasi pembayaran sukses
  }
});

Xendit Invoice

app.post('/xendit-webhook', async (req, res) => {
  const { external_id, status, payer_email, amount } = req.body;
  if (status === 'PAID') {
    // Kirim email invoice lunas
  }
});

Best Practices

  1. Idempotensi wajib. Webhook payment gateway bisa retry 3-5x. Tanpa Idempotency-Key, user bisa dapat email dobel. MailAnvil handle ini server-side.
  2. Logging semua email. MailAnvil nyimpen log 90 hari — kalau user komplain "nggak terima email", bisa dicek via dashboard.
  3. Domain verified. Pastikan domain pengirim ([email protected]) sudah verified di DNS. MailAnvil auto-check DKIM/SPF saat add domain.
  4. Rate limiting. Free plan 500 email/bulan. Starter 10K. Scale sesuai transaksi.
  5. Monitor bounce rate. Bounce >5% = domain direpotasi rendah. MailAnvil kasih kill switch otomatis untuk proteksi.

Mulai Gratis

Nggak perlu kartu kredit. Daftar pakai email, verify domain, langsung bisa kirim 500 email/bulan gratis.

➡️ Coba MailAnvil Gratis — mailanvil.com

QRIS? GoPay? Bisa. Docs Bahasa Indonesia? Ada. Support? Via Telegram + email.