Bun/TypeScript SDK untuk parse, validasi, deteksi provider, dan transformasi QRIS statis menjadi QRIS dinamis.
English | Bahasa Indonesia
- Apa itu QRIS?
- Bagaimana QRIS bekerja?
- QRIS statis vs dinamis
- Bagaimana qris-saurus bekerja?
- Tujuan
- Cara kerja
- Instalasi
- Konfigurasi environment
- Mulai cepat
- Implementasi sederhana
- Contoh lengkap
- Penanganan error
- Gateway & custom provider
- Gateway payments multi-method
- CLI
- Rendering dari library
- API tersedia
- Prioritas input CLI
- Pengembangan
- Dokumentasi
QRIS adalah standar QR payment di Indonesia yang menyatukan banyak metode pembayaran di bawah satu format QR. Secara teknis, payload QRIS adalah string TLV (Tag-Length-Value) berbasis spesifikasi EMVCo. Setiap segmen punya:
Tag: identitas field, misalnya54untuk amountLength: panjang isi fieldValue: isi field itu sendiri
Contoh sederhananya:
540812500.00
Artinya:
54= transaction amount08= panjang value12500.00= nilai amount
Secara umum, alurnya seperti ini:
- Merchant memiliki QRIS payload
- bisa QRIS statis dari acquirer/gateway
- bisa QRIS dinamis yang sudah digenerate gateway
- Customer scan QR dengan app seperti ShopeePay, GoPay, mobile banking, atau aplikasi lain yang mendukung QRIS
- App membaca payload TLV dan menampilkan informasi merchant/transaksi
- Switching dan routing dilakukan oleh ekosistem pembayaran sesuai identifier merchant dan acquirer
- Issuer memproses pembayaran
- Merchant menerima notifikasi/settlement dari gateway atau acquirer
Library ini bekerja di lapisan payload construction/manipulation, bukan di lapisan settlement atau switching network.
sequenceDiagram
participant M as Merchant
participant C as Customer
participant A as Payment App
participant N as Payment Network
participant I as Issuer
M->>C: 1. Tampilkan QR Code
C->>A: 2. Scan QR
A->>A: 3. Baca payload TLV
A->>N: 4. Routing & switching
N->>I: 5. Proses pembayaran
I-->>N: 6. Hasil
N-->>A: 7. Settlement
A-->>M: 8. Notifikasi
Biasanya dipakai untuk merchant display tetap. Nominal tidak tertanam di payload, sehingga customer mengisi nominal sendiri atau nominal ditentukan dari flow di sisi aplikasi pembayaran.
Ciri umumnya:
- point of initiation method
11 - bisa dipakai berkali-kali
- tidak spesifik ke satu transaksi
Dibuat untuk transaksi tertentu. Nominal dan data tambahan bisa disematkan ke payload.
Ciri umumnya:
- point of initiation method
12 - nominal transaksi ada di tag
54 - dapat membawa reference tambahan di tag
62 - lebih cocok untuk checkout, invoice, POS, dan order-based payments
qris-saurus mengikuti alur berikut:
- parse payload QRIS ke struktur TLV
- validate struktur dasar dan CRC
- detectProvider bila identifier provider dikenali
- transform QRIS statis menjadi dinamis
- serialize payload baru dan hitung ulang CRC
flowchart TD
A[String QRIS Statis] --> B["parse() → TLV Nodes"]
B --> C{"validate()\nCRC & tag valid?"}
C -- Invalid --> X[Throw Error]
C -- Valid --> D["Tag 01: 11 → 12\n(static → dynamic)"]
D --> E["Sisipkan Tag 54 (amount)"]
E --> F["Sisipkan Tag 62\n(merchant ref, terminal)"]
F --> G["Recalculate CRC\n(Tag 63)"]
G --> H["serialize()\n→ String QRIS Dinamis"]
Library ini mendukung dua mode: transformasi lokal dari QRIS statis menjadi QRIS dinamis, dan pembayaran gateway untuk membuat QRIS dinamis langsung lewat API provider seperti Midtrans, Xendit, Duitku, dan DOKU.
- Mengubah QRIS statis menjadi QRIS dinamis secara lokal
- Memastikan payload tetap valid dengan CRC yang benar
- Menyediakan fondasi provider-aware untuk ShopeePay, GoPay, Midtrans, Xendit, Duitku, dan DOKU
- Mudah di-import dari project Bun/TypeScript lain
QRIS mengikuti EMVCo QR Code Specification menggunakan encoding TLV (Tag-Length-Value):
[Tag: 2 digit][Length: 2 digit][Value: variable]
Contoh annotasi payload nyata:
00020101021126360014ID.CO.QRIS.WWW0114GENERICSTORE01520458125303360
│ │ │ │
│ │ │ └─ 26: merchant account info (length 36)
│ │ └─────── 01: initiation method (length 2, value "11" = static)
│ └──────────── 00: format indicator (length 2, value "01")
│
5802ID5911QRIS SAURUS6007JAKARTA63041669
│ │ │ │
│ │ │ └─ 63: CRC (length 4)
│ │ └──────────── 60: city (length 7)
│ └────────────────────────── 59: merchant name (length 11)
└──────────────────────────────────────── 58: country code (length 2)
Saat staticToDynamic() dipanggil, library melakukan:
- Parse — payload dipecah menjadi array TLV nodes
- Validasi — cek kehadiran dan validitas CRC (tag
63) - Ubah initiation method — tag
01dari11→12 - Sisipkan amount — tambahkan tag
54dengan nilai amount - Sisipkan additional data — tag
62berisi sub-tag:05= merchant reference (bila ada)07= terminal label (bila ada)
- Sisipkan tip — bila
tipTypediberikan, sisipkan tag root:55= tip indicator (02= fixed,03= percent)56= nominal tip fixed57= persentase tip
- Hitung ulang CRC — CRC16/CCITT atas seluruh payload kecuali 4 char terakhir
- Serialize — nodes dikembalikan ke string payload
| Tag | Nama | Contoh nilai |
|---|---|---|
00 |
Format indicator | 01 |
01 |
Initiation method | 11 statis, 12 dinamis |
26–51 |
Merchant account info | per provider |
52 |
Merchant category code (MCC) | 5812 |
53 |
Currency code | 360 (IDR) |
54 |
Transaction amount | 25000.00 |
55 |
Tip or convenience indicator | 02 fixed, 03 percent |
56 |
Fixed convenience fee | 1000.00 |
57 |
Percentage convenience fee | 2.00 |
58 |
Country code | ID |
59 |
Merchant name | QRIS SAURUS |
60 |
Merchant city | JAKARTA |
62 |
Additional data field | sub-tag 05, 07, 08 |
63 |
CRC | 4 char hex |
Instal dari package manager yang kamu pakai:
npm install qris-sauruspnpm add qris-saurusbun add qris-saurusKalau kamu bekerja langsung di repository ini:
bun installBuat file .env dari template:
cp .env.example .env.env.example:
# Midtrans
MIDTRANS_SERVER_KEY=SB-Mid-server-xxxxxxxxxxxxxxxxxxxx
MIDTRANS_SANDBOX=true
# Xendit
XENDIT_SECRET_KEY=xnd_development_xxxxxxxxxxxxxxxxxxxxxxxx
# Duitku
DUITKU_MERCHANT_CODE=Dxxxxx
DUITKU_MERCHANT_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
DUITKU_SANDBOX=true
# DOKU SNAP QRIS
DOKU_CLIENT_ID=BRN-xxxxxxxx
DOKU_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
DOKU_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----"
DOKU_MERCHANT_ID=xxxxxxxx
DOKU_TERMINAL_ID=A01
# Required only for direct Virtual Account payments.
DOKU_VA_PARTNER_SERVICE_ID=19008
DOKU_SANDBOX=trueGunakan dalam kode:
import { dokuAdapter, duitkuAdapter, midtransAdapter, xenditAdapter } from "qris-saurus";
const midtransConfig = {
serverKey: process.env.MIDTRANS_SERVER_KEY!,
sandbox: process.env.MIDTRANS_SANDBOX === "true",
};
const xenditConfig = {
secretKey: process.env.XENDIT_SECRET_KEY!,
};
const duitkuConfig = {
merchantCode: process.env.DUITKU_MERCHANT_CODE!,
merchantKey: process.env.DUITKU_MERCHANT_KEY!,
sandbox: process.env.DUITKU_SANDBOX === "true",
returnUrl: "https://merchant.example/return",
callbackUrl: "https://merchant.example/webhooks/duitku",
};
const dokuConfig = {
clientId: process.env.DOKU_CLIENT_ID!,
clientSecret: process.env.DOKU_CLIENT_SECRET!,
privateKey: process.env.DOKU_PRIVATE_KEY!,
merchantId: process.env.DOKU_MERCHANT_ID!,
terminalId: process.env.DOKU_TERMINAL_ID!,
sandbox: process.env.DOKU_SANDBOX === "true",
webhookPath: "/webhooks/doku",
};import { makeDynamic, staticToDynamic, validate } from "qris-saurus";
const dynamicQris = staticToDynamic(staticQrisString, {
amount: 12500,
merchantRef: "INV-001",
terminalLabel: "POS-A",
});
const result = makeDynamic(staticQrisString, {
amount: 12500,
});
console.log(dynamicQris);
console.log(result.provider);
console.log(validate(dynamicQris));Ada dua cara menggunakan qris-saurus: sebagai package (SDK) di project TypeScript/Bun kamu, atau langsung via CLI di terminal.
flowchart LR
subgraph SDK["Paket SDK"]
direction TB
S1["bun add qris-saurus"] --> S2["import & transform"]
S2 --> S3["render / gateway"]
end
subgraph Terminal["CLI Terminal"]
direction TB
C1["bun run build"] --> C2["validate → dynamic"]
C2 --> C3["render → PNG"]
end
bun add qris-saurusimport { makeDynamic, renderQrToDataUrl, validate } from "qris-saurus";
const STATIC_QRIS = "00020101021126610016ID.CO.SHOPEE.WWW...";
// 1. Transformasi statis → dinamis
const { qrisString, provider } = makeDynamic(STATIC_QRIS, {
amount: 25000,
merchantRef: "INV-001",
});
// 2. Validasi hasil
validate(qrisString); // { valid: true, errors: [] }
// 3. Render ke gambar
const qrImage = await renderQrToDataUrl(qrisString, { width: 320 });
// <img src={qrImage} /># Build CLI
bun run build
# Validasi payload
bun run dist/cli.js validate "000201010211..."
# Transformasi statis → dinamis
bun run dist/cli.js dynamic "000201010211..." --amount 25000 --merchant-ref INV-001
# Render ke file PNG
bun run dist/cli.js render "000201010211..." --output ./qris.pngContoh standalone tersedia di folder examples:
bun run examples/basic.ts # Core API: parse, validate, detect, transform
bun run examples/render.ts # Render QR ke file dan data URL
bun run examples/gateway.ts # Integrasi gateway (Midtrans, Xendit, Duitku, DOKU)import { parse } from "qris-saurus";
const qris =
"00020101021126610016ID.CO.SHOPEE.WWW01189360091800230223530208230223530303UMI51440014ID.CO.QRIS.WWW0215ID10265163524850303UMI5204581753033605802ID5913Chick n booth6010PEKALONGAN61055118262070703A016304B9ED";
const parsed = parse(qris);
parsed.nodes.find((n) => n.id === "59")?.value; // "Chick n booth"
parsed.nodes.find((n) => n.id === "60")?.value; // "PEKALONGAN"
parsed.nodes.find((n) => n.id === "53")?.value; // "360" (IDR)
parsed.crc; // "B9ED"import { validate } from "qris-saurus";
const result = validate(qris);
// { valid: true, errors: [] }
const tampered = qris.slice(0, -4) + "0000";
const invalid = validate(tampered);
// { valid: false, errors: ["Invalid CRC value"] }import { detectProvider, listProviders } from "qris-saurus";
const provider = detectProvider(qris);
console.log(provider?.info.code); // "shopeepay"
console.log(provider?.info.name); // "ShopeePay"
console.log(provider?.info.supportsApiDynamic); // false
const all = listProviders();
for (const p of all) {
console.log(`${p.info.code}: ${p.info.name}`);
}import { staticToDynamic } from "qris-saurus";
const dynamic = staticToDynamic(qris, {
amount: 75000,
merchantRef: "ORD-2024-001",
terminalLabel: "POS-01",
});import { staticToDynamic } from "qris-saurus";
// Tip tetap (Rp 2.000)
const withFixedTip = staticToDynamic(qris, {
amount: 50000,
tipType: "fixed",
tipValue: 2000,
});
// Tip persen (5%)
const withPercentTip = staticToDynamic(qris, {
amount: 50000,
tipType: "percent",
tipValue: 5,
});import { makeDynamic } from "qris-saurus";
const result = makeDynamic(qris, {
amount: 25000,
merchantRef: "INV-001",
});
console.log(result.source); // "local" (transformasi lokal)
console.log(result.provider); // "shopeepay"
console.log(result.amount); // 25000
console.log(result.qrisString); // payload dinamis baruimport { computeCrc, verifyCrc, parse, serialize } from "qris-saurus";
// Hitung CRC dari payload (termasuk "6304" di akhir)
const payload = qris.slice(0, -4); // buang 4 char CRC, simpan "6304"
const crc = computeCrc(payload);
console.log(crc); // "1669"
// Verifikasi CRC pada string QRIS
const isValid = verifyCrc(qris);
console.log(isValid); // true
// Parse lalu serialize — payload harus sama
const parsed = parse(qris);
const reserialized = serialize(parsed);
console.log(qris === reserialized); // trueimport { renderQrToDataUrl, renderQrToFile, makeDynamic } from "qris-saurus";
const { qrisString } = makeDynamic(qris, { amount: 50000 });
// Base64 data URL — langsung bisa dipakai di HTML
const dataUrl = await renderQrToDataUrl(qrisString, { width: 320 });
// data:image/png;base64,...
// Simpan ke file
await renderQrToFile(qrisString, "./qris.png", { width: 400, margin: 3 });import { validate, parse, makeDynamic, staticToDynamic } from "qris-saurus";
// validate() tidak pernah throw — kembalikan { valid, errors }
const check = validate("bukan-qris");
// { valid: false, errors: ["QRIS payload too short"] }
// parse() throw bila CRC hilang/salah
try {
parse("invalid");
} catch (err) {
console.error(err.message); // "QRIS payload is missing CRC tag"
}
// staticToDynamic() throw bila sudah dinamis
const dynamic = staticToDynamic(qris, { amount: 10000 });
try {
staticToDynamic(dynamic, { amount: 10000 });
} catch (err) {
console.error(err.message); // "QRIS payload is already dynamic"
}
// staticToDynamic() throw bila amount negatif
try {
staticToDynamic(qris, { amount: -500 });
} catch (err) {
console.error(err.message); // "Amount must be a positive number"
}validate() bersifat sinkron dan tidak pernah throw — hasil dikembalikan via ValidationResult. Namun parse(), staticToDynamic(), dan makeDynamic() dapat throw bila input tidak valid (CRC hilang/salah, amount tidak valid, dsb). Gateway adapters menggunakan async/await dan dapat throw bila request gagal.
import { validate, makeDynamic, parse, midtransAdapter } from "qris-saurus";
// validate() — tidak throw, cek .valid
const check = validate(qrisString);
if (!check.valid) {
console.error("Invalid QRIS:", check.errors);
// errors: ["Invalid CRC value", "Missing required tag 00", ...]
}
// parse() / makeDynamic() / staticToDynamic() — dapat throw, gunakan try/catch
try {
const dynamic = makeDynamic(qrisString, { amount: 25000 });
console.log(dynamic.source); // "local"
} catch (err) {
// Input tidak valid, CRC salah, atau amount tidak valid
console.error("Transform error:", err);
}
// Gateway adapter — dapat throw, tangkap dengan try/catch
try {
const result = await midtransAdapter.createDynamicQr(
{ orderId: "INV-001", amount: 25000 },
midtransConfig,
{ overrideNotificationUrl: "https://merchant.example/webhooks/midtrans" },
);
console.log(result.qrisString); // payload QRIS mentah
console.log(result.qrImageUrl); // URL PNG QR bila Midtrans mengembalikannya
} catch (err) {
// Network error, auth error, atau response tidak valid
console.error("Gateway error:", err);
}
// Cek status pembayaran
try {
const status = await midtransAdapter.checkPaymentStatus("INV-001", midtransConfig);
// status.status: "pending" | "paid" | "expired" | "failed" | "cancelled"
if (status.status === "paid") {
console.log("Lunas pada:", status.paidAt);
}
} catch (err) {
console.error("Status check error:", err);
}SDK ini menyediakan gateway singleton untuk mempermudah integrasi berbagai provider (Midtrans, Xendit, Duitku, DOKU) melalui satu interface yang terpusat. Gateway mendelegasikan panggilan ke adapter tanpa perlu pengecekan provider secara manual di kodemu.
| Provider | provider |
Dynamic QRIS | Status polling | Webhook verify | Config wajib | Catatan |
|---|---|---|---|---|---|---|
| Midtrans | midtrans |
Ya, via /v2/charge QRIS |
Ya | Signature SHA512 dari payload + serverKey |
serverKey, sandbox? |
Mendukung cancel(), expire(), dan refund() lewat gateway helper. |
| Xendit | xendit |
Ya, QR Code API + VA direct + e-wallet direct + hosted invoice | Ya, QRIS polling + webhook callback token | x-callback-token bila callbackToken diset |
secretKey, callbackToken? |
VA direct memakai BCA/BNI/BRI/Mandiri/Permata; e-wallet direct memakai ID_OVO, ID_DANA, ID_LINKAJA, ID_SHOPEEPAY. CIMB VA tetap guarded. |
| Duitku | duitku |
Ya, Direct API /v2/inquiry QRIS + VA + e-wallet |
Ya, /transactionStatus |
HMAC-SHA256 dari merchantCode + amount + merchantOrderId |
merchantCode, merchantKey, returnUrl, callbackUrl, sandbox? |
QRIS default SP; VA memakai BC, I1, BR, M2, BT, B1; e-wallet memakai OV, SA, DA, LF. |
| DOKU | doku |
Ya, SNAP QRIS MPM Generate + VA direct + e-wallet DANA/ShopeePay | Ya, QRIS MPM Query + helper VA/e-wallet status | HMAC-SHA512 SNAP dari method, path, token, body hash, timestamp | clientId, clientSecret, privateKey, merchantId, terminalId, virtualAccountPartnerServiceId?, webhookPath?, sandbox? |
Access token B2B ditandatangani RSA-SHA256 dan dicache otomatis; VA direct butuh BIN merchant; OVO tetap guarded karena perlu binding/tokenization. |
Semua provider memakai method gateway yang sama:
import { gateway } from "qris-saurus";
// 1. Configure provider aktif sekali saat boot aplikasi
gateway.configure({
provider: "doku",
clientId: process.env.DOKU_CLIENT_ID!,
clientSecret: process.env.DOKU_CLIENT_SECRET!,
privateKey: process.env.DOKU_PRIVATE_KEY!,
merchantId: process.env.DOKU_MERCHANT_ID!,
terminalId: process.env.DOKU_TERMINAL_ID!,
sandbox: true,
webhookPath: "/webhooks/doku",
});
// 2. Buat QRIS dinamis untuk satu order
const chargeResult = await gateway.charge("INV-001", 50000, {
description: "Pembayaran INV-001",
customerEmail: "customer@example.com",
});
// 3. Cek status atau polling sampai terminal state
const statusResult = await gateway.status(chargeResult.gatewayOrderId);
const finalStatus = await gateway.pollPaymentStatus(chargeResult.gatewayOrderId, {
intervalMs: 2000,
timeoutMs: 60000,
});
// 4. Verify webhook/callback provider
const verifyResult = gateway.verify(webhookPayload, headers); // sync
// DOKU: pass rawBody when your framework exposes it.
const dokuVerifyResult = gateway.verify(webhookPayload, headers, { rawBody });Status hasil normalisasi selalu memakai union berikut: pending, paid, refunded, expired, failed, atau cancelled. Simpan gatewayOrderId dari hasil charge() karena beberapa provider mengembalikan ID transaksi gateway yang berbeda dari order ID merchant. Adapter gateway hanya untuk server-side; jangan kirim secret/private key ke browser dan jangan log config, access token, atau raw error provider.
gateway.configure({
provider: "duitku",
merchantCode: process.env.DUITKU_MERCHANT_CODE!,
merchantKey: process.env.DUITKU_MERCHANT_KEY!, // API key Duitku
sandbox: true,
returnUrl: "https://merchant.example/payment/return",
callbackUrl: "https://merchant.example/webhooks/duitku",
paymentMethod: "SP", // default QRIS, bisa disesuaikan dengan channel Duitku yang aktif
});Duitku adapter mengirim paymentAmount, paymentMethod, merchantOrderId, productDetails, callbackUrl, returnUrl, dan signature HMAC-SHA256 ke endpoint inquiry. createPayment() mendukung QRIS, VA BCA/BNI/BRI/Mandiri/Permata/CIMB, serta e-wallet OVO/ShopeePay/DANA/LinkAja dari kode metode resmi Duitku. Callback diparse dari merchantOrderId, amount, resultCode, paymentCode, reference, vaNumber, dan signature. Secara default, parseWebhook() melempar error bila signature tidak valid; gunakan { throwOnInvalid: false } hanya jika ingin menerima hasil aman valid: false tanpa field pembayaran ternormalisasi.
gateway.configure({
provider: "doku",
clientId: process.env.DOKU_CLIENT_ID!,
clientSecret: process.env.DOKU_CLIENT_SECRET!,
privateKey: process.env.DOKU_PRIVATE_KEY!,
merchantId: process.env.DOKU_MERCHANT_ID!,
terminalId: process.env.DOKU_TERMINAL_ID!,
// Wajib hanya saat memakai direct Virtual Account.
virtualAccountPartnerServiceId: process.env.DOKU_VA_PARTNER_SERVICE_ID,
sandbox: true,
channelId: "H2H",
serviceCode: "47",
webhookPath: "/webhooks/doku",
additionalInfo: {
postalCode: "12190",
feeType: "1",
},
});DOKU adapter menjalankan flow SNAP: ambil access token B2B dengan RSA-SHA256, generate QRIS MPM dengan Bearer token, lalu query status dengan signature HMAC-SHA512. Untuk webhook DOKU, pastikan webhookPath sama persis dengan path endpoint publik yang menerima callback, karena path tersebut menjadi bagian dari string-to-sign. Verifikasi webhook menolak timestamp di luar 5 menit secara default (webhookMaxTimestampSkewMs atau option maxTimestampSkewMs) dan sebaiknya diberi rawBody supaya hash signature mengikuti body asli dari DOKU. Seperti Duitku, parseWebhook() melempar error bila signature/timestamp tidak valid; { throwOnInvalid: false } mengembalikan hasil aman tanpa status/order palsu.
Arsitektur gateway sangat scalable. Kamu bisa dengan mudah membawa provider-mu sendiri (misal Biller lain atau gateway internal) tanpa perlu memodifikasi core library. Cukup implementasikan interface GatewayAdapter yang wajib menyertakan 4 operasi inti: createDynamicQr, checkPaymentStatus, parseWebhook, dan pollPaymentStatus.
Ada dua pendekatan untuk memasang custom adapter:
1. gateway.useAdapter() (Direct Injection)
Gunakan cara ini untuk melempar instance adapter langsung ke singleton. Sangat cocok jika kamu membuat instance di module sendiri:
import { gateway, type GatewayAdapter } from "qris-saurus";
class FinpayAdapter implements GatewayAdapter {
// ...implementasi 4 operasi inti
}
// Pasang adapter langsung
gateway.useAdapter("finpay", new FinpayAdapter(), { apiKey: "secret" });
// Langsung bisa dipakai
await gateway.charge("INV-FIN", 10000); 2. Gateway.registerProvider() (Factory Registration)
Gunakan cara ini jika kamu membuat library atau helper yang mendaftarkan provider secara global, sehingga nantinya aplikasi kamu hanya perlu memanggil gateway.configure():
import { Gateway, gateway } from "qris-saurus";
// Daftarkan ke factory bawaan SDK
Gateway.registerProvider("finpay", () => new FinpayAdapter());
// Sekarang bisa dipakai selayaknya provider bawaan
gateway.configure({
provider: "finpay",
apiKey: "secret"
} as any); // custom provider belum ada di tipe GatewayConfigSelain QRIS gateway API lama (charge() / createDynamicQr()), qris-saurus sekarang punya fondasi multi-method:
gateway.capabilities()untuk membaca method yang didukung provider.gateway.createPayment()untuk direct API/custom UI (qris,virtual_account,ewallet).- Helper typed:
createQrisPayment(),createVirtualAccount(),createEwallet(). gateway.createCheckout()/gateway.createHostedCheckout()untuk hosted checkout/payment page provider.- Webhook tetap menjadi source of truth; redirect dan polling hanya UX/fallback.
gateway.configure({
provider: "midtrans",
serverKey: process.env.MIDTRANS_SERVER_KEY!,
sandbox: true,
});
const va = await gateway.createVirtualAccount({
orderId: "INV-VA-001",
amount: 50_000,
bank: "bca",
});
const checkout = await gateway.createCheckout({
orderId: "INV-CO-001",
amount: 75_000,
enabledMethods: ["qris", "virtual_account", "ewallet"],
notificationUrl: "https://merchant.example/webhooks/midtrans",
});Lihat docs/sdk/payments.md untuk detail direct payment vs hosted checkout, capability provider, dan best practice webhook.
Setelah build, CLI tersedia sebagai qris-saurus.
bun run buildbun run dist/cli.js --helpqris-saurus CLI
Usage:
qris-saurus validate [<qris>] [--input-file <file>]
qris-saurus parse [<qris>] [--input-file <file>]
qris-saurus detect [<qris>] [--input-file <file>]
qris-saurus dynamic [<qris>] --amount <number> [--merchant-ref <text>] [--terminal-label <text>] [--input-file <file>]
qris-saurus render [<qris>] --output <file.png> [--width <number>] [--margin <number>] [--input-file <file>]
Input priority:
1. positional <qris>
2. --input-file <file>
3. stdin pipe
Memeriksa CRC dan tag wajib pada payload.
bun run dist/cli.js validate "<QRIS_PAYLOAD>"
# atau
bun run dist/cli.js validate --input-file ./payload.txtOutput:
{
"valid": true,
"errors": []
}Jika ada masalah:
{
"valid": false,
"errors": [
"Invalid CRC value"
]
}Mem-parse payload menjadi struktur TLV.
bun run dist/cli.js parse "<QRIS_PAYLOAD>"
# atau
cat ./payload.txt | bun run dist/cli.js parseOutput:
{
"raw": "00020101021126360014ID.CO.QRIS.WWW0114GENERICSTORE01520458125303605802ID5911QRIS SAURUS6007JAKARTA63041669",
"nodes": [
{ "id": "00", "length": 2, "value": "01" },
{ "id": "01", "length": 2, "value": "11" },
{
"id": "26",
"length": 36,
"value": "0014ID.CO.QRIS.WWW0114GENERICSTORE01",
"children": [
{ "id": "00", "length": 14, "value": "ID.CO.QRIS.WWW" },
{ "id": "01", "length": 14, "value": "GENERICSTORE01" }
]
},
{ "id": "52", "length": 4, "value": "5812" },
{ "id": "53", "length": 3, "value": "360" },
{ "id": "58", "length": 2, "value": "ID" },
{ "id": "59", "length": 11, "value": "QRIS SAURUS" },
{ "id": "60", "length": 7, "value": "JAKARTA" }
],
"crc": "1669"
}Mendeteksi provider dari merchant account identifier.
bun run dist/cli.js detect "<QRIS_PAYLOAD>"
# atau
bun run dist/cli.js detect --input-file ./payload.txtJika provider dikenali (contoh ShopeePay):
{
"code": "shopeepay",
"name": "ShopeePay",
"aliases": ["shopeepay", "shopee pay"],
"merchantInfoTagIds": ["26", "27", "28", "..."],
"identifiers": ["shopee"],
"supportsApiDynamic": false,
"notes": "Standalone public dynamic QRIS API evidence is limited; use local QRIS transformation by default."
}Jika tidak dikenali:
nullMengubah QRIS statis menjadi dinamis dengan nominal transaksi.
bun run dist/cli.js dynamic "<QRIS_PAYLOAD>" --amount 25000 --merchant-ref INV-001 --terminal-label POS-A
# atau
cat ./payload.txt | bun run dist/cli.js dynamic --amount 25000 --merchant-ref INV-001Output adalah string payload QRIS dinamis baru, siap dirender:
00020101021226360014ID.CO.QRIS.WWW0114GENERICSTORE01520458125303360540825000.005802ID5911QRIS SAURUS6007JAKARTA62200507INV-0010705POS-A6304391F
Perbedaan dari payload asli:
- tag
01berubah dari11→12(static → dynamic) - tag
54ditambahkan dengan nominal25000.00 - tag
62ditambahkan denganmerchantRefdanterminalLabel - tag
63(CRC) dihitung ulang
Membuat file PNG dari payload QRIS.
bun run dist/cli.js render "<QRIS_PAYLOAD>" --output ./qris.png --width 320 --margin 2
# atau
cat ./payload.txt | bun run dist/cli.js render --output ./qris.pngOutput adalah path file PNG yang berhasil dibuat:
./qris.png
import { renderQrToFile } from "qris-saurus";
await renderQrToFile(qrisPayload, "./qris.png", { width: 320, margin: 2 });
// → file ./qris.png tersimpanimport { renderQrToDataUrl } from "qris-saurus";
const dataUrl = await renderQrToDataUrl(qrisPayload, { width: 320 });
console.log(dataUrl);
// data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAYAAADNkKWqAAAAAklEQVR4Ae...Hasil dataUrl langsung bisa dipakai di HTML:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAYAAADNkKWqAAAAAklEQVR4Ae..." />Atau dikirim sebagai JSON response:
return Response.json({ qrImage: dataUrl });Helper ini berguna kalau kamu ingin:
- menampilkan preview QR di web/app internal
- menyimpan QR image ke file
- mengirim hasil render ke pipeline lain setelah payload selesai dibentuk
Inti:
parse(qrisString)— string → TLV nodesserialize(qrisData)— TLV nodes → stringvalidate(qrisString)— cek CRC + tag wajibcomputeCrc(input)/verifyCrc(qrisString)
Transformasi:
staticToDynamic(qrisString, options)— local transform, return stringmakeDynamic(qrisString, options)— local transform + provider detection, returnDynamicResult
Provider:
detectProvider(qrisString)— returnProviderAdapter | nulllistProviders()— return semua provider terdaftar
Gateway adapters:
midtransAdapter.createDynamicQr(options, config, notificationOptions?)— buat QR via Midtrans API, dengan opsi override/append webhook per transaksimidtransAdapter.checkPaymentStatus(orderId, config)— cek status pembayaranmidtransAdapter.verifyWebhook(payload, config)/parseWebhook(payload, config)/getWebhookStatus(payload)— validasi dan normalisasi webhook MidtransxenditAdapter.createDynamicQr(options, config)— buat QR via Xendit APIxenditAdapter.checkPaymentStatus(gatewayOrderId, config)— cek status pembayaranxenditAdapter.parseWebhook(payload, config, headers)— validasi callback token dan normalisasi webhook XenditduitkuAdapter.createDynamicQr(options, config)— buat QR via Duitku Direct APIduitkuAdapter.checkPaymentStatus(orderId, config)— cek status pembayaran DuitkuduitkuAdapter.parseWebhook(payload, config)— validasi HMAC-SHA256 dan normalisasi callback DuitkudokuAdapter.createDynamicQr(options, config)— buat QR via DOKU SNAP QRIS MPM GeneratedokuAdapter.checkPaymentStatus(orderId, config)— cek status pembayaran DOKU SNAP QRIS MPM QuerydokuAdapter.parseWebhook(payload, config, headers)— validasi HMAC-SHA512 SNAP dan normalisasi webhook DOKU
Render:
renderQrToDataUrl(qrisString, options?)— return Base64 PNG data URLrenderQrToFile(qrisString, outputPath, options?)— simpan ke file PNG
CLI menerima input dengan urutan prioritas:
- argumen langsung
--input-file <file>- stdin / pipe
# 1 — argumen langsung
bun run dist/cli.js validate "00020101021126..."
# 2 — dari file
bun run dist/cli.js dynamic --input-file payload.txt --amount 25000
# 3 — dari stdin / pipe
cat payload.txt | bun run dist/cli.js render --output qris.pngbun install
bun test
bun run typecheck
bun run buildLihat folder docs:
docs/sdk/index.md— overview SDK & quick startdocs/sdk/api.md— full API referencedocs/sdk/workflow.md— panduan alur end-to-enddocs/sdk/gateway.md— gateway adapters & cek status pembayarandocs/architecture.md— desain internal librarydocs/qris-dynamic.md— teknis QRIS dinamis & TLVdocs/providers.md— catatan per providerdocs/cli.md— panduan CLI lengkap
