Skip to main content

QrisMerchantID — Unofficial Indonesian QRIS merchant API client for Python

Tests PyPI License: MIT

One Python package for Indonesia's QRIS merchant APIs. Provider lineup:

Provider Status Scope
GoPay / GoBiz merchant ✅ auth, users, merchants, transactions, payouts, QRIS, watcher login (password + OTP), merchants, transactions, payouts, QRIS helpers, payment watcher
ShopeePay partner 🗺️ roadmap (FASE B) login, stores, transactions — see research/RESEARCH_GOPAY_SHOPEEPAY.md §4

Research/educational use only. Not affiliated with GoTo/GoPay/GoBiz or Shopee/Sea Group. Read-only by design in v0.1.0 — it only reads your own merchant data (login + history + payouts) and never moves money.

⚠️ Disclaimer — harap dibaca dulu

English. This is an unofficial, independent research project. It is NOT affiliated with, endorsed by, or supported by GoTo, GoPay, GoBiz, Shopee, Sea Group, or any reference-repo author. It is provided for research and educational purposes only, without warranty of any kind. Using unofficial APIs may violate the providers' Terms of Service and can lead to rate limits, suspension, or termination of your accounts. You use this software entirely at your own risk — the author (AlfinAI) shall not be liable for any loss, damage, account action, or legal consequence arising from its use. Credentials and tokens you enter stay on your machine (they are only ever sent to the providers' own official servers) — never commit .env, *.har, or token/OTP cache files to any repository.

Bahasa Indonesia. Ini adalah proyek riset independen yang tidak resmi (unofficial). TIDAK berafiliasi, didukung, atau disetujui oleh GoTo, GoPay, GoBiz, Shopee, Sea Group, maupun author repo referensi mana pun. Disediakan hanya untuk riset dan edukasi, tanpa jaminan apa pun. Penggunaan API tidak resmi dapat melanggar Syarat & Ketentuan penyedia dan berakibat akun dibatasi, ditangguhkan, atau dihapus. Segala risiko dan akibat yang timbul sepenuhnya menjadi tanggung jawab pengguna — author (AlfinAI) tidak bertanggung jawab atas kerugian, kerusakan, tindakan terhadap akun, atau konsekuensi hukum apa pun dari penggunaan software ini. Kredensial/token hanya tersimpan di mesin Anda (dan hanya dikirim ke server resmi penyedia) — jangan pernah commit file .env, *.har, atau cache token/OTP ke repo mana pun.

Contents

Installation

pip install QrisMerchantID

Requires Python 3.10+ and one dependency: httpx.

Core concepts

Three things to understand before anything else:

  1. Amounts are minor units (sen). Analytics answers ints (gross_amount: 10600000), payouts answer decimal strings ("10600000.0") — both mean Rp106.000. Convert with money.to_rupiah(); never guess.
  2. Sessions carry no server expiry. /goid/token answers {access_token, refresh_token} with no expires_in, so the SDK caches the session and you revalidate cheaply (merchants.search()); on HTTP 401, log in again.
  3. Every HTTP error raises ApiException. It carries .http_status, .code (when the provider sent one), .payload (full body), and a readable message. Transport errors (DNS/connect/timeout) retry with backoff; HTTP errors never retry.

GoPay guide

Everything lives on one facade sharing a single client:

from qrismerchantid import GoPayMerchant

gopay = GoPayMerchant()
gopay.auth          # login: password + OTP
gopay.users         # portal profile
gopay.merchants     # search + detail
gopay.transactions  # analytics + journals + issuer breakdown
gopay.payouts       # payout history + payable balance
gopay.watch(...)    # payment watcher factory

1. Login — OTP & password

GoBiz (GoID) login answers HTTP 201 on success. Two flows:

