Public API Docs

QRIS SaaS API Documentation

Dokumentasi singkat untuk integrasi payment QRIS, API token, webhook notifikasi transaksi, dan operasional merchant.

Base URL

Gunakan APP_BASE_URL atau NEXT_PUBLIC_API_BASE_URL. Default local: http://localhost:3000

Authentication

Endpoint merchant dan developer memakai session login, dan integrasi server-to-server memakai header X-API-Token.

Generate QRIS

POST /api/payment/qris/generate dengan body amount, payment_reference opsional, dan return_url opsional.

Payment Status

GET /api/payment/qris/{qrisId}/status untuk memeriksa pending, paid, expired, atau cancelled.

Cancel QRIS

POST /api/payment/qris/{qrisId}/cancel untuk membatalkan QRIS yang masih pending.

Transactions & Balance

GET /api/payment/transactions dan GET /api/payment/balance untuk melihat histori transaksi dan saldo merchant.

Merchant Profile

GET/PUT /api/merchant/profile untuk mengatur merchantName, merchantCity, qrisStatic, dan webhookUrl.

Developer Tools

GET/POST /api/developer/tokens, DELETE /api/developer/tokens/{id}, dan POST /api/developer/webhook/test.

Contoh Header

Content-Type: application/json
X-API-Token: cki_your_token_here

Contoh Response Generate QRIS

{
  "status": "success",
  "data": {
    "qris_id": "abc-123",
    "amount": 50000,
    "qris_image_url": "http://localhost:3000/api/public/qris/abc-123/image",
    "expires_in_seconds": 900
  }
}

cURL

Contoh request untuk generate QRIS dinamis.

curl -X POST https://qris.commitflow.space/api/payment/qris/generate -H "X-API-Token: YOUR_API_TOKEN" -H "Content-Type: application/json" -d '{
  "amount": 50000,
  "payment_reference": "Order-123",
  "return_url": "https://yoursite.com/thanks"
}'

JavaScript

Contoh integrasi fetch untuk generate QRIS, cek status, dan cancel.

const API_TOKEN = 'YOUR_API_TOKEN';
const API_URL = 'https://qris.commitflow.space';

async function generateQRIS(amount, paymentRef = null, returnUrl = null) {
  const body = { amount };
  if (paymentRef) body.payment_reference = paymentRef;
  if (returnUrl) body.return_url = returnUrl;

  const response = await fetch(`https://qris.commitflow.space/api/payment/qris/generate`, {
    method: 'POST',
    headers: {
      'X-API-Token': API_TOKEN,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });

  return await response.json();
}

async function checkPaymentStatus(qrisId) {
  const response = await fetch(`https://qris.commitflow.space/api/payment/qris/${qrisId}/status`, {
    headers: { 'X-API-Token': API_TOKEN }
  });
  return await response.json();
}

async function cancelQRIS(qrisId) {
  const response = await fetch(`https://qris.commitflow.space/api/payment/qris/${qrisId}/cancel`, {
    method: 'POST',
    headers: { 'X-API-Token': API_TOKEN }
  });
  return await response.json();
}

PHP

Contoh integrasi server-side dengan cURL.

<?php

define('API_TOKEN', 'YOUR_API_TOKEN');
define('API_URL', 'https://qris.commitflow.space');

function generateQRIS($amount, $paymentRef = null, $returnUrl = null) {
    $url = API_URL . '/api/payment/qris/generate';
    $data = ['amount' => $amount];

    if ($paymentRef) {
        $data['payment_reference'] = $paymentRef;
    }

    if ($returnUrl) {
        $data['return_url'] = $returnUrl;
    }

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'X-API-Token: ' . API_TOKEN
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    return json_decode($response, true);
}

Python

Contoh integrasi memakai requests.

import requests

API_TOKEN = 'YOUR_API_TOKEN'
API_URL = 'https://qris.commitflow.space'

def generate_qris(amount, payment_ref=None, return_url=None):
    url = f'{API_URL}/api/payment/qris/generate'
    headers = {
        'X-API-Token': API_TOKEN,
        'Content-Type': 'application/json'
    }

    data = {'amount': amount}
    if payment_ref:
        data['payment_reference'] = payment_ref
    if return_url:
        data['return_url'] = return_url

    response = requests.post(url, json=data, headers=headers)
    return response.json()

result = generate_qris(50000, 'Order-123', 'https://yoursite.com/thanks')
print(result['data']['qris_image_url'])

🛡️ Webhook Security & Signature Verification

Setiap kali QRISPAY mengirimkan event webhook ke server Anda, request tersebut akan disertai dengan header keamanan bernama X-Qrispay-Signature. Gunakan header ini dan Webhook Secret unik Anda (dapat dilihat di Merchant Profile atau Developer Menu) untuk memverifikasi keaslian payload webhook dan mencegah pengiriman data palsu (fake webhooks).

Format Header Signature:

X-Qrispay-Signature: t=1672531199,v1=f6a9c7b8d8...
  • t: Unix timestamp dalam satuan detik saat webhook dikirim. Periksa ini di server Anda untuk mencegah replay attacks (mis. tolak jika selisih waktu > 5 menit).
  • v1: Tanda tangan hash HMAC-SHA256 yang dibuat dengan memformulasikan data ${timestamp}.${payloadString} menggunakan Webhook Secret Anda.

Contoh Kode Verifikasi (Node.js / Express):

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

app.use(express.json());

app.post('/webhook/callback', (req, res) => {
  const signatureHeader = req.headers['x-qrispay-signature'];
  const WEBHOOK_SECRET = 'YOUR_WEBHOOK_SECRET'; // whsec_...

  if (!signatureHeader) {
    return res.status(400).send('Missing signature header');
  }

  const parts = signatureHeader.split(',');
  const timestampPart = parts.find(p => p.startsWith('t='));
  const signaturePart = parts.find(p => p.startsWith('v1='));

  if (!timestampPart || !signaturePart) {
    return res.status(400).send('Invalid signature format');
  }

  const timestamp = timestampPart.split('=')[1];
  const receivedSignature = signaturePart.split('=')[1];

  const diffInMinutes = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) / 60;
  if (diffInMinutes > 5) {
    return res.status(400).send('Request expired / replay attack suspected');
  }

  const payloadString = JSON.stringify(req.body);
  const signaturePayload = `${timestamp}.${payloadString}`;

  const computedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signaturePayload)
    .digest('hex');

  if (computedSignature === receivedSignature) {
    console.log('✅ Webhook terverifikasi asli!');
    res.status(200).json({ received: true });
  } else {
    console.log('❌ Webhook palsu / signature tidak cocok!');
    res.status(401).send('Unauthorized signature');
  }
});