Send OTP Verification Emails in 5 Minutes with MailAnvil + Next.js API Routes
Your user just signed up. You need to send a 6-digit OTP to their inbox — fast, reliable, and out of the spam folder. If that OTP arrives 30 seconds late, your conversion rate tanks. If it lands in spam, the user bounces.
This tutorial shows you how to build a production-ready OTP email flow with MailAnvil and Next.js API routes. No third-party auth libraries. No US-dollar billing surprises. Just clean code and Rupiah-friendly pricing.
Why MailAnvil for OTP Emails?
Indonesian fintech apps — think OVO, Dana, GoPay — send millions of OTP emails daily. They need:
- Sub-second delivery via Cloudflare's edge network (Jakarta edge included)
- IDR pricing — Rp 149rb/month for 10,000 emails (that's ~$10, not $35 like SendGrid)
- QRIS/GoPay payment — pay with the same wallet your users use
- Bahasa documentation — no more guessing what "bounce rate" means in production
Step 1: Get Your MailAnvil API Key
curl -X POST https://api.mailanvil.com/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "your-secure-password"}'
Response:
{
"api_key": "key_01J...",
"plan": "free",
"quota": 500
}
Verify your sending domain in the MailAnvil dashboard — add the DKIM TXT records and you're ready.
Step 2: Create the Next.js API Route
// app/api/auth/send-otp/route.ts
import { NextRequest, NextResponse } from "next/server";
const MAILANVIL_API = "https://api.mailanvil.com/v1/send";
const MAILANVIL_KEY = process.env.MAILANVIL_API_KEY!;
function generateOTP(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
export async function POST(req: NextRequest) {
const { email } = await req.json();
if (!email || !email.includes("@")) {
return NextResponse.json(
{ error: "Invalid email" },
{ status: 400 }
);
}
const otp = generateOTP();
// Store OTP in your database with a 5-minute expiry
// await db.otp.create({ email, code: otp, expiresAt: Date.now() + 300_000 });
const response = await fetch(MAILANVIL_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${MAILANVIL_KEY}`,
},
body: JSON.stringify({
from: "[email protected]",
to: [email],
subject: "Your OTP Code — YourStartup",
html: `
<div style="max-width:480px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,sans-serif">
<div style="background:#000;padding:32px;border-radius:12px;border:1px solid rgba(214,235,253,0.19)">
<h1 style="color:#fff;font-size:24px;margin:0 0 8px">Your OTP Code</h1>
<p style="color:#94a3b8;margin:0 0 24px">Use this code to verify your account. Expires in 5 minutes.</p>
<div style="background:#1a1a2e;border-radius:8px;padding:24px;text-align:center">
<span style="font-family:monospace;font-size:36px;color:#ff801f;letter-spacing:8px;font-weight:bold">${otp}</span>
</div>
<p style="color:#64748b;font-size:13px;margin:24px 0 0">
If you didn't request this code, ignore this email.
</p>
</div>
</div>
`,
}),
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{ error: data.error?.message || "Failed to send OTP" },
{ status: response.status }
);
}
return NextResponse.json({
success: true,
message: "OTP sent",
emailId: data.email_id,
});
}
That's it. One file. No SDK to install, no SMTP config to wrestle with.
Step 3: Verify the OTP
// app/api/auth/verify-otp/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const { email, otp } = await req.json();
// Check OTP against your database
// const stored = await db.otp.findValid(email, otp);
// if (!stored) return NextResponse.json({ error: "Invalid or expired OTP" }, { status: 400 });
// Mark as used, issue session token
// await db.otp.markUsed(stored.id);
return NextResponse.json({ success: true, token: "your-jwt-token" });
}
Real Indonesian Use Cases
Travel booking (Tiket.com, Traveloka): Send booking confirmation + e-ticket OTP. MailAnvil's Jakarta edge delivers in <200ms for Indonesian recipients.
Food delivery (GoFood, GrabFood): Order confirmation emails with real-time tracking links. Free tier handles 500 emails/month — enough for MVP testing.
Fintech OTP (OVO, Dana, Bank Jago): Login verification codes that arrive instantly. MailAnvil's SES-backed delivery means Gmail "Important" tag and inbox placement.
Why Not SendGrid or Resend?
| MailAnvil | SendGrid | Resend | |
|---|---|---|---|
| Starter price | Rp 149rb/10K emails | $19.95/50K emails (~Rp 320rb) | $20/50K emails |
| IDR billing | ✅ Yes | ❌ | ❌ |
| QRIS/GoPay | ✅ Yes | ❌ | ❌ |
| Bahasa docs | ✅ Yes | ❌ | ❌ |
| MCP-native | ✅ Yes | ❌ | ✅ Yes |
| Jakarta edge | ✅ CF Workers | ❌ | ❌ |
At 100K emails/month: MailAnvil = Rp 599rb. SendGrid = ~Rp 1.1jt. That's 45% savings — enough to pay for your Supabase Pro plan.
Deploy to Production
# Add your API key to Vercel
vercel env add MAILANVIL_API_KEY
# Deploy
vercel --prod
Your OTP flow is live. Check the MailAnvil dashboard for delivery metrics, bounce rates, and per-email status.
What's Next?
- Add rate limiting per IP (3 OTPs per minute per email)
- Add idempotency keys so retries don't send duplicate OTPs
- Add webhooks to track delivery status and auto-retry on soft bounces
- Use the MCP server (
mcp.mailanvil.com) to let your AI agents send emails directly
Try MailAnvil free at mailanvil.com — 500 emails/month, no credit card, QRIS-ready.