Logoblog.tiara
  • Home
  • Posts

© 2025. Tiara S. Dewi. All rights reserved.

Mini Project: QRIS Payments for GB Toolkit

Mini Project: QRIS Payments for GB Toolkit

November 13th, 2025

By trsrdw

🧠 Why I Built This Mini Project

This wasn’t originally planned as a full payment system — it started as a small internal task to help my coworker who was preparing the GB Toolkit.

The goals were simple:

  • Make it easy for partners, educators, and organizations to donate and access the toolkit
  • Ensure the payment flow works smoothly
  • Automatically deliver toolkit files digitally
  • Reduce manual follow-ups or email chains
  • Keep everything low-maintenance for the team

But as I started building the pieces — payment, QRIS, download automation — the mini project naturally grew into a more complete, reliable system.

What began as a favor to support a coworker eventually turned into a solid micro-product powering GB Toolkit distribution.

🧩 Architecture Overview

Here’s what the system does:

1. User chooses package/amount
Screenshot 2025-11-13 at 20-11-32 Donasi Aldo - Greeneration Foundation.png
Screenshot 2025-11-13 at 20-11-58 Donasi Aldo - Greeneration Foundation.png
  • Enters email
  • Accepts Terms & Conditions
  • Clicks “Bayar Sekarang”
2. Next.js API route creates a Midtrans charge
  • Returns qr_string (for QRIS)
  • Saves order to DB
  • Sends a payment instruction email
3. User scans QR using GoPay/ShopeePay/DANA/OVO
Screenshot 2025-11-13 at 20-12-28 Donasi Aldo - Greeneration Foundation.png
  • Midtrans notifies our backend via webhook
4. Success page polls for status
Screenshot 2025-11-13 at 20-12-36 Donasi Aldo - Greeneration Foundation.png
  • When status = “paid”, auto-generates a single-use download token
5. User receives a second email
  • Including secure download link
  • Link expires after X minutes
  • Can be used only once

Simple, robust, and works well on mobile.

⚙️ 1. Creating the Midtrans Charge (QRIS)

Here’s the simplified server endpoint:

// /api/create-charge

import { NextRequest } from 'next/server';
import { createOrder, updateOrderFields } from '@/lib/db';
import { sendPaymentEmail } from '@/lib/email/payment';

export async function POST(req) {
const { amount, itemName, buyer_email } = await req.json();
const order_id = `AIG-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

await createOrder({
order_id,
amount,
item_name: itemName,
buyer_email,
status: 'pending',
});

const payload = {
payment_type: 'qris',
transaction_details: { order_id, gross_amount: amount },
customer_details: { email: buyer_email },
};

const serverKey = process.env.MIDTRANS_SERVER_KEY;
const auth = Buffer.from(`${serverKey}:`).toString('base64');

const r = await fetch('https://api.sandbox.midtrans.com/v2/charge', {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});

const data = await r.json();
await updateOrderFields(order_id, { midtrans_response: data });

if (buyer_email) {
const midtransUrl = data.actions?.[0]?.url || data.payment_url;
await sendPaymentEmail({
to: buyer_email,
orderId: order_id,
midtransUrl,
itemName,
amount,
});
}

return new Response(JSON.stringify({ ok: true, midtrans: data, order_id }));
}

This returns the QR string, which is rendered inside the modal.

🔄 2. Midtrans Webhook → Updating Order Status

Midtrans calls your server when payment is completed.

// /api/midtrans-notification

export async function POST(req) {
const body = await req.json();
const order_id = body.order_id;
const status = body.transaction_status;

if (status === 'settlement' || status === 'capture') {
await updateOrderFields(order_id, {
status: 'paid',
paid_at: new Date(),
midtrans_notification: body,
});
}

return new Response('OK');
}

🔁 3. Success Page Polling + Auto Download Token

The success page polls /api/order-status every few seconds:

useEffect(() => {
if (status === 'paid' && !downloadLink) {
createToken(); // auto-generate download link once
}
}, [status, downloadLink]);

Then the token endpoint:

// /api/create-download-token
const ttl = Number(process.env.DOWNLOAD_TOKEN_TTL_MINUTES || 60);
const token = crypto.randomBytes(32).toString('hex');

await insertDownloadToken({
token,
order_id,
email,
ttl_minutes: ttl,
});

return { downloadLink: `${base}/api/download?token=${token}` };

📩 4. Automated Emails with Nodemailer

Payment Instruction Email

Sent right after user clicks Pay.

Screenshot 2025-11-13 201309.png

Formatted HTML email:

  • Order ID
  • Amount
  • Payment button
  • Fallback link
  • “If you didn’t request this, ignore.”
Download Link Email

Sent only after payment = paid.

Screenshot 2025-11-13 203912.png

Includes:

  • Item name
  • Amount
  • Download button
  • Expiry timestamp
  • Single-use security note

🔐 Security Considerations

I used:

  • Single-use tokens
  • Expiry timestamps
  • QR string never stored in email
  • Email HTML escaping
  • Webhook validation

🎯 Conclusion

This project ended up as a complete, smooth, reliable donation system:

  • Donors get a clean payment flow and instant QR
  • Automated emails and simple data in the database
  • Users get fast download access without needing accounts
  • Fully customizable stack for future products

If you want to integrate Midtrans + QRIS + Next.js for digital products, this approach is flexible, scalable, and production-ready.

Categories:

ProjectsTech

Recent Posts

Content Request Dashboard with Next.js

Content Request Dashboard with Next.js

Nowadays, maintaining dynamic website content across multiple pages usually requ …

Read More
Bibi Turns One 🎂

Bibi Turns One 🎂

When I moved into my new place in Bandung, I didn’t expect to gain a companion — …

Read More