Dokumentasi Pengembang

QRISGate REST API Reference

Integrasikan pembayaran QRIS dinamis ber-kode unik dan webhook mutasi otomatis ke platform website atau aplikasi Anda dalam hitungan menit.

POST

/api/v1/transactions

Membuat sesi pembayaran baru, mengalokasikan kode unik bebas bentrok, dan menghasilkan string QRIS Dinamis ber-Tag 54.

GoPay hanya mendukung QRIS statis: nominal harus diketik manual oleh pembeli.

Aplikasi GoPay Merchant hanya mengirim notifikasi ke penjual untuk pembayaran QRIS statis. Jika QR memuat nominal (QRIS dinamis), GoPay menganggapnya sebagai transaksi yang belum diaktifkan untuk akun tersebut dan notifikasi tidak pernah muncul — pesanan akan tertahan di status Menunggu. Karena itu, untuk merchant GoPay QRISGate selalu menampilkan QRIS statis dan pembeli mengetik nominal sendiri. Nominal tersebut sudah termasuk kode unik, jadi pastikan pembeli mengetiknya persis. DANA dan kanal lain tetap memakai QRIS dinamis bernominal terkunci.

curl -X POST https://qrisgate.web.id/api/v1/transactions \
  -H "Authorization: Bearer qg_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_order_id": "INV-101",
    "amount": 50000,
    "customer_name": "Budi Santoso",
    "return_url": "https://merchant.com/thank-you"
  }'

Contoh Respon (201 Created)

{
  "success": true,
  "data": {
    "transaction_id": "trx_89fe32c81e2b",
    "external_order_id": "INV-101",
    "merchant_name": "Toko Buku Nusantara",
    "base_amount": 50000,
    "unique_code": 7,
    "total_amount": 50007,
    "qr_string": "000201010212...5405500075802ID...6304XXXX",
    "qr_image_base64": "data:image/png;base64,iVBORw0KGgo...",
    "checkout_url": "https://qrisgate.web.id/pay/trx_89fe32c81e2b",
    "expires_at": "2026-10-22T04:30:00.000Z"
  }
}
INFO

Biaya Platform & Kode Unik

Berapa yang dibayar pembeli, berapa yang Anda terima, dan apa yang dipotong QRISGate.

Biaya layananFlat Rp 100 per transaksi
DitanggungMerchant — dipotong dari saldo kredit Anda, bukan dari dana yang masuk ke rekening
Dibayar pembelibase_amount + kode unik Rp 1–10, langsung ke rekening Anda
Nominal minimumRp 5.000 per transaksi
LanggananOpsional, dibeli dari saldo kredit di dashboard: Rp 10.000 / 1 bulan, Rp 20.000 / 2 bulan, Rp 30.000 / 3 bulan, Rp 40.000 / 5 bulan. Selama periode berjalan biaya per transaksi Rp 0 dan order dicatat sebagai WAIVED
Biaya lainTidak ada komisi persentase, MDR tambahan Rp 0 dari QRISGate. Bulanan hanya ada jika Anda berlangganan
Tidak ditagihTransaksi gagal, kedaluwarsa, atau dibatalkan — reservasi biaya dikembalikan ke saldo

Kode unik dialokasikan acak antara Rp 1 dan Rp 10 dan tidak berhubungan dengan besarnya biaya. Ia ada hanya supaya dua pesanan bernominal sama yang berjalan bersamaan menghasilkan total bayar yang berbeda — kalau tidak, pembayaran yang masuk tidak dapat diatribusikan ke pesanan yang benar. Saat seluruh band Rp 1–10 sedang terisi, alokasi melebar sampai Rp 99 agar pesanan tidak pernah ditolak karena bentrok. Biaya itu sendiri angka tetap: Rp 100 per transaksi, dipotong dari saldo kredit merchant saat transaksi dibuat dan dikembalikan jika transaksi tidak pernah lunas. Jangan hardcoded nilai unique_code di sistem Anda — selalu pakai nominal yang kami kembalikan.

Webhook Verifikasi Tanda Tangan (HMAC-SHA256)

Setiap kali notifikasi mutasi masuk dan cocok dengan transaksi PENDING, QRISGate segera menembakkan HTTP POST ke webhook_url merchant.

Spesifikasi Header Webhook

X-QRISGate-Eventpayment.paid | payment.expired | payment.cancelled
X-QRISGate-TimestampUnix timestamp dalam satuan detik saat payload dibuat
X-QRISGate-SignatureHex digest dari HMAC_SHA256(timestamp + "." + rawBody, webhook_secret)
X-QRISGate-Delivery-IdSama untuk setiap retry event ini — pakai sebagai kunci idempotensi
X-QRISGate-AttemptPercobaan pengiriman ke-berapa (1-based)
Contoh Payload Webhook:
{
  "event": "payment.paid",
  "transaction_id": "trx_8f2c4b1a05c67e1f7294b3d0",
  "external_order_id": "INV-1042",
  "base_amount": 150000,
  "unique_code": 247,
  "total_amount": 150247,
  "customer_name": "Budi",
  "paid_at": "2026-10-24T09:15:22.000Z",
  "created_at": "2026-10-24T09:12:40.000Z"
}

Payload bersifat flat — tidak ada wrapper data. Baca external_order_id langsung di level atas.

Verifikasi signature (Node.js):
const crypto = require('crypto');
const express = require('express');
const app = express();

// express.raw() gives you the EXACT bytes QRISGate signed. Any JSON body
// parser (express.json(), body-parser) destroys that serialisation, and the
// signature will never match.
app.post('/api/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-qrisgate-signature'];
  const timestamp = req.headers['x-qrisgate-timestamp'];
  const secret    = process.env.QRISGATE_WEBHOOK_SECRET;

  // 1. Replay window — reject anything signed more than 5 minutes ago.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(401).send('Stale timestamp');
  }

  // 2. HMAC over the RAW body — never over a re-serialised object.
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + req.body)
    .digest('hex');

  // 3. Constant-time comparison, after a length check.
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(String(signature).replace(/^sha256=/, ''), 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('Invalid signature');
  }

  // 4. The payload is FLAT. There is no `data` wrapper.
  const { event, external_order_id, total_amount } = JSON.parse(req.body.toString('utf8'));

  if (event === 'payment.paid') {
    // ... mark order external_order_id as paid for total_amount
  }

  res.json({ received: true });
});

Tiga hal yang paling sering salah: mem-verifikasi body hasil JSON.stringify(req.body) (harus byte mentah), membandingkan dengan !== (harus constant-time), dan tidak memeriksa timestamp (harus ditolak bila lebih tua dari 5 menit).

Listener Mutasi Android (Endpoint Ingestion)

Aplikasi notification listener di handphone Android toko mengirimkan setiap push notifikasi masuk ke server QRISGate:

POST/api/v1/mutations/ingest
// Header:
// X-Device-Token: dt_YOUR_DEVICE_TOKEN

// Body:
{
  "source_app": "com.gojek.gopaymerchant",
  "title": "Pembayaran diterima",
  "text": "Kamu menerima pembayaran QRIS sebesar Rp 125.008 dari pelanggan."
}