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:
- Harga Rupiah — gak kena markup kurs USD 12-35%
- Bayar QRIS/GoPay — gak perlu kartu kredit internasional
- REST API simpel — satu endpoint
POST /v1/send, gantiin SMTP sepenuhnya - Dokumentasi Bahasa Indonesia — contoh kode dalam konteks lokal
- MCP-native — AI agent kamu bisa kirim email langsung
- Idempotency-Key — aman retry tanpa email duplikat
Prasyarat
- Python 3.10+
- Django 5.x
- Library
requests(dipasang viapip install requests) - Akun MailAnvil (daftar gratis di mailanvil.com)
- Domain terverifikasi di MailAnvil
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:
- Blocking I/O — SMTP handshake blocking di request thread, bikin response lambat
- Setup DNS sendiri — DKIM/SPF/DMARC manual, gampang kena spam folder
- Retry manual — gak ada idempotency, gampang kirim email duplikat saat retry
- 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:
- ✅ Tetep Django way — ORM, admin, views semua jalan normal
- ✅ Non-blocking — background thread, response tetep cepet
- ✅ Idempotency key — aman retry tanpa email duplikat
- ✅ Harga Rupiah, bayar QRIS — gak pusing kurs USD
- ✅ Dashboard + monitoring — bounce/complaint tracking built-in
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.