TUTORIAL DEVELOPER

Cara Integrasi API QRIS di Website (Contoh Kode Node.js dan PHP)

Tim TemanQRISDiterbitkan 17 Oktober 2026 · 5 menit baca
ringkas
Jawaban singkat

Integrasi API QRIS di website pada dasarnya tiga langkah: backend kamu memanggil POST /api/qris/payment-link dengan API key di header X-API-Key, mengarahkan pembeli ke link pembayaran yang dikembalikan, lalu menerima webhook bertanda tangan HMAC saat status berubah. Di bawah ini contoh kode lengkapnya dalam Node.js dan PHP, berdasarkan API TemanQRIS v1.3.0.

Gambaran alur integrasi API QRIS di website

Sebelum menulis kode, pahami dulu siapa melakukan apa. Di mode reguler TemanQRIS, QRIS yang dipakai tetap QRIS statis milik kamu sendiri (dari bank atau e-wallet). TemanQRIS mengubahnya jadi QRIS dinamis dengan nominal per pesanan, lalu membungkusnya dalam link pembayaran. Uang pembeli masuk langsung ke rekening atau akun yang terhubung dengan QRIS kamu, bukan ke saldo TemanQRIS.

Alurnya kira-kira begini:

  1. Pembeli checkout di website kamu.
  2. Backend kamu memanggil POST /api/qris/payment-link dengan nominal, order_id, webhook_url, dan callback_url.
  3. TemanQRIS mengembalikan payment_link.url (misalnya /p/AB12CD34EF56) dan gambar QR dalam Base64.
  4. Pembeli scan QR, bayar, lalu klik "Sudah Bayar". Status order berubah jadi awaiting_confirmation dan webhook dikirim ke server kamu.
  5. Setelah dana terlihat masuk di aplikasi bank atau e-wallet, kamu menandai order lunas lewat dashboard atau POST /api/qris/orders/:orderId/verify. Status jadi paid dan webhook payment.confirmed terkirim.

Langkah terakhir itu penting dan sering disalahpahami. Di mode reguler, TemanQRIS tidak bisa melihat mutasi rekening kamu, jadi konfirmasi akhir tetap ada di tangan merchant. Kalau kamu butuh konfirmasi yang benar-benar otomatis, ada mode Automatic Payment Gateway yang kami bahas di bagian akhir.

Persiapan sebelum menulis kode

  • Akun dan QRIS statis. Daftar di TemanQRIS, lalu upload QRIS statis sekali saja lewat dashboard atau POST /api/qris/upload. Cek dengan GET /api/qris/my-qris kalau ragu sudah upload atau belum.
  • API key. Ambil dari Dashboard Settings. Simpan di environment variable, jangan di JavaScript frontend atau di repository. Server hanya membaca API key dari header X-API-Key, bukan dari query string.
  • Webhook secret. Buat di dashboard. Ini sering terlewat: kalau akun belum punya webhook secret, webhook tidak dikirim sama sekali walaupun webhook_url sudah diisi.
  • Kuota request. Paket Free dapat 50 request per hari, Premium (Rp 9.000/bulan) dapat 1.000 request per hari. Yang dihitung adalah endpoint yang membuat atau mengubah data, seperti generate, payment link, verify, dan cancel. Cek status order tidak memotong kuota. Pantau sisa kuota dengan GET /api/qris/usage.
  • Endpoint webhook publik. Di production, webhook_url harus HTTPS dan tidak boleh mengarah ke IP privat atau localhost. Untuk uji coba lokal, pakai tunnel seperti ngrok atau Cloudflare Tunnel.

Contoh integrasi API QRIS dengan Node.js

Contoh di bawah memakai Express dan fetch bawaan Node.js 18 ke atas. Ada tiga route: membuat link pembayaran saat checkout, menerima webhook, dan cek status order. Ganti nama domain dan penyimpanan order dengan milik kamu.

