Kirim Email Transaksional dari FastAPI — Tutorial Lengkap dengan MailAnvil
FastAPI jadi framework Python favorit untuk backend API. Tapi banyak developer lupa: setiap endpoint yang terima user signup, booking, atau pembayaran butuh email transaksional.
Artikel ini tunjukkan cara kirim email dari FastAPI pakai MailAnvil — API email Indonesia yang harganya dalam Rupiah.
Kenapa Email dari FastAPI?
Setiap aplikasi SaaS Indonesia butuh email:
- Booking confirmation — Traveloka-like app kirim email konfirmasi pemesanan
- Invoice pembayaran — Tokopedia-like marketplace kirim receipt setelah bayar
- OTP verifikasi — Fintech seperti Kredivo kirim kode OTP saat login
- Password reset — Semua app butuh ini
Tanpa email transaksional, user tidak tahu apakah transaksi berhasil.
MailAnvil: API Email Indonesia
MailAnvil pakai AWS SES sebagai backend, tapi API-nya dibangun di Cloudflare Workers — edge delivery global. Yang bikin beda:
- Harga Rupiah — Starter Rp149rb/bulan untuk 10.000 email. Gratis plan ada 500 email/bulan
- Bayar pakai QRIS/GoPay — Tidak perlu kartu kredit
- Bahasa Indonesia — Dokumentasi dan support dalam Bahasa
- MCP-native — Bisa dipakai AI agent langsung
Daftar gratis di mailanvil.com.
Setup
1. Buat Akun MailAnvil
curl -X POST https://api.mailanvil.com/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "secure_password"}'
Response berisi API key. Simpan — ini yang dipakai setiap request.
2. Verifikasi Domain
Email hanya bisa dikirim dari domain yang sudah diverifikasi DKIM.
curl -X POST https://api.mailanvil.com/v1/domains \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "startup.id"}'
MailAnvil kirim record DKIM yang harus ditambahkan di DNS. Di Cloudflare, tinggal tambah TXT record.
3. Install Dependencies
pip install fastapi uvicorn httpx
Implementasi
Basic Email Sender
from fastapi import FastAPI
from pydantic import BaseModel
import httpx
app = FastAPI()
MAILANVIL_API_KEY = "key_xxxxxxxxxxxx"
MAILANVIL_BASE = "https://api.mailanvil.com/v1"
class EmailRequest(BaseModel):
to: str
subject: str
html: str
text: str | None = None
@app.post("/send-email")
async def send_email(email: EmailRequest):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{MAILANVIL_BASE}/send",
headers={
"Authorization": f"Bearer {MAILANVIL_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": "[email protected]",
"to": [email.to],
"subject": email.subject,
"html": email.html,
"text": email.text or email.subject,
},
)
if response.status_code == 202:
return {"status": "queued", "email_id": response.json().get("email_id")}
return {"error": response.json()}
Booking Confirmation Email
Contoh nyata: Travel booking app kirim email konfirmasi.
from datetime import datetime
@app.post("/book")
async def create_booking(booking: BookingRequest):
# Simpan booking ke database
booking_id = save_booking(booking)
# Kirim email konfirmasi
html = f"""
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #ff801f;">Booking Berhasil! 🎉</h2>
<p>Halo {booking.name},</p>
<p>Pemesanan kamu sudah terkonfirmasi:</p>
<ul>
<li><strong>Booking ID:</strong> {booking_id}</li>
<li><strong>Tujuan:</strong> {booking.destination}</li>
<li><strong>Tanggal:</strong> {booking.date}</li>
<li><strong>Total:</strong> Rp {booking.total:,.0f}</li>
</ul>
<p style="margin-top: 20px;">
<a href="https://startup.id/booking/{booking_id}"
style="background: #ff801f; color: white; padding: 10px 20px;
text-decoration: none; border-radius: 5px;">
Lihat Detail
</a>
</p>
</div>
"""
async with httpx.AsyncClient() as client:
await client.post(
f"{MAILANVIL_BASE}/send",
headers={
"Authorization": f"Bearer {MAILANVIL_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": "[email protected]",
"to": [booking.email],
"subject": f"Booking {booking_id} Terkonfirmasi",
"html": html,
},
)
return {"booking_id": booking_id, "status": "confirmed"}
OTP Email untuk Fintech
Fintech butuh OTP yang sampai ke inbox, bukan spam folder.
import secrets
@app.post("/send-otp")
async def send_otp(email: str):
otp_code = secrets.token_hex(3).upper() # 6 digit hex
# Simpan OTP dengan expiry 5 menit
save_otp(email, otp_code, expires_in=300)
html = f"""
<div style="font-family: sans-serif; max-width: 400px; margin: 0 auto;
text-align: center; padding: 20px;">
<h2 style="color: #ff801f;">Kode Verifikasi</h2>
<p style="font-size: 32px; font-weight: bold;
letter-spacing: 8px; color: #333;">
{otp_code}
</p>
<p style="color: #666; font-size: 14px;">
Kode ini berlaku selama 5 menit.
</p>
<p style="color: #999; font-size: 12px;">
Jangan bagikan kode ini kepada siapapun.
</p>
</div>
"""
async with httpx.AsyncClient() as client:
await client.post(
f"{MAILANVIL_BASE}/send",
headers={
"Authorization": f"Bearer {MAILANVIL_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": "[email protected]",
"to": [email],
"subject": "Kode Verifikasi Login",
"html": html,
},
)
return {"status": "sent"}
Invoice Email untuk Marketplace
Marketplace kirim invoice setelah pembayaran berhasil.
@app.post("/payment/webhook")
async def payment_webhook(webhook_data: dict):
# Verifikasi webhook signature
if not verify_webhook(webhook_data):
return {"error": "invalid signature"}
order_id = webhook_data["order_id"]
order = get_order(order_id)
html = f"""
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #1a1a1a; color: white; padding: 20px;
border-radius: 8px 8px 0 0;">
<h2 style="color: #ff801f; margin: 0;">Invoice Pembayaran</h2>
</div>
<div style="padding: 20px; border: 1px solid #ddd; border-top: none;">
<table style="width: 100%; border-collapse: collapse;">
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 8px 0;">Order ID</td>
<td style="padding: 8px 0; text-align: right;">{order_id}</td>
</tr>
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 8px 0;">Item</td>
<td style="padding: 8px 0; text-align: right;">{order.item_name}</td>
</tr>
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 8px 0;">Qty</td>
<td style="padding: 8px 0; text-align: right;">{order.quantity}</td>
</tr>
<tr style="font-weight: bold; font-size: 18px;">
<td style="padding: 12px 0;">Total</td>
<td style="padding: 12px 0; text-align: right; color: #ff801f;">
Rp {order.total:,.0f}
</td>
</tr>
</table>
<p style="color: #666; margin-top: 20px; font-size: 14px;">
Pembayaran diterima pada {datetime.now().strftime('%d %B %Y, %H:%M')} WIB
</p>
</div>
</div>
"""
async with httpx.AsyncClient() as client:
await client.post(
f"{MAILANVIL_BASE}/send",
headers={
"Authorization": f"Bearer {MAILANVIL_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": "[email protected]",
"to": [order.customer_email],
"subject": f"Invoice {order_id} — Pembayaran Berhasil",
"html": html,
},
)
return {"status": "processed"}
Cek Status Email
Setelah kirim, cek status delivery pakai email ID:
@app.get("/email/{email_id}/status")
async def email_status(email_id: str):
async with httpx.AsyncClient() as client:
response = await client.get(
f"{MAILANVIL_BASE}/emails/{email_id}",
headers={"Authorization": f"Bearer {MAILANVIL_API_KEY}"},
)
return response.json()
Response berisi status per-recipient: delivered, bounced, complained, atau deferred.
Error Handling
from fastapi import HTTPException
@app.post("/send-email-safe")
async def send_email_safe(email: EmailRequest):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{MAILANVIL_BASE}/send",
headers={
"Authorization": f"Bearer {MAILANVIL_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": "[email protected]",
"to": [email.to],
"subject": email.subject,
"html": email.html,
},
)
if response.status_code == 202:
return response.json()
if response.status_code == 429:
raise HTTPException(
status_code=429,
detail="Rate limit exceeded. Try again later."
)
if response.status_code == 403:
raise HTTPException(
status_code=403,
detail="Domain not verified. Check DKIM setup."
)
raise HTTPException(
status_code=response.status_code,
detail=response.json().get("error", {}).get("message", "Unknown error")
)
Tips Deliverability untuk Developer Indonesia
- Gunakan domain sendiri — Jangan pakai
@gmail.comatau@yahoo.com - Setup DKIM + SPF + DMARC — MailAnvil bantu setup DKIM. SPF dan DMARC ditambah manual di DNS
- Jangan kirim terlalu banyak sekaligus — Free plan dibatasi 500 email/bulan. Naik ke Starter jika butuh lebih
- Monitor bounce rate — Jika bounce rate > 5%, email bisa dipause otomatis
- Gunakan HTML yang clean — Email client Indonesia (Gmail, Yahoo) lebih suka HTML sederhana
Kenapa MailAnvil vs Internasional?
| Fitur | MailAnvil | Resend | SendGrid |
|---|---|---|---|
| Harga 10K email | Rp149rb | $20 (~Rp320rb) | $20 (~Rp320rb) |
| Bayar Rupiah | ✅ QRIS/GoPay | ❌ USD only | ❌ USD only |
| Support Bahasa | ✅ | ❌ | ❌ |
| MCP-native | ✅ | ❌ | ❌ |
| Edge delivery | ✅ CF Workers | ✅ | ✅ |
Kalau startup kamu target pasar Indonesia, bayar email dalam Rupiah lebih masuk akal. Tidak perlu konversi, tidak perlu kartu kredit internasional.
Conclusion
FastAPI + MailAnvil = kombosi sempurna untuk backend Python yang butuh email transaksional. Dalam 10 baris kode, aplikasi kamu sudah bisa kirim booking confirmation, OTP, dan invoice.
Coba gratis di mailanvil.com — 500 email/bulan, tidak perlu kartu kredit.
CTA: Kirim email pertama kamu dari FastAPI hari ini. Daftar di mailanvil.com.