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

Kirim Email Transaksional dengan Django + MailAnvil — Panduan Lengkap

Django tetap jadi framework pilihan nomor satu buat developer Python di Indonesia — baterai lengkap, ORM solid, admin panel gratis. E-commerce, fintech, dan startup SaaS lokal banyak yang jalan di atas Django.

Tapi ada satu hal yang selalu bikin ribet: ngirim email transaksional dari Django app.

Django memang punya django.core.mail bawaan. Masalahnya, fitur itu didesain buat SMTP — dan SMTP itu jadul, blocking, dan butuh setup DKIM/SPF/DMARC sendiri kalau gak mau email kamu nyangkut di folder spam.

Tutorial ini kasih kamu cara paling bersih kirim email transaksional dari Django — pakai MailAnvil REST API, satu service class, tanpa pusing SMTP.

Kenapa MailAnvil?

Beberapa alasan MailAnvil cocok buat developer Django Indonesia:

Prasyarat

Struktur Project

myproject/
├── manage.py
├── myproject/
│   └── settings.py
├── core/
│   ├── email.py          # ← service class MailAnvil
│   └── views.py
└── requirements.txt

Step 1: Simpan API Key di Settings

Jangan hardcode API key di kode. Simpan di environment variable:

# myproject/settings.py
import os

MAILANVIL_API_KEY = os.environ.get("MAILANVIL_API_KEY", "")
MAILANVIL_API_BASE = "https://api.mailanvil.com/v1"
# .env
export MAILANVIL_API_KEY="re_..."

Step 2: Service Class yang Reusable

Bikin core/email.py:

import requests
from django.conf import settings


class MailAnvilError(Exception):
    """Raised when the MailAnvil API returns a non-2xx response."""

    def __init__(self, status_code, code, message):
        self.status_code = status_code
        self.code = code
        self.message = message
        super().__init__(f"MailAnvil API error {status_code}: {code} — {message}")


class MailAnvilClient:
    """Thin wrapper around the MailAnvil /v1/send endpoint."""

    def __init__(self, api_key=None, api_base=None):
        self.api_key = api_key or settings.MAILANVIL_API_KEY
        self.api_base = api_base or settings.MAILANVIL_API_BASE

    def send(
        self,
        *,
        to,
        subject,
        from_email,
        html=None,
        text=None,
        reply_to=None,
        idempotency_key=None,
    ):
        if not self.api_key:
            raise MailAnvilError(401, "missing_api_key", "MAILANVIL_API_KEY not set")

        payload = {
            "from": from_email,
            "to": to if isinstance(to, list) else [to],
            "subject": subject,
        }
        if html:
            payload["html"] = html
        if text:
            payload["text"] = text
        if reply_to:
            payload["reply_to"] = reply_to

        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        resp = requests.post(
            f"{self.api_base}/send",
            json=payload,
            headers=headers,
            timeout=15,
        )

        if resp.status_code != 202:
            body = resp.json()
            err = body.get("error", {})
            raise MailAnvilError(
                resp.status_code,
                err.get("code", "unknown"),
                err.get("message", resp.text),
            )

        return resp.json()


# Module-level singleton — reuse across the app
mailanvil = MailAnvilClient()

Step 3: Template Email

Tambahin helper buat email yang sering dipakai:

# core/email.py (lanjutan)


def welcome_email(recipient_email, recipient_name, app_name):
    return {
        "to": recipient_email,
        "from_email": f"{app_name} <[email protected]>",
        "subject": f"Selamat datang di {app_name}!",
        "html": f"""<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <div style="text-align: center; padding: 40px 0;">
    <h1 style="color: #ff801f;">Selamat datang, {recipient_name}! 🎉</h1>
    <p style="font-size: 16px; color: #aaa;">Akun kamu di {app_name} sudah aktif.</p>
    <p style="font-size: 14px; color: #666;">Mulai kirim email transaksional sekarang.</p>
    <a href="https://app.mailanvil.com" style="display: inline-block; padding: 12px 32px; background: #ff801f; color: #000; text-decoration: none; border-radius: 9999px; font-weight: 600; margin-top: 20px;">Ke Dashboard →</a>
  </div>
  <p style="font-size: 12px; color: #444; text-align: center; margin-top: 40px;">Dikirim oleh {app_name} via MailAnvil</p>
</body>
</html>""",
    }


def password_reset_email(recipient_email, reset_link):
    return {
        "to": recipient_email,
        "from_email": "Security <[email protected]>",
        "subject": "Reset Password Kamu",
        "html": f"""<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <h1 style="color: #ff801f;">Reset Password</h1>
  <p style="color: #aaa;">Klik tombol di bawah untuk reset password kamu. Link berlaku 1 jam.</p>
  <a href="{reset_link}" style="display: inline-block; padding: 12px 32px; background: #ff801f; color: #000; text-decoration: none; border-radius: 9999px; font-weight: 600; margin: 20px 0;">Reset Password →</a>
  <p style="font-size: 12px; color: #444;">Kalau kamu tidak minta reset password, abaikan email ini.</p>
</body>
</html>""",
    }


