send-otp-verification-email-indonesia-fintech-20260724
Why Not SendGrid/Resend for Indonesian OTPs?
Three numbers matter:
| MailAnvil | Resend | SendGrid | |
|---|---|---|---|
| 10K emails/mo | Rp 149rb | Rp 300rb+ | Rp 370rb+ |
| Jakarta latency | ~80ms | ~300ms | ~400ms |
| Local payment | QRIS, GoPay ✅ | Credit card only ❌ | Credit card only ❌ |
OTP emails are time-sensitive. A 300ms API round trip to us-east-1 adds up when you're sending thousands of verifications per minute during peak hours.
Step 1: Sign Up and Get Your API Key
# Sign up at mailanvil.com
curl -X POST https://api.mailanvil.com/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "dev@yourstartup.id", "password": "your-secure-password"}'
Response:
{
"api_key": "ma_live_01j...",
"plan": "free",
"daily_quota": 10
}
Free plan gives you 500 emails/month. Verify your domain and the daily quota ramps up to 1,000.
Step 2: Verify Your Domain
Before sending, verify domain ownership:
curl -X POST https://api.mailanvil.com/v1/domains \
-H "Authorization: Bearer ma_live_01j..." \
-H "Content-Type: application/json" \
-d '{"domain": "yourstartup.id"}'
This returns DNS records (TXT + DKIM CNAMEs). Add them to your Cloudflare DNS — MailAnvil auto-detects Cloudflare and can verify in seconds.
Step 3: Install the Node.js SDK
npm install @mailanvil/node
Or use raw fetch — MailAnvil is a simple REST API:
// mailanvil.js
const MAILANVIL_KEY = process.env.MAILANVIL_API_KEY;
const BASE = 'https://api.mailanvil.com/v1';
async function sendEmail({ to, subject, html }) {
const res = await fetch(`${BASE}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${MAILANVIL_KEY}`,
},
body: JSON.stringify({
from: 'noreply@yourstartup.id',
to: [to],
subject,
html,
}),
});
return res.json();
}
Step 4: Send OTP Emails
const crypto = require('crypto');
function generateOTP() {
return crypto.randomInt(100000, 999999).toString();
}
const otpStore = new Map(); // Use Redis in production
app.post('/api/auth/send-otp', async (req, res) => {
const { email } = req.body;
const otp = generateOTP();
// Store OTP with 5-minute expiry
otpStore.set(email, { otp, expiresAt: Date.now() + 5 * 60 * 1000 });
const { id, status } = await sendEmail({
to: email,
subject: 'Kode Verifikasi Anda — YourStartup',
html: `
<div style="font-family: sans-serif; max-width: 480px; margin: 0 auto;">
<h2>Kode Verifikasi Anda</h2>
<p>Gunakan kode berikut untuk verifikasi akun:</p>
<div style="background: #f4f4f5; padding: 24px; border-radius: 12px; text-align: center; margin: 24px 0;">
<span style="font-size: 32px; font-weight: 700; letter-spacing: 4px; color: #ff801f;">${otp}</span>
</div>
<p style="color: #71717a; font-size: 14px;">Kode berlaku 5 menit. Jangan bagikan kode ini ke siapa pun.</p>
<hr style="border: none; border-top: 1px solid #e4e4e7; margin: 24px 0;" />
<p style="color: #a1a1aa; font-size: 12px;">Dikirim oleh YourStartup melalui MailAnvil</p>
</div>
`,
});
res.json({ success: true, message: 'OTP sent' });
});
Step 5: Verify the OTP
app.post('/api/auth/verify-otp', (req, res) => {
const { email, otp } = req.body;
const stored = otpStore.get(email);
if (!stored) return res.status(400).json({ error: 'No OTP requested' });
if (Date.now() > stored.expiresAt) {
otpStore.delete(email);
return res.status(400).json({ error: 'OTP expired' });
}
if (stored.otp !== otp) return res.status(400).json({ error: 'Invalid OTP' });
otpStore.delete(email);
res.json({ success: true, message: 'Verified' });
});
Real Indonesian Use Cases
Fintech — Payment Confirmation OTP
AkuLaku, Kredivo, Bank Jago: Every loan disbursement or large transfer triggers a confirmation email. With MailAnvil's 80ms Jakarta latency, your OTP lands before the user switches apps.
Travel — Booking Verification
Tiket.com, Traveloka, Pegipegi: Booking confirmations + e-ticket attachments. MailAnvil's API accepts HTML and plain text, so your PDF ticket lands alongside the summary.
Food Delivery — Order Receipts
GoFood, GrabFood, ShopeeFood: Every order generates an email receipt. At 100K orders/day, the IDR pricing difference vs SendGrid saves Rp 2.5jt/month.
Error Handling
OTP delivery failures happen. Network blips, full inboxes, typos. Handle them:
try {
const result = await sendEmail({ to, subject, html });
if (result.status === 'queued') {
// Poll email status or set up a webhook
console.log(`OTP queued: ${result.id}`);
}
} catch (err) {
// Fallback to SMS OTP via Twilio/Vonage
console.error('Email delivery failed, fallback to SMS');
}
Set up webhooks to track delivery status:
curl -X POST https://api.mailanvil.com/v1/webhooks \
-H "Authorization: Bearer ma_live_01j..." \
-d '{"url": "https://yourstartup.id/api/webhooks/mailanvil", "events": ["delivered", "bounced", "complained"]}'
What's Different About MailAnvil
- MCP-native. AI coding agents (Claude, Cursor, Copilot) discover MailAnvil's endpoints automatically. Your AI builds the email integration for you.
- Built on Cloudflare Workers. Edge deployment means Jakarta, Singapore, and Tokyo all get sub-100ms latency.
- Pay with QRIS or GoPay. No credit card needed. This alone eliminates the #1 friction for Indonesian indie devs.
- Bahasa Indonesia docs.
kirim emailnotsend email. Documentation that speaks your language.
Start with 500 free emails at mailanvil.com. No credit card. QRIS ready.
Shipping transactional email for Indonesia's next wave of fintech, e-commerce, and SaaS.