Send Transactional Email from Vercel Edge Functions with MailAnvil
If you deploy on Vercel, your API routes already run at the edge — and email belongs there too. OTP codes, payment receipts, shipping notifications: all should fire from the same serverless function handling the request, not from a separate cron or a second provider dashboard.
This tutorial wires MailAnvil transactional email into Vercel Edge Functions. It works identically in the Node runtime, but the Edge runtime is where you'll feel the difference: no SMTP client, no long-lived sockets, just one HTTPS call to a REST API.
Why not SMTP on Vercel?
Vercel serverless functions don't keep outbound TCP connections alive reliably. Traditional SMTP libraries (Nodemailer, nodemailer-smtp-transport) assume a persistent socket — on serverless they work until they time out mid-handshake, and then your OTP email silently fails.
An HTTP-based email API sidesteps the whole class of problems: one fetch, one JSON body, done. It's the same reason Supabase Edge Functions, Cloudflare Workers, and AWS Lambda all favor API-based sending over SMTP.
Setup
Get an API key from the MailAnvil dashboard, verify your sending domain (DKIM is automatic if your DNS is on Cloudflare), then add the key in your Vercel project:
vercel env add MAILANVIL_API_KEY
vercel env add MAILANVIL_BASE # https://api.mailanvil.com
The helper
Create lib/email.ts — a tiny typed wrapper you can reuse across every route:
// lib/email.ts
type SendArgs = {
to: string | string[];
subject: string;
html: string;
text?: string;
};
export async function sendEmail({ to, subject, html, text }: SendArgs) {
const base = process.env.MAILANVIL_BASE!;
const res = await fetch(`${base}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.MAILANVIL_API_KEY}`,
},
body: JSON.stringify({
from: '[email protected]',
to: Array.isArray(to) ? to : [to],
subject,
html,
text,
}),
});
const data = await res.json();
if (res.status === 202) return data; // 202 = queued for delivery
throw new Error(`MailAnvil error ${res.status}: ${data.error?.message ?? 'unknown'}`);
}
Two things worth noting:
202 Accepted, not200. MailAnvil queues the message and returns immediately. Treat anything else as a failure and retry — don't assume the mail arrived.- Always send
text. Plain-text fallback is what spam filters and terminal-based mail readers see. HTML-only email is a deliverability smell.
The route: OTP verification
A real example — app/api/otp/route.ts using the Edge runtime:
// app/api/otp/route.ts
export const runtime = 'edge';
function otpHtml(code: string) {
return `<div style="font-family:sans-serif;max-width:420px">
<h2>Kode verifikasi kamu</h2>
<p style="font-size:28px;font-weight:700;letter-spacing:6px">${code}</p>
<p>Berlaku 10 menit. Abaikan email ini kalau kamu tidak meminta kode.</p>
</div>`;
}
export async function POST(req: Request) {
const { email } = await req.json();
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
return Response.json({ error: 'email invalid' }, { status: 400 });
}
const code = String(Math.floor(100000 + Math.random() * 900000));
// ponytail: store the code in your DB/session here with a 10-min TTL —
// this demo skips persistence so the snippet stays focused on email.
await sendEmail({
to: email,
subject: 'Kode verifikasi: ' + code,
html: otpHtml(code),
text: `Kode verifikasi kamu: ${code} (berlaku 10 menit)`,
});
return Response.json({ ok: true });
}
Input validation on email happens before anything touches the provider — never pass raw user input into an email API, or you become an open relay for someone's spam campaign.
Deploy and test
vercel deploy --prod
curl -s -X POST https://yourapp.vercel.app/api/otp \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]"}'
# {"ok":true}
Then check delivery logs in the MailAnvil dashboard — you'll see the accepted event, and the delivery event once it lands in the inbox.
Local dev
vercel dev reads .env.local, so the same code works locally:
echo 'MAILANVIL_API_KEY=xxx' >> .env.local
echo 'MAILANVIL_BASE=https://api.mailanvil.com' >> .env.local
vercel dev
Wrapping up
That's the whole integration: one helper file, one route, zero SMTP sockets. Because it's just fetch, it runs the same on Vercel Edge, Node, Bun, Deno, and Cloudflare Workers — write it once, deploy anywhere.
Indonesian pricing note: MailAnvil bills in IDR and accepts QRIS/GoPay, so no foreign-currency card fees on top of your email volume. At 100K emails/month it runs roughly 2.4x cheaper than Resend's equivalent tier.
Browse the full docs for webhook signatures, template management, and delivery logs.