Skip to content

IDR via NICEPAY (QRIS / VA)

For uncle-z products selling to Indonesian buyers in IDR. NICEPAY is the aggregator; uncle-z holds one merchant account, all products share it.

MethodWhat buyer doesWallets / banks
qrisScans a QR code with their wallet appGoPay, OVO, DANA, ShopeePay, BCA Mobile, anything QRIS-compatible
va_bcaTransfers from BCA mobile/internet bankingBCA only
va_mandiriTransfers from MandiriMandiri only
va_bniTransfers from BNIBNI only
va_briTransfers from BRIBRI only
va_permataTransfers from PermataPermata only

qris is universal — any wallet works. VA is bank-specific — only customers using that exact bank can pay.

Funds flow: buyer wallet → NICEPAY merchant balance → uncle-z’s registered bank account on T+1 (or T+2 across weekends/holidays). NICEPAY batches all the day’s transactions into one settlement. Operationally instant for the cashier (webhook fires within seconds); literal bank credit is next business day.

There’s no way to skip the T+1 hop without becoming a direct PJP with a specific bank — covered briefly in When to use this gateway. Not worth it at our scale.

POST /v1/payments
X-PAY-Key: pk_<your app's public key>
X-PAY-Timestamp: <unix seconds>
X-PAY-Signature: <hex hmac>
Content-Type: application/json
{
"external_user_id": "<your user id>",
"email": "buyer@example.com",
"amount_minor": 50000,
"currency": "IDR",
"method": "qris",
"return_url": "https://<your-domain>/payment/return?id=...",
"invoice_prefix": "POS"
}

amount_minor is rupiah for IDR. IDR has no minor unit — 50000 means Rp 50,000, not Rp 500.00. Watch for this — it’s the most common integration bug.

Required quirks:

  • email is required. NICEPAY rejects QRIS / VA without billingEmail (error 9018). The gateway forwards your email field to that. Always send it.
  • userIP is filled by the gateway from your client’s X-Forwarded-For first hop. NICEPAY rejects without it (error 9024). If you’re behind an unusual proxy chain, talk to ops.

The response gives you checkout_url. For QRIS, it’s NICEPAY’s hosted page that displays the QR; for VA, it’s a page that displays the VA number + bank instructions.

Two flow shapes depending on your product:

Pop the URL in a new tab or iframe. Buyer pays. NICEPAY redirects them back to your return_url. You verify completion via webhook (preferred) or by polling GET /v1/payments/{id}.

Same shape — the cashier device shows the URL in an embedded webview / iframe, buyer scans the QR with their wallet, cashier sees “Paid” via webhook within seconds. See POS retail flow.

20 minutes hard cap, in WIB (Asia/Jakarta) timezone — NICEPAY’s server checks the expiry against its local clock, NOT UTC. The gateway formats the expiry correctly; you don’t have to handle this. But if you build a “pay this QR” UI, design it for buyers who walk away and come back: the QR will be expired after 20 minutes and they need a fresh one (new POST /v1/payments).

Default 24 hours from issuance. The gateway sets vacctValidDt/vacctValidTm automatically. Buyers who don’t pay within 24h get an expired VA; no charge possible without a new request.

Native QR rendering on POS devices (advanced)

Section titled “Native QR rendering on POS devices (advanced)”

By default, the gateway uses NICEPAY’s V2 Checkout (redirect to hosted page) — the QR is rendered on NICEPAY’s server. POS devices typically iframe this and it’s fine.

If you need the raw QR string for native rendering on a kiosk / cashier display (no iframe), the gateway supports NICEPAY’s V2 Direct API:

POST /v1/payments → response includes:
"qr_string": "00020101...", # raw EMVCo payload — render with any QR library
"qr_image_url": "https://.../qr.png" # NICEPAY-hosted PNG

qr_string is populated only when:

  1. The operator has set NICEPAY_*_QRIS_DIRECT=1 in the gateway env.
  2. NICEPAY has activated Direct API on the merchant (separate per-merchant onboarding step).

Until both are true, qr_string stays empty and you fall back to iframing checkout_url. Your code should handle BOTH shapes:

if (response.qr_string) {
renderQRLocally(response.qr_string);
} else {
iframeOrPopup(response.checkout_url);
}

The gateway forwards payment.succeeded / payment.failed to your webhook_url once NICEPAY confirms. Standard signing scheme — see Verifying webhooks.

For QRIS specifically, the gateway also archives the original NICEPAY notification in inbound_events — useful for ops debugging if your webhook doesn’t fire (rare, usually your handler is offline).

  • NICEPAY card processing. Pending merchant approval. Sending method=cc with currency=IDR returns 422 no provider supports method=cc for currency=IDR.
  • NICEPAY live mode. Sandbox is approved (IONPAYTEST); live merchant approval is pending. Until then, products with app.mode=live and method=qris will fall back to mock.