7.5 KiB
| title | description | status | category | tags | draft | created | updated | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GoPay Merchant QRIS gateways — how a merchant account becomes an unofficial payment gateway | 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. | concluded | technical |
|
true | 2026-10-15 | 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
- [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]
- [verified] Auth = GoID portal replay. OTP or email+password against
/goid/login/request+/goid/tokenwith copied browser fingerprint headers; JWE bearer tokens, cached on disk and re-obtained on expiry. [S7][S10] - [verified] Payment confirmation = polling the merchant's own feed.
GET api.gojekapi.com/merchant-analytics/v2/merchants/transactions(primary) orPOST 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] - [verified] Anti-double-claim is a known hard problem (two buyers paying the same nominal). Implemented: claim scoping by a unique
trx_id+ dedupe bytransaction_id. Recommended additionally: add a unique Rp1–99 code to the billed amount. [S3][S6] - [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]
- [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]
- [reported] Version drift breaks clients regularly. Documented fixes: phone normalisation (bare national digits only), dropping the
login_typefield, moving auth calls to the portal host, and repeatedX-AppVersionbumps. Issue trackers show OTP delivery and empty-history reports — maintenance is ongoing. [S7][S13][S14] - [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
- gopay-merchant-qris-gateway/reverse-engineering
- gopay-merchant-qris-gateway/repos
- gopay-merchant-qris-gateway/security
- gopay-merchant-qris-gateway/sources
Open questions
- [unverified] Unit-handling discrepancy across implementations:
gobiz.jsandQrisMerchantIDboth dividegross_amountby 100 (minor units, i.e. sen), while the gateway'sverifyPayment()compares the raw field against a rupiah input with no normalisation. Either it was validated against a different response shape, or its/check-paymentnever matches. Resolvable only with a live merchant account. Details in gopay-merchant-qris-gateway/analysis §4 (Money units). - [unverified] App-side protocol remains traffic-unverified: the GoPay Merchant app's own API set (realtime WebSocket
raccoonevents,qris-linkcharge 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] - [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]
- [reported] Suspension triggers: community threads tie account bans to aggressive polling and to transaction spikes; neither is confirmed against any GoPay statement. [S4][S14]