Dokumentasi API QRIS Gateway

Pedoman integrasi untuk upload stiker QRIS statis, membuat tagihan QRIS dinamis, menerima notifikasi listener, dan memvalidasi pembayaran lewat webhook.

5 Endpoint API utama
HMAC Signature webhook
QR Decode Stiker dibaca otomatis
Nominal Matching via pay_amount

Autentikasi

Semua endpoint API menggunakan credential yang terhubung ke merchant aktif.

Header Keterangan
X-API-KEY API key merchant dari tabel api_keys.
X-API-SECRET API secret merchant. Merchant harus berstatus active.
X-API-KEY: test-api-key
X-API-SECRET: test-api-secret
Content-Type: application/json

Alur Integrasi

Urutan kerja normal dari setup QRIS sampai pembayaran terverifikasi.

1 Upload Stiker Merchant upload gambar QRIS. Sistem membaca QR string dari gambar.
2 Buat Invoice Merchant membuat tagihan. Sistem membuat QRIS dinamis dan pay_amount.
3 Customer Bayar Customer scan QRIS dinamis dan membayar nominal pay_amount.
4 Listener Kirim Data Listener mengirim nominal, referensi, waktu, dan raw data.
5 Matching Sistem mencocokkan merchant_id, pay_amount, status pending, dan expiry.
6 Webhook Jika cocok, invoice paid dan webhook invoice.paid dikirim ke merchant.

Upload QRIS Merchant

Sistem membaca QRIS string otomatis dari gambar stiker.

POST /api/merchant-qris
Field Wajib Keterangan
name Tidak Nama QRIS. Default Default QRIS.
sticker Ya Gambar stiker QRIS, maksimal 4 MB.
is_active Tidak Default true. QRIS lain akan dinonaktifkan.
curl -X POST https://qris-gateway.stilabook.com/api/merchant-qris \
  -H "X-API-KEY: test-api-key" \
  -H "X-API-SECRET: test-api-secret" \
  -F "name=QRIS Utama" \
  -F "sticker=@/path/qris.png"

Jika gambar QRIS tidak terbaca, gunakan gambar yang jelas, QR code terlihat penuh, tidak blur, dan tidak terpotong.

Buat Invoice QRIS Dinamis

Membuat tagihan dan mengembalikan QRIS dinamis yang siap discan customer.

POST /api/invoices
Field Wajib Keterangan
order_id Ya Unik per merchant.
amount Ya Nominal sebelum kode unik.
qris_id Tidak Pilih QRIS aktif tertentu.
expired_in_minutes Tidak Default 15, maksimum 1440.
use_unique_code Tidak Default true.
metadata Tidak Object data tambahan.
curl -X POST https://qris-gateway.stilabook.com/api/invoices \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: test-api-key" \
  -H "X-API-SECRET: test-api-secret" \
  -d '{
    "order_id": "ORDER-1001",
    "amount": 10000,
    "expired_in_minutes": 15,
    "metadata": {
      "customer_name": "Budi"
    }
  }'
{
  "success": true,
  "message": "Invoice QRIS berhasil dibuat.",
  "data": {
    "invoice_number": "INV-20260503093000-ABC123",
    "order_id": "ORDER-1001",
    "amount": 10000,
    "unique_code": 123,
    "pay_amount": 10123,
    "status": "pending",
    "qris_string": "000201010212...",
    "qris_image_url": "https://qris-gateway.stilabook.com/storage/qris-invoices/INV-20260503093000-ABC123.svg",
    "expired_at": "2026-05-03T09:45:00+08:00",
    "paid_at": null,
    "payment": null
  }
}

Cek Invoice

Ambil status invoice menggunakan invoice number atau id.

GET /api/invoices/{invoice_number_or_id}
curl https://qris-gateway.stilabook.com/api/invoices/INV-20260503093000-ABC123 \
  -H "X-API-KEY: test-api-key" \
  -H "X-API-SECRET: test-api-secret"

Notifikasi Listener

Dipakai listener Android/server untuk mengirim pembayaran yang masuk.

