Kirim E-Tiket Event dengan Lampiran QR Code — MailAnvil + Hono
Platform event dan ticketing tumbuh cepat di Indonesia — dari workshop komunitas tech di Jakarta sampai konser indie di Bandung. Pola yang sama juga kepakai di hotel booking dan kelas online. Semuanya butuh satu hal yang sama: e-tiket yang masuk inbox pembeli, lengkap dengan QR code untuk check-in.
Nah, di sini banyak developer keteteran. SMTP server sendiri? Attachment sering gagal, email nyangkut spam. Skill AWS? Format invoice-nya ribet. Tutorial ini pakai jalur paling bersih: REST API MailAnvil dengan attachment base64, dari aplikasi Hono (jalan di Node, Bun, Deno, dan Cloudflare Workers).
Kenapa bukan SMTP biasa?
Tiga alasan praktis:
- Deliverability. Email transaksional (tiket = bukti pembayaran) wajib masuk inbox. SMTP self-host tanpa SPF/DKIM/DMARC yang benar = spam folder. MailAnvil handle DKIM via Cloudflare DNS otomatis.
- Attachment itu first-class. Satu field
attachmentsdi payload JSON. Gak perlu MIME multipart manual. - Harga Rupiah. Bayar QRIS/GoPay, gak perlu kartu kredit internasional. Mulai Rp 0 gratis 3.000 email/bulan — cukup buat event komunitas awal.
Skema attachment MailAnvil
Endpoint POST /v1/send menerima dua bentuk attachment:
{
"attachments": [
{ "filename": "e-tiket.pdf", "content": "<base64>", "content_type": "application/pdf" },
{ "filename": "qr.png", "path": "https://cdn.example.com/qr/abc.png" }
]
}
content— base64 langsung (untuk file yang kamu generate, misal QR code)path— URL publik, MailAnvil yang fetch (untuk file yang sudah ada di CDN)
Batas: sampai 10 file, total 25 MB. Cukup untuk e-tiket + invoice + peta venue.
Setup Hono
mkdir e-tiket-api && cd e-tiket-api
bun init -y # atau npm init -y
bun add hono @hono/node-server nanoid
Struktur minimal:
src/
index.ts # server
tiket.ts # generate QR + kirim email
Langkah 1 — Generate QR code sebagai base64
Untuk tutorial ini pakai library qrcode (tanpa dependensi canvas, murni data URL):
bun add qrcode
// src/tiket.ts
import QRCode from 'qrcode';
export async function qrBase64(payload: string): Promise<string> {
// QR berisi token check-in unik — jangan pakai data sensitif mentah
const buf = await QRCode.toBuffer(payload, { width: 512, margin: 1 });
return buf.toString('base64');
}
Token check-in sebaiknya disimpan di database kamu (status: unused → checked_in), bukan cuma di dalam QR. QR hanya pembawa token — server yang memvalidasi.
Langkah 2 — Service kirim e-tiket
// src/tiket.ts (lanjutan)
import { qrBase64 } from './tiket';
const API_BASE = 'https://api.mailanvil.com/v1';
const API_KEY = process.env.MAILANVIL_API_KEY!; // simpan di env, jangan hardcode
export async function kirimETiket(opts: {
email: string;
nama: string;
namaEvent: string;
tanggalEvent: string;
venue: string;
tokenCheckin: string;
orderRef: string;
}) {
const qr = await qrBase64(
`https://app-eventkamu.com/checkin?token=${opts.tokenCheckin}`
);
const res = await fetch(`${API_BASE}/send`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: '[email protected]',
to: [opts.email],
subject: `E-Tiket ${opts.namaEvent} — ${opts.orderRef}`,
html: `
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto;">
<h2 style="color:#ff801f;">E-Tiket ${opts.namaEvent}</h2>
<p>Halo ${opts.nama},</p>
<p>Pesanan <strong>${opts.orderRef}</strong> terkonfirmasi. Simpan email ini —
tampilkan QR di bawah saat check-in.</p>
<table style="width:100%; background:#f6f6f6; border-radius:8px; padding:16px;">
<tr><td>Event</td><td><strong>${opts.namaEvent}</strong></td></tr>
<tr><td>Tanggal</td><td>${opts.tanggalEvent}</td></tr>
<tr><td>Venue</td><td>${opts.venue}</td></tr>
</table>
<p>QR code juga terlampir sebagai PNG biar gampang di-screenshot.</p>
<p style="color:#888; font-size:12px;">Email ini dikirim otomatis — mohon tidak dibalas.</p>
</div>`,
text: `E-Tiket ${opts.namaEvent} — ${opts.orderRef}. Token check-in: ${opts.tokenCheckin}`,
attachments: [
{
filename: `e-tiket-${opts.orderRef}.png`,
content: qr,
content_type: 'image/png',
},
],
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`Gagal kirim e-tiket: ${res.status} ${JSON.stringify(err)}`);
}
return res.json(); // 202 — email masuk queue
}
Catatan penting:
fromharus domain terverifikasi di dashboard MailAnvil (DKIM verified). Domain baru? Verifikasi dulu lewatPOST /v1/domains.- Response
202artinya masuk antrian — bukan langsung terkirim. Untuk status akhir, pollingGET /v1/emails/:idatau pasang webhook event.
Langkah 3 — Endpoint di Hono
// src/index.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { kirimETiket } from './tiket';
const app = new Hono();
app.post('/webhook/pembayaran', async (c) => {
// ponytail: langsung percaya payload payment gateway —
// di production, verifikasi signature webhook (Xendit/Midtrans) dulu
const { email, nama, order_ref, nama_event, tanggal, venue, token } =
await c.req.json();
const result = await kirimETiket({
email, nama,
namaEvent: nama_event,
tanggalEvent: tanggal,
venue,
tokenCheckin: token,
orderRef: order_ref,
});
return c.json({ ok: true, email_id: result.id }, 202);
});
serve({ fetch: app.fetch, port: 3000 });
Jalankan:
MAILANVIL_API_KEY=re_... bun run src/index.ts
curl -X POST localhost:3000/webhook/pembayaran -H 'Content-Type: application/json' \
-d '{"email":"[email protected]","nama":"Rina","order_ref":"ORD-8812","nama_event":"Jakarta JS Meetup #12","tanggal":"24 Okt 2026, 18:00","venue":"Hub Baliwoso, Jakarta","token":"chk_9f2k31"}'
{"ok":true,"email_id":"em_..."} — cek inbox pembeli, e-tiket + QR PNG masuk.
Naik ke production: tiga hal yang gak boleh dilewat
- Idempotency-Key. Webhook pembayaran bisa retry. Kirim header
Idempotency-Key: <order_ref>supaya pembeli gak dapat dua tiket — MailAnvil dedupe otomatis. - Webhook event MailAnvil. Daftarkan webhook di dashboard, tangani
bouncedancomplaint. Email tiket yang bounce = harus ada mekanisme kirim ulang ke alamat perbaikan. - Jangan taruh token raw di QR tanpa server validation. Pola aman: QR berisi URL pendek → server verifikasi token → tandai
checked_insekali saja.
Kenapa stack ini cocok buat tim Indonesia
- Edge-ready. Hono jalan native di Cloudflare Workers — kalau suatu saat ticketing kamu butuh API global dengan latensi rendah, kode-nya pindah tanpa rewrite.
- Bayar lokal. QRIS/GoPay, tagihan Rupiah, support Bahasa Indonesia — gak ada drama kartu kredit internasional.
- Diskon biaya internasional. Email API luar umumnya tagih USD dengan markup kurs 12-35% di payment gateway lokal.
Coba sekarang: Try MailAnvil free at mailanvil.com — gratis 3.000 email/bulan, tanpa kartu kredit.