const express = require('express');
            const crypto = require('crypto');

            const app = express();
            const API_KEY = process.env.TEMANQRIS_API_KEY;
            const WEBHOOK_SECRET = process.env.TEMANQRIS_WEBHOOK_SECRET;
            const BASE = 'https://temanqris.com/api/qris';

            async function temanqris(path, options = {}) {
              const res = await fetch(BASE + path, {
                ...options,
                headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }
              });
              const data = await res.json();
              if (!res.ok) throw new Error(data.message || data.error || 'TemanQRIS error');
              return data;
            }

            // 1. Checkout: buat link pembayaran untuk satu pesanan
            app.post('/checkout', express.json(), async (req, res) => {
              const orderId = 'INV-' + Date.now().toString(36).toUpperCase();
              const { payment_link } = await temanqris('/payment-link', {
                method: 'POST',
                body: JSON.stringify({
                  amount: 125000, // rupiah bulat, tanpa desimal
                  description: 'Pesanan ' + orderId,
                  order_id: orderId, // maksimal 30 karakter, unik per akun
                  webhook_url: 'https://tokokamu.com/webhooks/temanqris',
                  callback_url: 'https://tokokamu.com/terima-kasih?order=' + orderId
                })
              });
              // simpan orderId + payment_link.link_code di database, status "pending"
              res.json({ payment_url: 'https://temanqris.com' + payment_link.url });
            });

            // 2. Webhook: pakai raw body, karena tanda tangan dihitung dari teks aslinya
            app.post('/webhooks/temanqris', express.raw({ type: 'application/json' }), (req, res) => {
              const expected = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET)
                .update(req.body).digest('hex');
              const received = req.get('X-TemanQRIS-Signature') || '';
              const valid = received.length === expected.length &&
                crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
              if (!valid) return res.status(401).send('invalid signature');

              const payload = JSON.parse(req.body.toString('utf8'));
              // lewati kalau payload.idempotency_key sudah pernah diproses
              if (payload.event === 'payment.awaiting_confirmation') {
                // pembeli klik "Sudah Bayar": cek mutasi sebelum kirim barang
              }
              if (payload.event === 'payment.confirmed') {
                // tandai payload.data.order_id lunas, lanjut proses pesanan
              }
              res.sendStatus(204);
            });

            // 3. Cek status dari backend
            app.get('/orders/:id/status', async (req, res) => {
              const { order } = await temanqris('/orders/' + encodeURIComponent(req.params.id));
              res.json({ status: order.status, is_paid: order.is_paid, is_expired: order.is_expired });
            });

            app.listen(3000);

Perhatikan route webhook memakai express.raw, bukan express.json. Kalau body sudah di-parse lalu di-stringify ulang, urutan atau spasi bisa berubah dan tanda tangan tidak akan cocok. Ini penyebab nomor satu "signature selalu gagal" yang kami temui.

Contoh integrasi API QRIS dengan PHP

Versi PHP memakai cURL bawaan, jadi jalan di hosting biasa tanpa Composer. Pisahkan jadi dua file: satu untuk membuat pembayaran, satu untuk menerima webhook.

<?php
            // buat-pembayaran.php
            function temanqris(string $method, string $path, ?array $body = null): array
            {
                $ch = curl_init('https://temanqris.com/api/qris' . $path);
                curl_setopt_array($ch, [
                    CURLOPT_CUSTOMREQUEST  => $method,
                    CURLOPT_RETURNTRANSFER => true,
                    CURLOPT_TIMEOUT        => 15,
                    CURLOPT_HTTPHEADER     => [
                        'X-API-Key: ' . getenv('TEMANQRIS_API_KEY'),
                        'Content-Type: application/json',
                    ],
                ]);
                if ($body !== null) {
                    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
                }
                $raw = curl_exec($ch);
                $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);

                $data = json_decode($raw ?: '{}', true) ?? [];
                if ($status >= 400) {
                    throw new RuntimeException($data['message'] ?? $data['error'] ?? 'TemanQRIS error');
                }
                return $data;
            }

            $orderId = 'INV-' . date('Ymd') . '-' . random_int(1000, 9999);
            $result = temanqris('POST', '/payment-link', [
                'amount'       => 125000,
                'description'  => 'Pesanan ' . $orderId,
                'order_id'     => $orderId,
                'webhook_url'  => 'https://tokokamu.com/webhook-temanqris.php',
                'callback_url' => 'https://tokokamu.com/terima-kasih.php?order=' . $orderId,
            ]);

            // simpan $orderId dan $result['payment_link']['link_code'] dengan status "pending"
            header('Location: https://temanqris.com' . $result['payment_link']['url']);
            exit;
<?php
            // webhook-temanqris.php
            $rawBody   = file_get_contents('php://input');
            $signature = $_SERVER['HTTP_X_TEMANQRIS_SIGNATURE'] ?? '';
            $expected  = 'sha256=' . hash_hmac('sha256', $rawBody, getenv('TEMANQRIS_WEBHOOK_SECRET'));

            if (!hash_equals($expected, $signature)) {
                http_response_code(401);
                exit('invalid signature');
            }

            $payload = json_decode($rawBody, true);
            $orderId = $payload['data']['order_id'] ?? '';
            $key     = $payload['idempotency_key'] ?? '';
            // 1. hentikan di sini kalau $key sudah pernah diproses (simpan di tabel sendiri)

            if ($payload['event'] === 'payment.awaiting_confirmation') {
                // pembeli bilang sudah bayar: cek mutasi dulu
            } elseif ($payload['event'] === 'payment.confirmed') {
                // tandai $orderId lunas
            }

            http_response_code(200);
            echo json_encode(['received' => true]);

hash_equals dipakai supaya perbandingan tanda tangan aman dari timing attack. Jangan ganti dengan ==.

Status order yang perlu kamu tangani

