← Back to all posts
2026-09-10 · MailAnvil Team

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:

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:

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

  1. Gunakan domain sendiri — Jangan pakai @gmail.com atau @yahoo.com
  2. Setup DKIM + SPF + DMARC — MailAnvil bantu setup DKIM. SPF dan DMARC ditambah manual di DNS
  3. Jangan kirim terlalu banyak sekaligus — Free plan dibatasi 500 email/bulan. Naik ke Starter jika butuh lebih
  4. Monitor bounce rate — Jika bounce rate > 5%, email bisa dipause otomatis
  5. 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.