Send Order Confirmation Emails with Remix + MailAnvil
A customer checks out, the payment webhook fires, and then... silence. If the confirmation email lands late, that customer opens a support ticket, files a chargeback, or abandons the cart thinking the order failed.
For Indonesian e-commerce, the order-confirmation email is the single highest-value transactional message you send. This tutorial wires it up in a Remix app using MailAnvil.
Why Remix + MailAnvil?
Remix runs your action and loader on the server, so the send call lives next to the data it needs — no separate worker to orchestrate. MailAnvil is edge-native (Cloudflare Workers), so the email goes out from the closest point of presence instead of a us-east datacenter half a world away.
| MailAnvil | Resend | SendGrid | |
|---|---|---|---|
| 10K emails/mo | Rp 149rb | ~Rp 320rb | ~Rp 319rb |
| Payment | QRIS, GoPay | Credit card | Credit card |
| Docs | Bahasa + English | English | English |
| Latency | Edge (Jakarta PoP) | us-east | us-east |
What We Build
POST /checkout → Remix action → create order → MailAnvil /v1/send → customer inbox
Step 1: Scaffold Remix
npx create-remix@latest tokopaedia --template remix-run/remix/templates/remix
cd tokopaedia
npm install
Step 2: Store Secrets
Never hardcode keys. Use .env:
MAILANVIL_API_KEY=mk_live_xxxxxxxx
[email protected]
Step 3: The Checkout Action
app/routes/checkout.tsx:
import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
const BASE = "https://api.mailanvil.com/v1";
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const email = String(form.get("email"));
const orderId = crypto.randomUUID();
const total = Number(form.get("total"));
if (!email || !total) {
return json({ error: "email and total are required" }, { status: 400 });
}
// 1. Persist the order in your DB here.
// await db.orders.insert({ orderId, email, total, status: "pending" });
// 2. Send the confirmation email.
const res = await fetch(`${BASE}/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.MAILANVIL_API_KEY}`,
"Idempotency-Key": orderId,
},
body: JSON.stringify({
from: process.env.MAILANVIL_FROM,
to: [email],
subject: `Your order ${orderId} is confirmed 🎉`,
html: buildReceiptHtml({ orderId, total }),
text: `Order ${orderId} confirmed. Total: Rp${total.toLocaleString("id-ID")}.`,
}),
});
if (res.status !== 202) {
const err = await res.json();
return json({ error: err.error?.message || "send failed" }, { status: 502 });
}
return json({ queued: true, orderId });
}
Step 4: Receipt Template
app/lib/email.ts:
export function buildReceiptHtml(d: { orderId: string; total: number }) {
return `
<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
<h1 style="color:#ff801f">Order ${d.orderId} confirmed</h1>
<p>Terima kasih! We're packing your order now.</p>
<p style="font-size:20px"><strong>Total: Rp${d.total.toLocaleString("id-ID")}</strong></p>
<a href="https://tokopaedia.id/orders/${d.orderId}"
style="display:inline-block;background:#ff801f;color:#fff;padding:12px 24px;border-radius:9999px;text-decoration:none">
Track order
</a>
</div>`;
}
Step 5: Test
npm run dev
curl -X POST http://localhost:3000/checkout \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "[email protected]&total=285000"
Response {"queued":true,"orderId":"..."} and the receipt lands in under a second.
Real Use Cases in Indonesia
The same POST /v1/send endpoint covers the three most common transactional messages:
- E-commerce order confirmation — marketplaces and D2C stores send a receipt the moment the payment webhook clears.
Idempotency-Key=orderId, so a webhook retry never double-sends. - Food delivery invoices — GoFood/GrabFood merchants send the digital struk + delivery status. High-volume lunch rushes make the flat IDR price matter at the margin.
- OTP verification for fintech — DANA/OVO-style apps send a code as an SMS fallback. Edge delivery means the OTP doesn't expire before it arrives.
Best Practices
- Idempotency-Key is mandatory — key on
orderIdso a retry or double webhook can't send two emails. - Verify your domain first — DKIM is required to stay out of spam. MailAnvil auto-configures DKIM for domains on Cloudflare DNS.
- Watch bounces — MailAnvil's kill switch pauses sending if hard-bounce exceeds 5%, protecting your domain reputation automatically.
- Keep keys in secrets — use
process.envand.env, never commit credentials.
Start Free
Free plan: 500 emails/month, no credit card. Pay with QRIS/GoPay. Full Bahasa Indonesia docs.