Send Transactional Email from SvelteKit with MailAnvil (Edge + IDR Billing)
SvelteKit is the framework a lot of Indonesian indie hackers and AI SaaS builders reach for — one codebase, server routes next to components, deploy to Vercel/Netlify/Cloudflare. The piece everyone still wires up wrong is email: welcome flows, OTP codes, payment receipts.
This tutorial adds transactional email to SvelteKit in one +server.ts route with MailAnvil. No SDK dependency, no queue to run, no credit card needed to start.
Why MailAnvil
For an SvelteKit app shipping to Indonesian users, three things decide the email provider: latency to Jakarta, how you pay, and whether the docs are in your language.
| MailAnvil | Resend | SendGrid | |
|---|---|---|---|
| Delivery to Jakarta | ~10ms edge (CF Workers) | ~180ms | ~200ms |
| Payment | QRIS, GoPay, bank transfer | Credit card only | Credit card only |
| 10k emails/mo | Rp 149rb | $20 (~Rp 320rb) | $19.95 (~Rp 319rb) |
| Docs | Bahasa + English | English | English |
MailAnvil runs on Cloudflare Workers, so an API call from your SvelteKit server (or directly from a Jakarta user) is answered at the nearest edge. You pay in rupiah.
Step 1: Scaffold SvelteKit
npm create svelte@latest my-app
# choose: Skeleton project, TypeScript
cd my-app
npm install
Step 2: Add the API key
.env:
MAILANVIL_API_KEY=ma_key_xxxxxxxxxxxxx
MAILANVIL_BASE=https://api.mailanvil.com/v1
Add the key in your hosting dashboard too (Vercel/Netlify/Cloudflare) so it's available at deploy time.
Step 3: One +server.ts route
// src/routes/api/send-email/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
const BASE = import.meta.env.VITE_MAILANVIL_BASE ?? process.env.MAILANVIL_BASE!;
const KEY = process.env.MAILANVIL_API_KEY!;
export const POST: RequestHandler = async ({ request }) => {
const { to, subject, html, text, idempotencyKey } = await request.json();
const res = await fetch(`${BASE}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${KEY}`,
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: JSON.stringify({
from: '[email protected]',
to: [to],
subject,
html,
text,
}),
});
if (res.status === 202) return json({ ok: true, id: (await res.json()).id });
const err = await res.json();
return json({ ok: false, error: err.error?.message ?? res.status }, { status: 502 });
};
That's the whole integration. Call it from a form action, a webhook handler, or a cron job.
Step 4: Send from a form action
// src/routes/contact/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ request, fetch }) => {
const data = await request.formData();
const to = data.get('email')?.toString() ?? '';
const res = await fetch('/api/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to,
subject: 'Welcome to yourapp',
html: '<h1>Welcome!</h1><p>Your account is ready.</p>',
text: 'Welcome! Your account is ready.',
idempotencyKey: crypto.randomUUID(),
}),
});
return res.ok ? { success: true } : fail(502, { error: 'Send failed' });
},
};
The idempotency pattern (don't skip)
A user who double-taps "Send verification email" should get one email, not two. MailAnvil dedupes on Idempotency-Key, so a retried request with the same key is a no-op. Generate the key once per logical action and reuse it across retries — crypto.randomUUID() per submission, or a hash of userId + action.
Real Indonesian use cases
- AI SaaS onboarding. A new signup triggers a welcome + OTP. Edge delivery means a Jakarta user sees the code while the loading spinner is still up.
- E-commerce receipts. SvelteKit storefronts send the QRIS payment receipt the second the webhook lands.
- Booking platforms. Confirmation + reminder emails from the same
+server.tsroute, no separate worker to maintain.
Best practices
- Never hardcode
from. Use a verified domain address ([email protected]) or MailAnvil rejects the send. - Ship both
htmlandtext. Plain-text fallback matters for deliverability and for email clients that strip HTML. - Handle 202, not 200. MailAnvil returns
202 Acceptedfor a queued send. Treat anything else as a failure path. - Use
Idempotency-Keyon every retriable send.
Try it free
500 emails/month free, no credit card. Verify your domain, grab a key, and the route above works as-is.
➡️ Try MailAnvil free — mailanvil.com
Pay in IDR via QRIS or GoPay. Docs in Bahasa Indonesia and English. Support via Telegram.