Flow Overview
1. Register a static QRIS
Section titled “1. Register a static QRIS”Upload the merchant’s real static QRIS (image or raw EMV string) via
POST /v1/qris-codes — once per mode, using that mode’s key. You get back a
qris_id; reuse it for every transaction in that same mode. Live and sandbox never share a
registration, even for the same physical QRIS — generating in sandbox needs a qris_id uploaded
with the sandbox key, live needs one uploaded with the live key. This is deliberate: it’s what
lets sandbox-generated QR codes be made non-payable (see
Known limitations) without
touching live at all.
2. Generate a transaction
Section titled “2. Generate a transaction”POST /v1/qris/generate with qris_id and amount. The response includes
qris_string (render this as the QR code shown to the payer) and history_id (use this to track
the transaction). A random offset (1–999) is added to the amount so payments can be matched
uniquely; the transaction defaults to expiring in 15 minutes. qris_id must belong to the same
mode as the API key used, or you get QRIS_NOT_FOUND.
3. Payer scans and pays
Section titled “3. Payer scans and pays”The payer pays the amount shown. In live mode, confirmation comes from the
App Listener reading the merchant’s payment app notification and calling
the inbound webhook. In sandbox, call
POST /v1/qris-history/:id/simulate-payment instead — no
real payment needed.
4. Get notified
Section titled “4. Get notified”The moment a transaction is marked paid, a signed webhook (payment.success) is sent to your
configured webhook_url — see Webhooks. You can also poll
GET /v1/qris-history/:id at any time; it’s the source of truth if a
webhook delivery is delayed or lost.
Transaction lifecycle
Section titled “Transaction lifecycle” ┌────────┐ │ unpaid │ └───┬────┘ ┌──────────────┴──────────────┐ │ payment confirmed │ expires_at elapses ▼ ▼ ┌──────┐ ┌─────────┐ │ paid │◀───────────────────│ expired │ └──────┘ late payment └─────────┘ (only if allow_payment_after_expiry was set at generate time — otherwise expired is terminal)expired is set within 30 seconds by a background sweep, or immediately if the transaction
happens to be read (GET) first. See Known limitations for what
expired does — and does not — prevent.
Continue to QRIS Codes →