# --- OTP (recommended: no password stored anywhere) ---
otp = gopay.auth.request_otp("0812xxxxxxx")  # SMS, 4 digits, ~12 min window
# otp -> {"otp_token", "otp_expires_in", "otp_length", "next_state"}
session = gopay.auth.login_with_otp(input("OTP: "), otp["otp_token"])

# --- Password (verified against reference code; live re-check is TODO-R1) ---
session = gopay.auth.login_with_password("you@shop.id", "secret")

session  # -> {"access_token", "refresh_token", ...} — also set on the client

Notes (all verified against a live portal capture, research §9):

  • request_otp() sends no login_type field — the portal doesn't either. (Some reference repos send one; the server ignores it.)
  • Phone numbers may carry spaces/dashes ("0812 345-678" is normalized); country_code defaults to "62".
  • Keep otp_token server-side between the two calls (or token_cache.save_pending_otp()); it expires with the code.

2. Session cache

Skip OTP on the next run by caching the session to disk:

from qrismerchantid.core import token_cache

token_cache.save(".gopay-session.json", session)

session = token_cache.load(".gopay-session.json")  # None if missing/invalid
gopay = GoPayMerchant(access_token=session["access_token"]) if session else GoPayMerchant()

Recommended loop for long-running gateways: load cache → cheap merchants.search() probe → on 401, login again and re-save. See examples/02_merchants.py.

3. Users & merchants

me = gopay.users.me()
# -> {"user": {"id", "email", "full_name", "phone", "roles", "scopes", ...}}

found = gopay.merchants.search()            # {"total", "success", "hits"}
found = gopay.merchants.search(from_=40, size=5)
merchant_id = found["hits"][0]["id"]        # IDs look like "G…"

detail = gopay.merchants.detail(merchant_id)
# -> full object: KYC, outlet, bank, active_payment_channels, payment_settings…

search() doubles as the token-validity probe used by every reference gateway.

4. Transactions

The primary feed is merchant-analytics (query shape verified 1:1):

txns = gopay.transactions.analytics(merchant_id, days=7)
txns = gopay.transactions.analytics(
    merchant_id, start_time="2026-09-01T00:00:00.000Z", end_time="2026-09-08T00:00:00.000Z"
)
txns = gopay.transactions.analytics(["G111", "G222"], days=1, size=50)  # multi-outlet
# -> {"from", "size", "total", "transactions": [...]}

Each transaction carries 23 keys, including order_id (QRIS-…), transaction_status (SETTLEMENT/CAPTURE/REFUND/PARTIAL_REFUND), payment_type, channel_type (STATIC_QR), transaction_source (GOPAY_INSTORE), qris_provider_aspi_issuer/…_acquirer, shares, promo_details. Defaults mirror the portal exactly (statuses=SETTLEMENT,…, payment_types=QRIS,GOPAY,…) and are overridable.

Two more reads on /journals/search (special journal headers handled for you):

journal = gopay.transactions.journals(merchant_id, start, end, size=50)
by_issuer = gopay.transactions.qris_issuer_breakdown(start, end)
# -> {"aggregations": {"by_qris_issuer": {"buckets": [...]}}}

5. Payouts

HAR-discovered endpoints — absent from every reference repo:

page = gopay.payouts.list()              # ?page=1&per=10
page = gopay.payouts.list(page=2, per=25)
# -> {"payouts": [{"payout_id", "net_amount", "gross_amount", "status": "paid",
#      "paid_at", "account_no", ...}], "current_page", "per", "next_page"}

payable = gopay.payouts.payable_detail(merchant_id)
# -> {"payable_detail": {"payable", "net_amount", "total_settlement", fees…}}

Amounts here are decimal strings in minor unitsto_rupiah("11610000.0").

6. QRIS dynamic

Pure offline helpers (qrismerchantid.gopay.qris): parse a static QRIS as EMVCo TLV, set tag 54 to the bill, recompute CRC16-CCITT, done:

from qrismerchantid.gopay import qris

