{"openapi":"3.1.0","info":{"title":"QR-Fik Payment Gateway API","version":"1.0.0","description":"\n# QR-Fik Integration Guide\n\nQR-Fik is a headless, multi-merchant payment gateway microservice that transforms static Indonesian QRIS codes into dynamic, amount-locked QR codes. It verifies payments automatically via an Android notification forwarding bridge without requiring bank payment gateway contracts or monthly fees.\n\nThe microservice exposes standard REST endpoints. **No third-party SDK or NPM package is required.** Integration is achieved through native HTTP clients in any programming language (such as Laravel HTTP Client in PHP or native fetch in TypeScript/Node.js).\n\n---\n\n## Integration Workflow\n\n```mermaid\nsequenceDiagram\n    autonumber\n    actor Customer as Customer\n    participant ClientApp as Client Application (PHP / Nuxt)\n    participant QRFik as QR-Fik Microservice (qr.fikfikk.my.id)\n    participant Bank as Banking / E-Wallet Network\n    participant AndroidBridge as Android Bridge Device\n\n    Customer->>ClientApp: Select product & proceed to checkout\n    ClientApp->>QRFik: POST /api/v1/orders (X-Api-Key)\n    QRFik-->>ClientApp: Return totalAmount, uniqueCode, and qrSvgUrl\n    ClientApp-->>Customer: Display dynamic QR code and locked amount\n    Customer->>Bank: Scan QRIS and complete payment\n    Bank-->>AndroidBridge: Push notification: \"QRIS payment received\"\n    AndroidBridge->>QRFik: POST /api/v1/bridge/webhook (X-Bridge-Secret)\n    QRFik->>QRFik: Match amount with active pending order\n    QRFik->>ClientApp: POST outbound webhook callback (X-Signature HMAC)\n    ClientApp-->>QRFik: 200 OK\n    ClientApp-->>Customer: Update order state to PAID in real time\n```\n\n---\n\n## 1. PHP / Laravel Integration Guide\n\n### Step 1: Configure Environment Variables (.env)\nAdd your API credentials obtained during merchant registration:\n\n```env\nQRFIK_BASE_URL=\"https://qr.fikfikk.my.id\"\nQRFIK_API_KEY=\"qrfik_live_your_merchant_api_key\"\nQRFIK_WEBHOOK_SECRET=\"whsec_your_webhook_signing_secret\"\n```\n\n### Step 2: Create Payment Order (PaymentController.php)\nUse Laravel's built-in HTTP client to request dynamic QRIS generation:\n\n```php\nnamespace App\\Http\\Controllers;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Http;\n\nclass PaymentController extends Controller\n{\n    public function createOrder(Request $request)\n    {\n        // Memanggil endpoint QR-Fik untuk menghasilkan QRIS dinamis\n        $response = Http::withHeaders([\n            'X-Api-Key' => env('QRFIK_API_KEY'),\n            'Content-Type' => 'application/json',\n        ])->post(env('QRFIK_BASE_URL') . '/api/v1/orders', [\n            'amount' => 50000,\n            'clientReferenceId' => 'INV-' . time(),\n        ]);\n\n        if ($response->successful()) {\n            $orderData = $response->json('data');\n            // Simpan data tagihan ke database lokal Anda\n            return view('checkout', ['order' => $orderData]);\n        }\n\n        return back()->with('error', 'Gagal memproses tagihan QRIS');\n    }\n}\n```\n\n### Step 3: Render QR Code in Blade View (checkout.blade.php)\nEmbed the direct SVG vector URL into an `<img>` element:\n\n```html\n<div class=\"payment-container\">\n  <h2>Total Amount: Rp {{ number_format($order['pricing']['totalAmount'], 0, ',', '.') }}</h2>\n  <p>Scan the QR code below using any banking or e-wallet application:</p>\n  \n  <!-- Render gambar QRIS SVG langsung dari endpoint -->\n  <img src=\"{{ env('QRFIK_BASE_URL') }}{{ $order['qris']['qrSvgUrl'] }}\" alt=\"Dynamic QRIS\" width=\"280\">\n  \n  <p>Transfer the exact amount including unique digits to enable automatic verification.</p>\n</div>\n```\n\n### Step 4: Handle Outbound Webhook & Verify Signature (WebhookController.php)\nVerify the HMAC-SHA256 signature to guarantee request authenticity:\n\n```php\nnamespace App\\Http\\Controllers;\n\nuse Illuminate\\Http\\Request;\n\nclass WebhookController extends Controller\n{\n    public function handle(Request $request)\n    {\n        $signature = $request->header('X-Signature');\n        $rawBody = $request->getContent(); // Wajib membaca raw payload agar signature identik\n        $secret = env('QRFIK_WEBHOOK_SECRET');\n\n        // Validasi signature menggunakan hash_hmac SHA256\n        $calculatedSignature = hash_hmac('sha256', $rawBody, $secret);\n\n        if (!hash_equals($calculatedSignature, (string)$signature)) {\n            return response()->json(['error' => 'Invalid webhook signature'], 401);\n        }\n\n        $payload = json_decode($rawBody, true);\n        $clientReferenceId = $payload['clientReferenceId'];\n\n        // Perbarui status transaksi di database lokal Anda menjadi PAID\n        // Order::where('invoice_number', $clientReferenceId)->update(['status' => 'PAID']);\n\n        return response()->json(['success' => true]);\n    }\n}\n```\n\n> **Note on CSRF Exemption**: Exclude your webhook route from CSRF verification in `bootstrap/app.php` (Laravel 11) or `VerifyCsrfToken.php`:\n> ```php\n> $middleware->validateCsrfTokens(except: ['api/payment/callback']);\n> ```\n\n---\n\n## 2. TypeScript / Node.js Integration Guide\n\n### Step 1: Create Payment Order (server/api/payment/create.post.ts)\n```typescript\nexport default defineEventHandler(async (event) => {\n  const body = await readBody(event);\n  const qrfikBaseUrl = process.env.QRFIK_BASE_URL || 'https://qr.fikfikk.my.id';\n  const apiKey = process.env.QRFIK_API_KEY!;\n\n  // Buat order tagihan menggunakan HTTP fetch standar\n  const response = await $fetch(`${qrfikBaseUrl}/api/v1/orders`, {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': apiKey,\n      'Content-Type': 'application/json',\n    },\n    body: {\n      amount: body.amount,\n      clientReferenceId: `INV-${Date.now()}`,\n    },\n  });\n\n  return response;\n});\n```\n\n### Step 2: Client-side Polling & Real-time Redirect (pages/checkout.vue)\n```vue\n<script setup>\nconst { data: order } = await useFetch('/api/payment/create', {\n  method: 'POST',\n  body: { amount: 50000 },\n});\n\n// Jalankan polling setiap 3 detik untuk mendeteksi pembayaran lunas\nconst poller = setInterval(async () => {\n  if (!order.value?.data?.id) return;\n  const statusRes = await $fetch(`https://qr.fikfikk.my.id/api/v1/orders/${order.value.data.id}`);\n  if (statusRes.data?.status === 'PAID') {\n    clearInterval(poller);\n    navigateTo('/payment/success');\n  }\n}, 3000);\n\nonUnmounted(() => clearInterval(poller));\n</script>\n\n<template>\n  <div v-if=\"order?.data\" class=\"payment-card\">\n    <h2>Total: Rp {{ order.data.pricing.totalAmount.toLocaleString('id-ID') }}</h2>\n    <img :src=\"`https://qr.fikfikk.my.id${order.data.qris.qrSvgUrl}`\" alt=\"QRIS\" width=\"280\" />\n  </div>\n</template>\n```\n\n### Step 3: Webhook Route & HMAC Verification (server/api/payment/callback.post.ts)\n```typescript\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\nexport default defineEventHandler(async (event) => {\n  const signature = getHeader(event, 'x-signature');\n  const rawBody = await readRawBody(event); // Wajib membaca raw payload teks\n  const secret = process.env.QRFIK_WEBHOOK_SECRET!;\n\n  if (!signature || !rawBody) {\n    throw createError({ statusCode: 400, message: 'Missing webhook signature or payload' });\n  }\n\n  // Hitung ulang hash HMAC-SHA256 dari raw body\n  const calculatedSignature = createHmac('sha256', secret).update(rawBody).digest('hex');\n\n  // Bandingkan hash dengan timingSafeEqual untuk mencegah timing attack\n  const isSignatureValid = timingSafeEqual(\n    Buffer.from(calculatedSignature),\n    Buffer.from(signature)\n  );\n\n  if (!isSignatureValid) {\n    throw createError({ statusCode: 401, message: 'Invalid webhook signature' });\n  }\n\n  const payload = JSON.parse(rawBody);\n\n  // Perbarui status transaksi di database lokal Anda menjadi PAID\n  // await db.update(orders).set({ status: 'PAID' }).where(eq(orders.id, payload.clientReferenceId));\n\n  return { success: true };\n});\n```\n\n---\n\n## 3. Android Notification Bridge: Architecture & Step-by-Step Setup Guide\n\n### 3.1 What is the Android Bridge and Why is It Required?\nIndonesian banks and financial providers (*GoPay Partner*, *BCA*, *Livin' Mandiri*, *Dana Bisnis*, *ShopeePay*) **do not provide free incoming webhook APIs** to individual merchants or small businesses without expensive enterprise contracts and 0.7%–1.5% MDR commission deductions.\n\nHowever, whenever a customer scans your QRIS and completes payment, the bank app pushes an immediate **Android Push Notification** to the merchant's smartphone (*e.g., \"GoPay: Pembayaran QRIS Rp 50.156 dari Budi berhasil diterima\"*).\n\nThe **Android Bridge** is a notification listener automation running on that merchant smartphone. Its workflow is:\n1. Detect incoming push notifications from banking apps in real time (< 1 second).\n2. Extract the mutation amount (*e.g., 50156*).\n3. Dispatch a secure HTTP POST request to QR-Fik's `/api/v1/bridge/webhook` endpoint.\n4. QR-Fik matches the amount against active pending orders, marks the order as `PAID` via atomic CAS, and dispatches the signed outbound webhook to your client website (*e.g., RSVP ticketing or online store*).\n\n---\n\n### 3.2 Required Credentials & Endpoint\nBefore configuring the Android device, obtain your credentials from your **Merchant Registration Response** or from the **Cek Akun (Self-Service Portal)**:\n\n* **Target URL**: `https://qr.fikfikk.my.id/api/v1/bridge/webhook` (or `http://YOUR_SERVER_IP:4000/api/v1/bridge/webhook`)\n* **Required Header**: `X-Bridge-Secret: bridge_sec_xxxxxxxxxxxxxxxxxxxxxxxx`\n* **Content-Type**: `application/json`\n\n---\n\n### 3.3 Practical Setup via MacroDroid (Recommended for Ease & Reliability)\n\n[MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) is a free automation application on the Google Play Store that requires no coding skills.\n\n#### Step 1: Create a New Macro\n1. Open MacroDroid and tap **Add Macro**.\n2. Set the macro name: `QR-Fik Forwarder`.\n\n#### Step 2: Configure the Trigger (Notification Received)\n1. Tap the **(+)** button in the **Triggers** (Red) section.\n2. Select **Device Events** → **Notification** → **Notification Received**.\n3. Choose **Select Applications** and check your merchant payment applications:\n   * *GoPay Merchant / GoBiz*\n   * *BCA mobile / myBCA*\n   * *Livin' by Mandiri*\n   * *DANA Bisnis*\n   * *BRImo / BNI Mobile*\n4. Under text content, leave it to match any or specify keywords like `QRIS`, `Pembayaran`, `Transfer`, `Berhasil`.\n\n#### Step 3: Extract the Numerical Amount\nUse MacroDroid's local variable or text manipulation:\n1. In **Actions**, add **Applications** → **Text Manipulation** (or define variable `amount`).\n2. Extract the digits after currency symbol (regex `[\\d\\.]+` after `Rp`).\n3. Strip any thousand separators/dots so the value becomes a pure integer (*e.g., 50.156 → 50156*).\n\n#### Step 4: Configure the HTTP POST Webhook Action\n1. Tap the **(+)** button in the **Actions** (Blue) section.\n2. Select **Connectivity** → **HTTP Request**.\n3. Configure the HTTP parameters:\n   * **Request Method**: `POST`\n   * **URL**: `https://your-domain.com/api/v1/bridge/webhook`\n   * **Headers**:\n     * Header 1: `X-Bridge-Secret` = `bridge_sec_your_merchant_secret`\n     * Header 2: `Content-Type` = `application/json`\n   * **Content Body**:\n     ```json\n     {\n       \"amount\": {lv=amount},\n       \"sender\": \"{notif_title}\",\n       \"rawText\": \"{notif_body}\"\n     }\n     ```\n4. Save and activate the Macro.\n\n---\n\n### 3.4 Crucial Android OS Permissions (Preventing Background Killing)\n\nAndroid aggressively suspends background services to save battery. To ensure 24/7 continuous operation for your store:\n\n1. **Notification Access Permission**:\n   * Go to: `Settings` → `Apps` → `Special App Access` → `Notification Access`.\n   * Ensure MacroDroid / Bridge App is toggled **ON**.\n2. **Battery Optimization Exemption**:\n   * Go to: `Settings` → `Battery` → `Battery Optimization` (or `App Info` → `Battery`).\n   * Set the app to **Unrestricted** (*Don't optimize*).\n3. **Autostart Permission (Xiaomi MIUI, Oppo ColorOS, Vivo Funtouch, Realme)**:\n   * Enable **Autostart** for the listener app in device security manager.\n   * Lock the application in the **Recent Apps / Task Switcher** screen (*padlock icon*) so it persists after reboots.\n4. **Persistent Internet Connectivity**:\n   * Keep the cashier device connected to uninterrupted Wi-Fi or cellular data with charger plugged in during store operating hours.\n\n---\n\n### 3.5 Bank Notification Regex Reference (Indonesia)\n\n| Banking Application | Typical Notification Format | Regex Extraction |\n|---|---|---|\n| **GoPay Partner / GoBiz** | `Pembayaran QRIS Rp 50.156 dari Budi berhasil diterima.` | `Rp\\s*([\\d\\.]+)` |\n| **BCA Mobile** | `m-Transfer: Rp 50.156 dari HENDRA masuk ke rekening.` | `Rp\\s*([\\d\\.]+)` |\n| **Livin' by Mandiri** | `Dana Masuk Rp 50.156 dari QRIS via ASPI.` | `Rp\\s*([\\d\\.]+)` |\n| **DANA Bisnis** | `Kamu menerima pembayaran QRIS sebesar Rp50.156.` | `Rp\\s*([\\d\\.]+)` |\n\n---\n\n### 3.6 Manual Diagnostic Test via cURL\n\nYou can test your bridge endpoint immediately from terminal or Postman without waiting for a bank transfer:\n\n```bash\ncurl -X POST \"https://qr.fikfikk.my.id/api/v1/bridge/webhook\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Bridge-Secret: YOUR_BRIDGE_SECRET\" \\\n  -d '{\n    \"amount\": 50156,\n    \"sender\": \"GoPay Partner\",\n    \"rawText\": \"Pembayaran QRIS Rp 50.156 dari Budi berhasil diterima.\"\n  }'\n```\n\n* If an active pending order exists with total amount `50156`, the endpoint returns `200 OK` and resolves the transaction.\n* If no matching active order is found, it returns `404 Not Found` and records the payload in `unmatched_logs` for auditing.\n\n---\n\n## 4. Error Handling and Edge Cases\n\n* **Expired Orders**: Transactions default to a 15-minute validity window. If unpaid after 15 minutes, orders transition to `EXPIRED` and unique codes are returned to the allocation pool.\n* **Unmatched Settlements**: If payments arrive after expiration or with mismatched amounts, records are preserved in `unmatched_logs` for audit and manual resolution.\n    ","contact":{"name":"QR-Fik Support","url":"https://github.com/FikFikk"}},"servers":[{"url":"https://qr.fikfikk.my.id","description":"Production Server (HTTPS)"},{"url":"http://localhost:4000","description":"Local Development Server"}],"tags":[{"name":"Merchant","description":"Merchant registration, API keys, and static QRIS storage"},{"name":"Orders","description":"Dynamic QRIS generation, status polling, and simulation"},{"name":"Bridge","description":"Android notification bridge inbound webhook receiver"},{"name":"System","description":"Server diagnostics and health checks"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Merchant API key required for order creation and profile retrieval"},"BridgeSecretAuth":{"type":"apiKey","in":"header","name":"X-Bridge-Secret","description":"Bridge secret token configured on the Android notification listener device"},"AdminSecretAuth":{"type":"apiKey","in":"header","name":"X-Admin-Secret","description":"Master administrative secret token required to authorize registration of new merchants"}},"schemas":{"ErrorResponse":{"type":"object","required":["success","message"],"properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Invalid or missing credentials"}}},"ValidationErrorResponse":{"type":"object","required":["success","errors"],"properties":{"success":{"type":"boolean","example":false},"errors":{"type":"object","description":"Peta rincian kegagalan validasi tiap field","example":{"amount":{"_errors":["Nominal harus berupa bilangan bulat positif"]}}}}},"MerchantResponse":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Merchant berhasil didaftarkan"},"data":{"type":"object","required":["id","name","apiKey","bridgeSecret","webhookSecret","qrisStaticRaw"],"properties":{"id":{"type":"string","description":"Pengenal unik merchant","example":"mrc_cfe5f4b7-c029-4409-ba44-a76da3e1571e"},"name":{"type":"string","description":"Nama entitas toko/merchant","example":"Fikri Store"},"apiKey":{"type":"string","description":"Kunci API untuk transaksi pembuat order","example":"qrfik_live_3d0356a0ddd5c6c25cf5073ef55ce7b005c7195386814e29"},"bridgeSecret":{"type":"string","description":"Kunci rahasia untuk otentikasi HP Android bridge","example":"bridge_sec_790fdd8353387dc2e8b045b18b7f662803962590fc0444f3"},"webhookSecret":{"type":"string","description":"Kunci simetris untuk penandatanganan HMAC-SHA256","example":"whsec_c614a6de3e710bf99eb34c83740fe04af1c5d2cfbf1e3d9a"},"webhookUrl":{"type":"string","nullable":true,"description":"URL webhook callback milik sistem klien","example":"https://domain-rsvp.com/api/payment/callback"},"qrisStaticRaw":{"type":"string","description":"String EMVCo QRIS statis master","example":"00020101021126600016ID.CO.SHOPEE.PAY..."},"qrisImagePath":{"type":"string","nullable":true,"description":"Jalur relatif penyimpanan berkas gambar QR","example":"/storage/qris/qris_de3f610b.png"},"qrisImageUrl":{"type":"string","nullable":true,"description":"URL publik gambar QRIS statis di storage","example":"https://qr.fikfikk.my.id/storage/qris/qris_de3f610b.png"}}}}},"MerchantProfileResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"id":{"type":"string","example":"mrc_cfe5f4b7-c029-4409-ba44-a76da3e1571e"},"name":{"type":"string","example":"Fikri Store"},"apiKey":{"type":"string","example":"qrfik_live_3d0356a0ddd5c6c25cf5073e..."},"bridgeSecret":{"type":"string","example":"bridge_sec_790fdd8353387dc2e8b0..."},"webhookUrl":{"type":"string","example":"https://domain-rsvp.com/api/payment/callback"},"qrisStaticRaw":{"type":"string","example":"00020101021126600016ID.CO.SHOPEE.PAY..."},"qrisImageUrl":{"type":"string","example":"https://qr.fikfikk.my.id/storage/qris/qris_de3f610b.png"},"isActive":{"type":"boolean","example":true},"createdAt":{"type":"string","example":"2026-09-05T08:31:58.000Z"}}}}},"OrderCreateRequest":{"type":"object","required":["amount"],"properties":{"amount":{"type":"integer","minimum":1000,"description":"Nominal tagihan asli tanpa kode unik (minimal Rp 1.000)","example":50000},"clientReferenceId":{"type":"string","maxLength":128,"description":"ID referensi transaksi atau nomor invoice dari sistem klien","example":"INV-RSVP-101"}}},"OrderResponse":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Order tagihan berhasil dibuat"},"data":{"type":"object","properties":{"id":{"type":"string","description":"ID unik transaksi order di QR-Fik","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"clientReferenceId":{"type":"string","nullable":true,"example":"INV-RSVP-101"},"merchantName":{"type":"string","example":"Fikri Store"},"pricing":{"type":"object","properties":{"originalAmount":{"type":"integer","description":"Nominal awal","example":50000},"uniqueCode":{"type":"integer","description":"Kode unik 3 digit acak","example":570},"totalAmount":{"type":"integer","description":"Total wajib transfer (nominal + kode unik)","example":50570}}},"qris":{"type":"object","properties":{"rawString":{"type":"string","description":"String EMVCo QRIS dinamis lengkap","example":"00020101021226600016ID.CO.SHOPEE.PAY...540550570...6304F8B6"},"qrImageDataUrl":{"type":"string","description":"Base64 PNG gambar QR Code siap render","example":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAYAAADNkKWq..."},"qrSvgUrl":{"type":"string","description":"URL endpoint vektor SVG gambar QR","example":"/api/v1/orders/ord_b3489779-cbb3-4117-9279-3dde9fc71287/qr.svg"}}},"status":{"type":"string","enum":["PENDING","PAID","EXPIRED","CANCELLED"],"example":"PENDING"},"expiredAt":{"type":"string","description":"Waktu ISO 8601 kedaluwarsa (15 menit)","example":"2026-09-05T01:47:02.000Z"},"remainingSeconds":{"type":"integer","description":"Sisa detik pembayaran","example":900}}}}},"OrderDetailResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"id":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"clientReferenceId":{"type":"string","nullable":true,"example":"INV-RSVP-101"},"merchantId":{"type":"string","example":"mrc_cfe5f4b7-c029-4409-ba44-a76da3e1571e"},"pricing":{"type":"object","properties":{"originalAmount":{"type":"integer","example":50000},"uniqueCode":{"type":"integer","example":570},"totalAmount":{"type":"integer","example":50570}}},"qris":{"type":"object","properties":{"rawString":{"type":"string","example":"000201010212...540550570...6304F8B6"},"qrSvgUrl":{"type":"string","example":"/api/v1/orders/ord_b3489779/qr.svg"}}},"status":{"type":"string","enum":["PENDING","PAID","EXPIRED","CANCELLED"],"example":"PAID"},"paidAt":{"type":"string","nullable":true,"example":"2026-09-05T01:32:13.000Z"},"expiredAt":{"type":"string","example":"2026-09-05T01:47:02.000Z"},"remainingSeconds":{"type":"integer","example":887},"webhookStatus":{"type":"string","enum":["NONE","PENDING","DELIVERED","FAILED"],"example":"DELIVERED"}}}}},"SimulationPaymentResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Simulasi pembayaran berhasil diverifikasi"},"data":{"type":"object","properties":{"orderId":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"status":{"type":"string","example":"PAID"},"paidAt":{"type":"string","example":"2026-09-05T08:30:00.000Z"},"webhookDispatched":{"type":"boolean","example":true}}}}},"BridgeNotificationRequest":{"type":"object","required":["amount"],"properties":{"amount":{"type":"integer","description":"Nominal mutasi yang diterima dari notifikasi bank","example":50570},"sender":{"type":"string","description":"Nama pengirim mutasi","example":"Budi Santoso"},"rawText":{"type":"string","description":"Teks lengkap isi push notification","example":"Pembayaran QRIS Rp 50.570 dari Budi Santoso diterima"}}},"BridgeSuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Pembayaran berhasil dicocokkan dan diselesaikan"},"data":{"type":"object","properties":{"orderId":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"clientReferenceId":{"type":"string","example":"INV-RSVP-101"},"amount":{"type":"integer","example":50570},"status":{"type":"string","example":"PAID"}}}}},"BridgeUnmatchedResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Tidak ditemukan transaksi aktif yang cocok dengan nominal transfer tersebut"},"unmatchedLogId":{"type":"string","example":"unm_5748c9ba-cde0-4ccd-9b2c-d5c36e0ccd53"},"reason":{"type":"string","enum":["NO_MATCHING_ACTIVE_ORDER","ORDER_EXPIRED"],"example":"NO_MATCHING_ACTIVE_ORDER"}}},"OrderListResponse":{"type":"object","required":["success","data","pagination"],"properties":{"success":{"type":"boolean","example":true},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"clientReferenceId":{"type":"string","nullable":true,"example":"INV-RSVP-101"},"pricing":{"type":"object","properties":{"originalAmount":{"type":"integer","example":50000},"uniqueCode":{"type":"integer","example":570},"totalAmount":{"type":"integer","example":50570}}},"status":{"type":"string","enum":["PENDING","PAID","EXPIRED","CANCELLED"],"example":"PAID"},"paidAt":{"type":"string","nullable":true,"example":"2026-09-05T01:32:13.000Z"},"expiredAt":{"type":"string","example":"2026-09-05T01:47:02.000Z"},"webhookStatus":{"type":"string","enum":["NONE","PENDING","DELIVERED","FAILED"],"example":"DELIVERED"},"createdAt":{"type":"string","example":"2026-09-05T01:30:00.000Z"}}}},"pagination":{"type":"object","required":["page","limit","totalItems","totalPages"],"properties":{"page":{"type":"integer","example":1},"limit":{"type":"integer","example":20},"totalItems":{"type":"integer","example":48},"totalPages":{"type":"integer","example":3}}}}},"OrderCancelResponse":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Order tagihan berhasil dibatalkan dan kode unik dilepaskan"},"data":{"type":"object","required":["id","status","releasedUniqueCode"],"properties":{"id":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"status":{"type":"string","example":"CANCELLED"},"releasedUniqueCode":{"type":"integer","example":570}}}}},"OrderMarkPaidResponse":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Order tagihan berhasil ditandai lunas secara manual"},"data":{"type":"object","required":["id","status","paidAt","webhookDispatched"],"properties":{"id":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"clientReferenceId":{"type":"string","nullable":true,"example":"INV-RSVP-101"},"status":{"type":"string","example":"PAID"},"paidAt":{"type":"string","example":"2026-09-05T08:35:00.000Z"},"webhookDispatched":{"type":"boolean","example":true}}}}},"OrderResendWebhookResponse":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Webhook callback berhasil dikirim ulang ke server tujuan"},"data":{"type":"object","required":["orderId","destinationUrl","delivered","httpStatusCode"],"properties":{"orderId":{"type":"string","example":"ord_b3489779-cbb3-4117-9279-3dde9fc71287"},"destinationUrl":{"type":"string","example":"https://domain-rsvp.com/api/payment/callback"},"delivered":{"type":"boolean","example":true},"httpStatusCode":{"type":"integer","example":200}}}}},"HealthResponse":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"service":{"type":"string","example":"qr-fik-payment-gateway"},"database":{"type":"string","example":"qr-fik"},"timestamp":{"type":"string","example":"2026-09-05T08:00:00.000Z"}}}}},"paths":{"/health":{"get":{"tags":["System"],"summary":"System Health Diagnostics","description":"Returns the operational status of the microservice and database connectivity.","responses":{"200":{"description":"Server operating nominally","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/v1/merchants":{"post":{"tags":["Merchant"],"summary":"Register Merchant & Upload QRIS","description":"Registers a merchant record with static QRIS credentials. Protected by X-Admin-Secret header or adminSecret body property. Supports standard JSON payloads or multipart/form-data with QR image file uploads (PNG, JPEG, WebP) featuring automated QR text extraction.","security":[{"AdminSecretAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","qrisStaticRaw"],"properties":{"adminSecret":{"type":"string","description":"Admin passcode / invitation code","example":"qrfik_admin_2026"},"name":{"type":"string","example":"Fikri Store"},"qrisStaticRaw":{"type":"string","example":"00020101021126600016ID.CO.SHOPEE.PAY..."},"webhookUrl":{"type":"string","example":"https://domain-rsvp.com/api/payment/callback"}}}},"multipart/form-data":{"schema":{"type":"object","required":["name"],"properties":{"adminSecret":{"type":"string","description":"Admin passcode / invitation code","example":"qrfik_admin_2026"},"name":{"type":"string","example":"Fikri Store"},"qrisImage":{"type":"string","format":"binary","description":"Master static QRIS image file"},"qrisStaticRaw":{"type":"string","description":"Manual QRIS string if image upload is omitted","example":""},"webhookUrl":{"type":"string","example":"https://domain-rsvp.com/api/payment/callback"}}}}}},"responses":{"201":{"description":"Merchant registered successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MerchantResponse"}}}},"400":{"description":"Validation error or missing parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"},"example":{"success":false,"message":"Field 'name' wajib diisi"}}}},"403":{"description":"Unauthorized registration attempt (invalid or missing X-Admin-Secret)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Akses ditolak: Pendaftaran merchant baru memerlukan Kode Akses / Admin Secret yang valid (X-Admin-Secret)."}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal mendaftarkan merchant: Database connection error"}}}}}}},"/api/v1/merchants/me":{"get":{"tags":["Merchant"],"summary":"Retrieve Current Merchant Profile","description":"Fetches configuration details and credentials for the authenticated merchant.","security":[{"ApiKeyAuth":[]}],"responses":{"200":{"description":"Merchant profile retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MerchantProfileResponse"}}}},"401":{"description":"Missing or invalid merchant API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header X-Api-Key wajib diisi"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal memuat profil merchant"}}}}}}},"/api/v1/orders":{"get":{"tags":["Orders"],"summary":"List Orders & History","description":"Retrieves a paginated list of orders for the authenticated merchant with optional filtering by status or client reference ID.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Filter transactions by status","schema":{"type":"string","enum":["PENDING","PAID","EXPIRED","CANCELLED"]}},{"name":"clientReferenceId","in":"query","required":false,"description":"Filter by external client reference / invoice ID","schema":{"type":"string"}},{"name":"page","in":"query","required":false,"description":"Page number for pagination (defaults to 1)","schema":{"type":"integer","default":1,"minimum":1}},{"name":"limit","in":"query","required":false,"description":"Maximum records per page (defaults to 20, max 100)","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"Paginated list of transactions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderListResponse"}}}},"401":{"description":"Missing or invalid merchant API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header X-Api-Key wajib disertakan"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal memuat daftar riwayat order"}}}}}},"post":{"tags":["Orders"],"summary":"Generate Dynamic QRIS Order","description":"Creates a new transaction with an isolated 3-digit collision-free unique code, dynamic Tag 54 amount injection, and recalculated CRC16-CCITT checksum.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCreateRequest"}}}},"responses":{"201":{"description":"Order generated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"400":{"description":"Invalid request payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"},"example":{"success":false,"errors":{"amount":{"_errors":["Nominal harus berupa bilangan bulat positif"]}}}}}},"401":{"description":"Missing or invalid merchant API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header X-Api-Key wajib disertakan"}}}},"429":{"description":"Unique code capacity exhausted for this merchant window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Kapasitas transaksi saat ini penuh. Silakan coba kembali beberapa saat lagi."}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal membuat order tagihan"}}}}}}},"/api/v1/orders/{id}":{"get":{"tags":["Orders"],"summary":"Retrieve Order Status","description":"Retrieves the current settlement status and pricing breakdown for transaction polling.","parameters":[{"name":"id","in":"path","required":true,"description":"Order identifier (e.g., ord_b3489779...)","schema":{"type":"string"}}],"responses":{"200":{"description":"Current order details and payment state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDetailResponse"}}}},"404":{"description":"Transaction not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi tidak ditemukan"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal memuat status order"}}}}}}},"/api/v1/orders/{id}/qr.svg":{"get":{"tags":["Orders"],"summary":"Render Direct SVG QR Code","description":"Returns an image/svg+xml vector representation of the dynamic QRIS for direct embedding in frontend image tags.","parameters":[{"name":"id","in":"path","required":true,"description":"Order identifier","schema":{"type":"string"}}],"responses":{"200":{"description":"SVG vector image of the dynamic QR code","content":{"image/svg+xml":{"schema":{"type":"string"},"example":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 53 53\">...</svg>"}}},"404":{"description":"Transaction not found","content":{"text/plain":{"schema":{"type":"string"},"example":"Transaksi tidak ditemukan"}}},"500":{"description":"Internal server error","content":{"text/plain":{"schema":{"type":"string"},"example":"Gagal menghasilkan SVG"}}}}}},"/api/v1/orders/{id}/simulate-pay":{"post":{"tags":["Orders"],"summary":"Simulate Payment Settlement","description":"Development testing endpoint to verify status transition to PAID and outbound webhook dispatching without physical devices.","parameters":[{"name":"id","in":"path","required":true,"description":"Order identifier","schema":{"type":"string"}}],"responses":{"200":{"description":"Payment simulated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimulationPaymentResponse"}}}},"400":{"description":"Order was already settled previously","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi ini sudah lunas sebelumnya"}}}},"404":{"description":"Transaction not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi tidak ditemukan"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal menjalankan simulasi pembayaran"}}}}}}},"/api/v1/orders/{id}/cancel":{"post":{"tags":["Orders"],"summary":"Cancel Order & Release Unique Code","description":"Explicitly cancels a pending transaction and immediately releases the reserved 3-digit unique code for subsequent customer checkouts.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Order identifier","schema":{"type":"string"}}],"responses":{"200":{"description":"Order cancelled and unique code released","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCancelResponse"}}}},"400":{"description":"Cannot cancel an already PAID or CANCELLED order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi sudah berstatus PAID dan tidak dapat dibatalkan"}}}},"401":{"description":"Missing or invalid merchant API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header X-Api-Key wajib disertakan"}}}},"404":{"description":"Transaction not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi tidak ditemukan"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal membatalkan transaksi"}}}}}}},"/api/v1/orders/{id}/mark-paid":{"post":{"tags":["Orders"],"summary":"Manual Settlement Override","description":"Allows an authorized merchant to manually settle a pending transaction and trigger outbound webhook delivery when Android notification forwarding is delayed or failed.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Order identifier","schema":{"type":"string"}}],"responses":{"200":{"description":"Transaction marked as PAID and webhook dispatched","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderMarkPaidResponse"}}}},"400":{"description":"Order is already paid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi ini sudah berstatus PAID sebelumnya"}}}},"401":{"description":"Missing or invalid merchant API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header X-Api-Key wajib disertakan"}}}},"404":{"description":"Transaction not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi tidak ditemukan"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal menyelesaikan transaksi"}}}}}}},"/api/v1/orders/{id}/resend-webhook":{"post":{"tags":["Orders"],"summary":"Resend Outbound Webhook","description":"Re-dispatches the HMAC-signed webhook notification for a PAID order to the merchant's configured webhookUrl.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Order identifier","schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook re-dispatched to client receiver","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResendWebhookResponse"}}}},"400":{"description":"Order not paid or webhook URL unconfigured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Webhook hanya dapat dikirim ulang untuk transaksi yang telah berstatus PAID"}}}},"401":{"description":"Missing or invalid merchant API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header X-Api-Key wajib disertakan"}}}},"404":{"description":"Transaction not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Transaksi tidak ditemukan"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Gagal mengirim ulang webhook"}}}}}}},"/api/v1/bridge/webhook":{"post":{"tags":["Bridge"],"summary":"Inbound Android Bridge Webhook","description":"Receives parsed payment mutation notifications dispatched by the Android notification listener device. Matches the amount against active pending orders and confirms settlement.","security":[{"BridgeSecretAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeNotificationRequest"}}}},"responses":{"200":{"description":"Payment matched and order settled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeSuccessResponse"}}}},"400":{"description":"Invalid notification payload structure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"},"example":{"success":false,"errors":{"amount":{"_errors":["Nominal harus berupa bilangan bulat positif"]}}}}}},"401":{"description":"Invalid or missing bridge secret token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Header otentikasi X-Bridge-Secret wajib diisi"}}}},"404":{"description":"No matching active pending order found (logged to unmatched_logs)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeUnmatchedResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"success":false,"message":"Terjadi kesalahan saat memproses webhook bridge"}}}}}}}}}