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.
Method slugs
Section titled “Method slugs”| Method | What buyer does | Wallets / banks |
|---|---|---|
qris | Scans a QR code with their wallet app | GoPay, OVO, DANA, ShopeePay, BCA Mobile, anything QRIS-compatible |
va_bca | Transfers from BCA mobile/internet banking | BCA only |
va_mandiri | Transfers from Mandiri | Mandiri only |
va_bni | Transfers from BNI | BNI only |
va_bri | Transfers from BRI | BRI only |
va_permata | Transfers from Permata | Permata only |
qris is universal — any wallet works. VA is bank-specific — only customers using that exact bank can pay.
Settlement
Section titled “Settlement”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.
Step 1: Issue a payment
Section titled “Step 1: Issue a payment”POST /v1/paymentsX-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:
emailis required. NICEPAY rejects QRIS / VA withoutbillingEmail(error9018). The gateway forwards youremailfield to that. Always send it.userIPis filled by the gateway from your client’sX-Forwarded-Forfirst hop. NICEPAY rejects without it (error9024). If you’re behind an unusual proxy chain, talk to ops.
Step 2: Buyer pays
Section titled “Step 2: Buyer pays”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:
Online checkout (most products)
Section titled “Online checkout (most products)”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}.
POS retail (posz pattern)
Section titled “POS retail (posz pattern)”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.
QRIS expiry
Section titled “QRIS expiry”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).
VA expiry
Section titled “VA expiry”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 PNGqr_string is populated only when:
- The operator has set
NICEPAY_*_QRIS_DIRECT=1in the gateway env. - 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);}Step 3: Webhook
Section titled “Step 3: Webhook”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).
What’s NOT wired today
Section titled “What’s NOT wired today”- NICEPAY card processing. Pending merchant approval. Sending
method=ccwithcurrency=IDRreturns422 no provider supports method=cc for currency=IDR. - NICEPAY live mode. Sandbox is approved (
IONPAYTEST); live merchant approval is pending. Until then, products withapp.mode=liveandmethod=qriswill fall back to mock.