# Dynamic QRIS Payment Integration in Node.js: The Complete Guide

> Static vs dynamic QRIS, generating a dynamic QR through a licensed PSP, the MPM flow, webhook handling, NMID onboarding, and the October 2026 MDR rates — a hands-on Node.js guide.

URL: https://www.matthewswong.com/en/blog/qris-dynamic-payment-integration-nodejs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-07-14
- Updated: 2026-10-12
- Category: Backend

> **Key takeaway:** A dynamic QRIS embeds the amount in a fresh QR per transaction, generated through a licensed Indonesian PSP's API and confirmed by a signed webhook. From 1 October 2026, Bank Indonesia sets MDR at 0 percent up to Rp100,000 for regular merchants (0.7 percent above) and up to Rp500,000 for micro merchants (0.3 percent above).

On a recent project I had to add QRIS to an Indonesian checkout, and the first thing that tripped up the team was assuming QRIS is one thing. It is not. There is a static flavor you print on a sticker and a dynamic flavor you generate on the fly from your backend. For anything resembling an app or an online store, you want dynamic QRIS, and this post walks through exactly how to build that in Node.js.

QRIS (Quick Response Code Indonesian Standard) is Bank Indonesia's unified QR standard: one QR that any registered e-wallet or mobile banking app (GoPay, OVO, DANA, ShopeePay, bank apps) can scan and pay. As a developer you almost never talk to the QRIS network directly. You go through a licensed PSP, and your real work is generating the QR, rendering it, and reacting to a webhook when the money lands.

## Static vs dynamic QRIS: which one do you need?

The difference matters for both UX and reconciliation. With a static QR the customer scans your one printed code and types the amount themselves — fine for a warung, painful for software because you cannot reliably tie a payment back to a specific order. A dynamic QR is minted per transaction with the amount locked in, and it carries an order reference your webhook can match. Here is how they compare.

| Aspect | Static QRIS | Dynamic QRIS |
| --- | --- | --- |
| Amount | Customer types it in manually | Embedded and locked at generation |
| QR lifetime | One printed QR reused forever | Fresh QR per transaction, expires |
| Reconciliation | Manual, hard to match order to payment | Automatic via order_id in the webhook |
| Best for | Warungs, tip jars, low volume | Apps, e-commerce, checkout with an order |
| Integration effort | Print a sticker, no code | API call plus a webhook handler |

## Generating dynamic QRIS through a licensed PSP

You cannot legally mint a valid QRIS from scratch. Every QR must embed a real NMID and be signed into Bank Indonesia's national switching network, which only licensed acquirers and PJSPs can do. So the architecture is always the same: your backend calls a PSP, the PSP returns a QR, and the PSP settles funds to your account minus MDR. These are the licensed PSPs Indonesian developers usually reach for.

- Midtrans (Core API and Snap) — widest e-wallet coverage, strong Node.js SDK
- Xendit — clean API, popular with startups and fintech
- DOKU — long-established, strong on enterprise and biller use cases
- Direct bank APIs (BRI, BCA, Mandiri) — if you already bank with them and want lower fees at higher volume

## NMID and merchant onboarding

Before a single QR works you have to be onboarded as a merchant. Your PSP collects your business documents (KTP and NIK for individuals, NPWP and deed of establishment for companies), verifies legality, and your acquirer issues an NMID — the National Merchant ID that is baked into every QR you present and links each payment to your business in the national merchant repository. How long issuance takes depends on your PSP and acquirer, so ask for their current turnaround before you commit to a launch date. Nothing you build will transact until this finishes, so start onboarding early.

## The MPM flow: generating a dynamic QR in Node.js