StatusArtinyaYang sebaiknya dilakukan sistem kamu
pendingLink sudah dibuat, belum ada tindakan dari pembeli.Tampilkan halaman menunggu pembayaran.
awaiting_confirmationPembeli klik "Sudah Bayar".Beri tahu admin untuk cek mutasi. Jangan kirim barang dulu.
paidMerchant (atau gateway otomatis) sudah mengonfirmasi.Proses pesanan.
expiredLink melewati masa berlaku (default 24 jam di Free, 7 hari di Premium).Buat link baru kalau pembeli masih mau bayar.
cancelledOrder dibatalkan lewat dashboard atau POST /api/qris/orders/:orderId/cancel.Tutup pesanan.

Daftar lengkap field dan contoh respons ada di dokumentasi API QRIS TemanQRIS. Kalau kamu ingin mendalami cara kerja notifikasinya, kami menulis panduan terpisah soal webhook pembayaran QRIS dan cara cek status bayar.

Kesalahan yang sering muncul saat integrasi

  • API key di frontend. Siapa pun bisa membukanya lewat DevTools. Semua panggilan ke TemanQRIS harus lewat backend.
  • Nominal desimal. amount harus rupiah bulat. Nilai seperti 125000.5 ditolak dengan error 400.
  • Order ID dipakai ulang. Endpoint payment link menolak order_id yang sudah ada di akun kamu. Pakai nomor invoice yang unik dan maksimal 30 karakter.
  • Menganggap callback_url sebagai bukti bayar. Callback hanya mengarahkan browser pembeli setelah klik "Sudah Bayar". Siapa pun bisa membuka URL itu secara manual.
  • Endpoint webhook yang redirect. Pengirim webhook tidak mengikuti redirect. Respons 301 atau 302 dihitung gagal dan akan dicoba ulang. Pastikan URL-nya final, termasuk soal garis miring di akhir.
  • Proses berat sebelum membalas. Batas waktu tunggu webhook 10 detik. Simpan dulu, balas 2xx, lalu kerjakan sisanya di background.

Kapan pakai mode Automatic Payment Gateway

Kalau toko kamu ramai dan cek mutasi manual mulai merepotkan, TemanQRIS punya endpoint POST /api/payment-gateway/payments. Di mode ini server TemanQRIS mencocokkan pembayaran sendiri lalu mengirim payment.confirmed tanpa campur tangan admin. Ada beberapa syarat dan konsekuensi yang perlu kamu tahu:

  • Akun harus lolos verifikasi KYC.
  • Setiap request wajib membawa header Idempotency-Key. Pakai key yang sama saat mengulang request yang sama.
  • Nominal dasar maksimal Rp1.000.000 per transaksi.
  • Server menambahkan kode unik 0 sampai 999 rupiah ke total, dan kode unik ini tidak dikembalikan.
  • Ada biaya layanan gateway yang dipotong dari saldo kamu, bukan ditambahkan ke tagihan pembeli. Tarifnya 0,8% dari nominal dasar untuk semua nominal, dibulatkan ke atas ke rupiah penuh. Saldo kamu bertambah sebesar net_amount.
  • Dana masuk ke saldo wallet TemanQRIS dan bisa ditarik mulai H+1, yaitu pukul 00.00 WIB sehari setelah pembayaran. Penarikan dikenai biaya persentase dengan batas bawah dan batas atas (biaya penarikan yang berlaku saat ini ada di FAQ).

Untuk toko kecil dengan beberapa pesanan sehari, mode reguler biasanya sudah cukup dan lebih hemat. Mode gateway mulai masuk akal saat jumlah pesanan membuat cek mutasi manual jadi pekerjaan tersendiri. Kalau kamu masih menimbang penyedia lain, baca juga perbandingan kami soal alternatif Midtrans dan Xendit untuk QRIS.

Pertanyaan yang Sering Diajukan (FAQ)

Apakah API QRIS TemanQRIS bisa dipakai gratis?

Bisa. Paket Free memberi akses API dengan kuota 50 request per hari dan 1 QRIS merchant. Paket Premium Rp 9.000 per bulan menaikkan kuota menjadi 1.000 request per hari.

Bahasa pemrograman apa yang bisa dipakai untuk integrasi API QRIS?

Bahasa apa pun yang bisa mengirim HTTP request dan menghitung HMAC-SHA256. Artikel ini memberi contoh Node.js dan PHP, dan dokumentasi resmi juga menyediakan contoh cURL.

Apakah pembayaran langsung terkonfirmasi otomatis setelah pembeli scan?

Di mode reguler tidak. Uang masuk langsung ke rekening QRIS kamu, jadi kamu yang mengonfirmasi lunas lewat dashboard atau endpoint verify. Konfirmasi otomatis tersedia di mode Automatic Payment Gateway yang butuh verifikasi KYC.

Kenapa webhook saya tidak pernah masuk?

Penyebab paling umum adalah webhook secret belum dibuat di dashboard, URL webhook tidak bisa diakses publik atau tidak HTTPS, atau endpoint membalas dengan redirect. Pastikan endpoint membalas status 2xx dalam 10 detik.