qris.get_tag(static_qris, "59")          # merchant name, e.g. "NUXYS STORE"
dynamic = qris.inject_amount(static_qris, 50000)  # Rp50.000, CRC valid
qris.get_tag(dynamic, "54")              # "50000"

Render dynamic with any QR library (qrcode, segno) and display it at checkout. Anti double-claim (when two buyers pay the same nominal at once): add a unique code (Rp1–99) to the bill and dedupe by the watcher's transaction_id/order_id on your side — the same recipe every gateway uses.

7. Payment watcher

The classic gateway loop, ported from gobiz.js: seed → poll → match nominal:

watcher = gopay.watch(merchant_id)  # poll_interval=6.0 like the references
watcher.seed()                      # mark current history as seen, returns count

paid = watcher.wait_for_payment(5_000_000, timeout=300)  # Rp50.000 in minor units
paid = watcher.wait_for_payment(5_000_000, timeout=300, tolerance=100)
# -> raw transaction dict; raises TimeoutError when the invoice lapses

Lower-level: watcher.poll_once() returns only never-seen transactions (seen cache capped at 500). Polling etiquette: keep the 6s interval, poll only while a checkout is active — aggressive polling is how accounts get rate-limited.

8. Configuration

gopay = GoPayMerchant(
    access_token="...",
    timeout=30.0,       # seconds
    max_retries=2,      # transport errors only — never HTTP errors
    backoff_base=0.5,   # exponential: 0.5s, 1s, 2s, ...
    app_version="platform-v3.119.0-eab7f749",  # default follows the analyzed portal
    user_agent="...",
)

Need HTTP/2, a proxy, or a custom CA? Pass your own transport:

import httpx
from qrismerchantid.core.transport import HttpxTransport

transport = HttpxTransport(httpx.Client(http2=True, proxy="http://localhost:8080"))
gopay = GoPayMerchant(transport=transport)

Development

pip install -e ".[dev]"
pytest          # 100% offline — never hits the real API
ruff check src tests && ruff format --check src tests
mypy src        # strict
python -m build

Runnable flows (need real merchant credentials via env, except QRIS): examples/01_login_otp.py, 02_merchants.py, 03_transactions.py, 04_qris_dynamic.py, 05_watch_payment.py.

Research

Full endpoint research (repos surveyed + anonymized HAR verification): research/RESEARCH_GOPAY_SHOPEEPAY.md. Per-service pages: docs/gopay/.

Credits

API knowledge: kavionn/gobiz-payment, warungerik/API-GOPAY-MERCHANT, alhifnywahid/merchantid, ahmadzakiyox/gopay-api-gateaway, ahmadzakiyox/shoppepay-api-gateway, namtxs/gopay-api. Python package maintained by AlfinAI.

License

MIT — see LICENSE.

Release files for QrisMerchantID 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for QrisMerchantID 0.1.0
File Size Uploaded
qrismerchantid-0.1.0.tar.gz 25.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for QrisMerchantID 0.1.0
File Interpreter ABI Platform
qrismerchantid-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 49.7 kB

Release files / qrismerchantid-0.1.0.tar.gz

Download URL qrismerchantid-0.1.0.tar.gz
Size 25.4 kB
Tags Source
SHA-256 checksum
How to use checksums
16b9ab9c576c81689e6ac6ca2190251aae6dd9965e3f800d893eb974bd276c27
BLAKE2b-256 checksum
How to use checksums
82d7a8f2f81828ff28cd9a81476268dc05f36c88ce1b81ad1aa489dd55437694
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release files / qrismerchantid-0.1.0-py3-none-any.whl

Download URL qrismerchantid-0.1.0-py3-none-any.whl
Size 24.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b2fb82e5165650fbd4901523de241ebc183846e6ceca641c76dcea83b9a166d1
BLAKE2b-256 checksum
How to use checksums
cb266ba163d2428d0037501f55c0b398ebbb039aa2b3424494b04cad68a9f496
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page