Dynamic QRIS uses MPM — Merchant Presented Mode — meaning the merchant presents the QR and the customer scans it (the opposite is CPM, where the customer shows their app's QR to a scanner). The MPM flow in code is three steps: charge, render, confirm. With Midtrans Core API and their Node.js SDK, generating the QR is a single charge call with payment_type set to qris.

> The charge response returns an actions array. Grab the generate-qr-code action URL and render that image, or embed the QR string in your own frontend. Also store transaction_id and the expiry you set. A dynamic QR expires and each provider sets its own default: Midtrans's GoPay QRIS guide gives 15 minutes, adjustable with custom_expiry, so set the expiry explicitly or check your gateway's default, then show a countdown and a regenerate button rather than a silently dead QR.

## Handling the payment callback/webhook

This is where most integrations quietly break. The customer's app says paid, but your backend only truly knows when the PSP posts a webhook to your notification URL. Configure that URL in the PSP dashboard, expose a public HTTPS endpoint, and — critically — verify the signature before trusting anything. Midtrans signs each notification as an SHA-512 hash of order_id, status_code, gross_amount, and your server key. The identifier of the QR being paid arrives as merchant_cross_reference_id.

> Never mark an order paid from the frontend or a redirect — attackers can forge both. Trust only a webhook whose signature key matches your recomputed hash, and make the handler idempotent: PSPs retry, so the same settlement can arrive several times. Respond 200 fast and do heavy work asynchronously, or the PSP keeps retrying and duplicates your side effects.

## The MDR fee structure

Finally, the money. MDR (Merchant Discount Rate) is the fee your PSP deducts from each settlement — you receive the gross amount minus MDR. Bank Indonesia sets the rates, and they changed again on 1 October 2026, when the 0% band was extended to small, medium and large merchants for transactions up to Rp 100,000. BI's table also sets special rates of 0.6% for education and 0.4% for public fuel stations (SPBU), which BI's August 2026 announcement does not mention separately. Verify the current numbers against Bank Indonesia before quoting them to a client, because these are policy-driven and shift. As of 1 October 2026:

| Merchant category | MDR (from 1 Oct 2026) |
| --- | --- |
| Micro (UMI), transaction up to Rp 500,000 | 0% (free) |
| Micro (UMI), transaction above Rp 500,000 | 0.3% |
| Small, medium, large (UKE, UME, UBE) | 0% up to Rp 100,000; 0.7% above |
| Public service, G2P, P2G, donations | 0% |

> Two practical notes: MDR is legally borne by the merchant and cannot be surcharged to the customer, so price it into your margins, not the checkout total. And build reconciliation expecting net settlement — log gross_amount from the webhook, subtract the MDR tier, and reconcile against what actually hits your bank so a 0.7 percent gap never looks like a missing payment.

## Sources

- Bank Indonesia — Merchant Discount Rate (MDR) QRIS
- Midtrans Docs — QRIS Core API charge reference
- Midtrans Docs — HTTP(S) Notification / Webhooks
- Bank Indonesia (BICARA 131) — Identifying a QRIS issuer via the NSS code / NMID
- Midtrans Docs — GoPay QRIS POS integration (default expiry and custom_expiry)

## Source links

- https://www.bi.go.id/id/publikasi/ruang-media/cerita-bi/Pages/mdr-qris.aspx
- https://docs.midtrans.com/reference/qris
- https://docs.midtrans.com/docs/https-notification-webhooks
- https://bicara131.bi.go.id/knowledgebase/article/KA-01154/en-us
- https://docs.midtrans.com/docs/gopay-qris-pos-integration

## FAQ

### What is the difference between static and dynamic QRIS?

A static QRIS is a single printed QR that every customer scans and then types the amount into manually. A dynamic QRIS is generated per transaction with the amount already embedded, so the customer just confirms and pays. Dynamic QRIS also lets your backend auto-reconcile each payment against an order ID.

### Can I generate a dynamic QRIS myself without a payment provider?

In practice, no. QRIS codes must carry a valid NMID issued by a Bank Indonesia licensed acquirer or PJSP, and the QR must be signed into the national switching network. You generate dynamic QRIS by calling a licensed PSP's API such as Midtrans, Xendit, or DOKU, which handles the NMID and settlement for you.

### How much is the QRIS MDR fee in 2026?

From 1 October 2026, Bank Indonesia sets QRIS MDR at 0 percent for transactions up to Rp 100,000 at small, medium and large merchants, and 0.7 percent above that. Micro merchants pay 0 percent up to Rp 500,000 and 0.3 percent above. Education is 0.6 percent and fuel stations (SPBU) 0.4 percent, while public service, government and donation categories are 0 percent. The fee is borne by the merchant and cannot be passed to the customer.

### How do I know a QRIS payment actually succeeded?

You confirm payment through the webhook (HTTP notification) your PSP sends to your server when the status changes to settlement. Always verify the signature key before trusting it, and never rely on the customer's screen or the frontend alone. Treat the webhook as the single source of truth and make its handler idempotent.

### What is an NMID in QRIS?

NMID (National Merchant ID) is a unique merchant identifier issued by your acquirer and embedded in every QRIS code you present. It ties each transaction back to your registered business in Bank Indonesia's national merchant repository. When you integrate through a PSP, the NMID is provisioned during merchant onboarding, and how long that takes depends on your PSP and acquirer, so ask before you set a launch date.

---

- HTML version: https://www.matthewswong.com/en/blog/qris-dynamic-payment-integration-nodejs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/qris-dynamic-payment-integration-nodejs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