def order_confirmation_email(recipient_email, recipient_name, order_no, total, payment_link):
    return {
        "to": recipient_email,
        "from_email": "Billing <[email protected]>",
        "subject": f"Order #{order_no} — {total}",
        "html": f"""<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <h1 style="color: #ff801f;">Order Baru</h1>
  <p style="color: #aaa;">Hai {recipient_name},</p>
  <div style="background: #111; padding: 24px; border-radius: 12px; border: 1px solid rgba(214,235,253,0.19); margin: 20px 0;">
    <p style="margin: 0; color: #888;">Nomor Order</p>
    <p style="margin: 4px 0 16px; font-size: 18px; font-weight: 600;">#{order_no}</p>
    <p style="margin: 0; color: #888;">Total</p>
    <p style="margin: 4px 0; font-size: 24px; font-weight: 700; color: #ff801f;">{total}</p>
  </div>
  <a href="{payment_link}" style="display: inline-block; padding: 12px 32px; background: #ff801f; color: #000; text-decoration: none; border-radius: 9999px; font-weight: 600;">Bayar Sekarang →</a>
</body>
</html>""",
    }

Step 4: Pakai di View

# core/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_POST

from .email import mailanvil, welcome_email, order_confirmation_email, MailAnvilError


@require_POST
def signup_view(request):
    # ... simpan user ke DB ...
    user_email = request.POST["email"]
    user_name = request.POST["name"]

    try:
        mailanvil.send(**welcome_email(user_email, user_name, "WarungOnline"))
    except MailAnvilError as e:
        # Log error, jangan bikin signup gagal
        import logging
        logging.getLogger(__name__).error("welcome email failed: %s", e)

    return JsonResponse({"status": "created"}, status=201)

Kenapa Gak Pakai django.core.mail?

django.core.mail punya email backend SMTP. Tapi di production, itu berarti:

  1. Blocking I/O — SMTP handshake blocking di request thread, bikin response lambat
  2. Setup DNS sendiri — DKIM/SPF/DMARC manual, gampang kena spam folder
  3. Retry manual — gak ada idempotency, gampang kirim email duplikat saat retry
  4. No dashboard — gak ada monitoring bounce/complaint

MailAnvil REST API ngasih semua itu lewat satu requests.post. Kamu tetep bisa pakai Django apa adanya — cuma ganti backend email-nya.

Production Tips

1. Idempotency Key untuk Safe Retry

Gunakan business ID yang stabil, bukan UUID random:

# Notifikasi pembayaran — kunci berdasarkan order ID
mailanvil.send(
    **order_confirmation_email(
        "[email protected]", "Budi", "ORD-2026-0891", "Rp 149.000",
        "https://warungonline.com/bayar/ord-0891",
    ),
    idempotency_key=f"order-ORD-2026-0891",  # ← stabil, aman retry
)

Kalau request timeout dan kamu retry, email yang sama gak bakal terkirim dua kali.

2. Kirim di Background Thread

Jangan blocking HTTP response. Pakai thread sederhana:

import threading


def send_async(**kwargs):
    threading.Thread(
        target=mailanvil.send, kwargs=kwargs, daemon=True
    ).start()

Untuk production scale-up, ganti dengan Celery + Redis (queue worker yang proper). Thread OK buat trafik awal.

3. Catching Error di Sentry / Monitoring

Bungkus semua mailanvil.send(...) di try/except MailAnvilError. Email gagal = log, bukan crash request. Kalau status_code 5xx, retry dengan idempotency key yang sama.

Perbandingan dengan Approach Lain

Approach Dependency Blocking Retry Harga IDR
django.core.mail SMTP 0 (bawaan) Ya Manual Tergantung SMTP
django-anymail + provider 1 dep + akun Provider-dependent Built-in USD + kurs
Resend SDK 1 dep Tidak Built-in USD + kurs
MailAnvil REST 1 (requests) Tidak Idempotency key IDR + QRIS

requests itu udah jadi dependency standar hampir semua Django project — kemungkinan besar udah kepasang, gak perlu tambahan apa-apa.

Kesimpulan

Django + MailAnvil = kombinasi yang pas buat startup Indonesia. Kamu dapet:

Coba sendiri: daftar di mailanvil.com, verifikasi domain, dan jalankan contoh kode di atas dalam 5 menit.


Butuh API key? Daftar early access di mailanvil.com — gratis 500 email/bulan.