← Back to all posts
2026-10-06 · MailAnvil Team

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:

  1. 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.
  2. Attachment itu first-class. Satu field attachments di payload JSON. Gak perlu MIME multipart manual.
  3. 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" }
  ]
}

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:

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

  1. Idempotency-Key. Webhook pembayaran bisa retry. Kirim header Idempotency-Key: <order_ref> supaya pembeli gak dapat dua tiket — MailAnvil dedupe otomatis.
  2. Webhook event MailAnvil. Daftarkan webhook di dashboard, tangani bounce dan complaint. Email tiket yang bounce = harus ada mekanisme kirim ulang ke alamat perbaikan.
  3. Jangan taruh token raw di QR tanpa server validation. Pola aman: QR berisi URL pendek → server verifikasi token → tandai checked_in sekali saja.

Kenapa stack ini cocok buat tim Indonesia

Coba sekarang: Try MailAnvil free at mailanvil.com — gratis 3.000 email/bulan, tanpa kartu kredit.