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

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:

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?


Try MailAnvil free at mailanvil.com — 500 emails/month, no credit card, QRIS-ready.