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.
Upload QRIS Merchant
Sistem membaca QRIS string otomatis dari gambar stiker.
| 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.
| 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.
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.
| 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.
X-QRIS-Gateway-Event: invoice.paid
X-QRIS-Gateway-Timestamp dalam format ISO 8601.
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.
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.
X-API-KEY dan X-API-SECRET.
order_id baru atau cek invoice yang sudah ada.
pay_amount, bukan amount.
Untuk database existing, pastikan migration sudah dijalankan agar kolom seperti sticker_path tersedia.