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

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:

Best Practices

  1. Idempotency-Key is mandatory — key on orderId so a retry or double webhook can't send two emails.
  2. Verify your domain first — DKIM is required to stay out of spam. MailAnvil auto-configures DKIM for domains on Cloudflare DNS.
  3. Watch bounces — MailAnvil's kill switch pauses sending if hard-bounce exceeds 5%, protecting your domain reputation automatically.
  4. Keep keys in secrets — use process.env and .env, never commit credentials.

Start Free

Free plan: 500 emails/month, no credit card. Pay with QRIS/GoPay. Full Bahasa Indonesia docs.

➡️ Try MailAnvil Free — mailanvil.com