research/gopay-merchant-qris-gateway/index.md

7.5 KiB
Raw Blame History

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
gopay
gobiz
qris
payment-gateway
reverse-engineering
emvco
har-capture
apk-analysis
api-replay
indonesia
fintech
unofficial-api
gojek
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

  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

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 §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]