--- title: "GoPay Merchant QRIS gateways — how a merchant account becomes an unofficial payment gateway" description: "Reverse-engineering audit of three open-source repos (gopay-api-gateaway, gobiz-payment, QrisMerchantID): how a GoPay/GoBiz merchant's own QRIS + private portal APIs are automated into a self-hosted 'payment gateway' — dynamic QRIS via EMVCo TLV, payment confirmation by polling the merchant's own transaction feed, and the HAR-capture/APK-analysis methods used to reverse engineer it. Pinned at Oct 2026 commits." status: concluded category: technical tags: [gopay, gobiz, qris, payment-gateway, reverse-engineering, emvco, har-capture, apk-analysis, api-replay, indonesia, fintech, unofficial-api, gojek] draft: true created: 2026-10-15 updated: 2026-10-15 --- # GoPay Merchant QRIS gateways **Research question:** How do tools like `gopay-api-gateaway`, `gobiz-payment`, and `QrisMerchantID` turn a GoPay/GoBiz merchant account into a "payment gateway API" — how does the mechanism actually work, and how were the underlying APIs reverse engineered? **Short answer [verified].** There is no fake gateway and no money-routing trick. All three repos automate **two things the merchant can already do with their own account**: (1) **render** a dynamic QRIS for an order — done fully offline by editing the merchant's own static QRIS string (EMVCo TLV: inject the amount into tag 54, recompute CRC16); and (2) **detect** the matching incoming payment by polling the merchant's own transaction feed through GoBiz's private web-portal API, authenticated as the merchant. Settlement still happens on GoPay's normal QRIS rails (buyer's wallet/bank app → merchant's GoPay/GoBiz balance). The "gateway" is presentation + reconciliation automation around the merchant's own account — which is why it works without any licensed payment provider (PJP), and why it sits in a ToS/compliance gray zone. **How it was reverse engineered [verified].** Mostly *gray-box*, not exploits: the APIs were recovered by capturing the merchant's own web-portal sessions (HAR exports shared between researchers), replaying them request-by-request ("mirror the browser, byte-for-byte"), reading older open-source repos, and extracting endpoint inventories from the public Android APKs (apktool/androguard/strings on the Flutter app). The QR half needed no reverse engineering at all — EMVCo QRIS is a public spec. The ongoing maintenance burden is **client-version drift**: the portal appears to gate requests on an `X-AppVersion` build string and has moved its auth endpoints, so replay clients must track portal releases (observed sequence v3.107 → v3.119 → v3.122 → v3.125 between Jun–Oct 2026). ## Repos audited | Repo | What it is | Lang · license | Stars (captured 2026-10-15) | Audit note | |---|---|---|---|---| | [S3] `ahmadzakiyox/gopay-api-gateaway` | Self-hosted Express gateway: REST API + hosted QRIS checkout page | Node.js · MIT | 233 ★ / 84 forks | `server.js` readable; `login.js` + `sessionManager.js` **obfuscated** | | [S2] `kavionn/gobiz-payment` | Library: `GoPayMerchant` client + polling watcher | Node.js · MIT | 73 ★ / 29 forks | Fully readable; primary code reference | | [S1] `AlfinAI/QrisMerchantID` | Python SDK + **the public RE knowledge base** (APK analyses, HAR verification reports, reproduction guide) | Python · MIT | 12 ★ / 2 forks | Rich documentation; parts explicitly runtime-unverified | ## Key findings 1. **[verified] The "gateway" never touches money.** It renders QRIS (offline string edit) and confirms payments (own-account polling). No PJP, no acquiring, no fund flow. [S6] 2. **[verified] Auth = GoID portal replay.** OTP or email+password against `/goid/login/request` + `/goid/token` with copied browser fingerprint headers; JWE bearer tokens, cached on disk and re-obtained on expiry. [S7][S10] 3. **[verified] Payment confirmation = polling the merchant's own feed.** `GET api.gojekapi.com/merchant-analytics/v2/merchants/transactions` (primary) or `POST api.gobiz.co.id/journals/search` (fallback/detail), every ~6 s while a checkout is active; match amount + time; dedupe by transaction id. [S2][S6] 4. **[verified] Anti-double-claim is a known hard problem** (two buyers paying the same nominal). Implemented: claim scoping by a unique `trx_id` + dedupe by `transaction_id`. Recommended additionally: add a unique Rp1–99 code to the billed amount. [S3][S6] 5. **[verified] Reverse engineering was HAR-driven.** Four captured sessions are documented (up to 80 MB / 1063 entries each); replay clients were diffed against capture data header-by-header. Live-credential HARs are treated as crown jewels: gitignored, never committed, masked in reports. [S6][S7] 6. **[verified] The APK analysis is a map of what's possible, not what's used.** ~250 endpoints were inventoried statically from GoPay Merchant 2.3.0 + GoFood Merchant 5.49.0; almost all are marked *unverified at runtime*. The subset actually in use is the HAR-verified portal API above. [S8][S12] 7. **[reported] Version drift breaks clients regularly.** Documented fixes: phone normalisation (bare national digits only), dropping the `login_type` field, moving auth calls to the portal host, and repeated `X-AppVersion` bumps. Issue trackers show OTP delivery and empty-history reports — maintenance is ongoing. [S7][S13][S14] 8. **[verified] Obfuscation sits exactly where it hurts.** The most popular gateway hides precisely the OTP login + session-refresh code (built with `javascript-obfuscator`), so what it does with the merchant's OTP, credentials, and tokens **cannot be audited from source**. [S3] ## Confidence labels used in this report - **[verified]** — read directly in pinned source code, or corroborated by a documented live capture. - **[reported]** — asserted by a source (repo author, capture report); plausible and consistent, not independently confirmed by us. - **[unverified]** — explicitly flagged as untested/unconfirmed in the sources themselves. - **[inference]** — this report's own analysis, not a source claim. ## Files - [[gopay-merchant-qris-gateway/analysis|Analysis — how the mechanism works]] - [[gopay-merchant-qris-gateway/reverse-engineering|Reverse engineering — methods & drift playbook]] - [[gopay-merchant-qris-gateway/repos|Repo audits & comparison]] - [[gopay-merchant-qris-gateway/security|Security, legal & operational risks]] - [[gopay-merchant-qris-gateway/sources|Sources & citations]] ## Open questions 1. **[unverified] Unit-handling discrepancy across implementations:** `gobiz.js` and `QrisMerchantID` both divide `gross_amount` by 100 (minor units, i.e. sen), while the gateway's `verifyPayment()` compares the raw field against a rupiah input with no normalisation. Either it was validated against a different response shape, or its `/check-payment` never matches. Resolvable only with a live merchant account. Details in [[gopay-merchant-qris-gateway/analysis|analysis]] §4 (Money units). 2. **[unverified] App-side protocol remains traffic-unverified:** the GoPay Merchant app's own API set (realtime WebSocket `raccoon` events, `qris-link` charge endpoints) is inventoried from static analysis only; a realtime (non-polling) watcher on the app protocol is the obvious next build for anyone with a device lab. [S8][S15] 3. **[unverified] Request signing:** no static signing secret exists client-side and no signing was observed on the web path; whether app-side calls add per-request signatures is unknown. [S15] 4. **[reported] Suspension triggers:** community threads tie account bans to aggressive polling and to transaction spikes; neither is confirmed against any GoPay statement. [S4][S14]