POST /api/listener/qris-notifications
Field Wajib Keterangan
amount Ya Integer atau format seperti Rp15.000.
reference Tidak Referensi bank/payment app.
transaction_time Tidak Default waktu server.
source Tidak Default android-listener.
raw_data Tidak Payload asli listener.
curl -X POST https://qris-gateway.stilabook.com/api/listener/qris-notifications \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: test-api-key" \
  -H "X-API-SECRET: test-api-secret" \
  -d '{
    "amount": "Rp10.123",
    "reference": "BANK-REF-001",
    "transaction_time": "2026-05-03 10:00:00",
    "source": "android-listener"
  }'

Alias endpoint /api/payment-notifications tersedia untuk kompatibilitas.

Webhook ke Merchant

Dikirim saat invoice cocok dengan notifikasi listener dan berubah menjadi paid.

Header Event X-QRIS-Gateway-Event: invoice.paid
Header Timestamp X-QRIS-Gateway-Timestamp dalam format ISO 8601.
Header Signature hash_hmac('sha256', timestamp + '.' + raw_body, webhook_secret).
{
  "event": "invoice.paid",
  "invoice": {
    "invoice_number": "INV-20260503093000-ABC123",
    "order_id": "ORDER-1001",
    "amount": 10000,
    "unique_code": 123,
    "pay_amount": 10123,
    "status": "paid"
  },
  "payment": {
    "payment_reference": "BANK-REF-001",
    "amount": 10123,
    "status": "success",
    "source": "android-listener"
  }
}

Contoh Kode Penerima Webhook (Laravel)

Berikut contoh siap pakai untuk menerima webhook, verifikasi signature, lalu membaca payload yang diterima merchant endpoint.

POST /webhook/examples/merchant-receiver

1) Tambahkan env secret:

MERCHANT_WEBHOOK_SECRET=change-this-with-random-secret-min-32-chars

2) Route contoh receiver:

// routes/web.php
Route::post('/webhook/examples/merchant-receiver', [WebhookExampleController::class, 'receiveMerchantWebhook'])
    ->name('webhook.examples.merchant-receiver');

3) Controller verifikasi signature + baca payload:

// app/Http/Controllers/WebhookExampleController.php
namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class WebhookExampleController extends Controller
{
    public function receiveMerchantWebhook(Request $request): JsonResponse
    {
        $secret = (string) config('services.merchant_webhook.secret', '');

        if ($secret === '') {
            return response()->json([
                'success' => false,
                'message' => 'MERCHANT_WEBHOOK_SECRET belum diatur pada environment.',
            ], 500);
        }

        $timestamp = (string) $request->header('X-QRIS-Gateway-Timestamp', '');
        $signature = (string) $request->header('X-QRIS-Gateway-Signature', '');
        $event = (string) $request->header('X-QRIS-Gateway-Event', 'unknown');

        if ($timestamp === '' || $signature === '') {
            return response()->json([
                'success' => false,
                'message' => 'Header signature/timestamp tidak lengkap.',
            ], 400);
        }

        $rawBody = $request->getContent();
        $expectedSignature = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);

        if (! hash_equals($expectedSignature, $signature)) {
            return response()->json([
                'success' => false,
                'message' => 'Signature webhook tidak valid.',
            ], 401);
        }

        $payload = $request->json()->all();

        Log::info('Merchant webhook received', [
            'event' => $event,
            'payload' => $payload,
            'received_at' => now()->toIso8601String(),
        ]);

        return response()->json([
            'success' => true,
            'message' => 'Webhook diterima dan signature valid.',
            'data' => $payload,
        ]);
    }
}

Status Data

Nilai status yang perlu dipakai saat integrasi.

Invoice Arti
pending Menunggu pembayaran.
paid Sudah dibayar dan payment tercatat.
expired Kadaluarsa.
cancelled Dibatalkan.
Notification Arti
unmatched Belum cocok dengan invoice.
matched Cocok dan invoice menjadi paid.
ignored Duplikat atau sudah pernah diproses.

Error Umum

Kondisi yang paling sering muncul saat integrasi.

401 API credential tidak valid Periksa header X-API-KEY dan X-API-SECRET.
422 QRIS aktif tidak ditemukan Upload stiker QRIS dulu atau aktifkan salah satu QRIS merchant.
409 Order ID sudah pernah dibuat Gunakan order_id baru atau cek invoice yang sudah ada.
Listener unmatched Pastikan nominal pembayaran sama dengan pay_amount, bukan amount.

Untuk database existing, pastikan migration sudah dijalankan agar kolom seperti sticker_path tersedia.