POS retail flow
For products that take retail payments at a physical or virtual POS — cashier rings up an amount, buyer scans / pays, cashier sees “Paid” within seconds. The canonical posz integration.
Shape of the flow
Section titled “Shape of the flow”1. Cashier rings up amount.2. POS calls POST /v1/payments with currency=IDR method=qris.3. POS displays the QR (either iframe checkout_url, or render qr_string locally).4. Buyer scans with their wallet, pays.5. Wallet → NICEPAY → gateway webhook → product's webhook receiver.6. POS sees "Paid" and prints receipt.Time from amount-entered to “Paid” on the cashier display: typically 2-8 seconds. The bottleneck is the buyer scanning + the NICEPAY → gateway → product webhook chain.
QR display: two modes
Section titled “QR display: two modes”The POS code should handle BOTH response shapes from POST /v1/payments:
const r = await createPayment(...);
if (r.qr_string) { // Native rendering — operator has flipped on V2 Direct. // Render qr_string with any QR library (qrcode.js, etc.). // Faster UX, no NICEPAY page in the flow. renderQRLocally(r.qr_string);} else { // V2 Checkout (default) — iframe / popup / webview the URL. // NICEPAY's hosted page renders the QR. showCheckoutURL(r.checkout_url);}This way your POS code works regardless of whether NICEPAY has activated Direct API on the merchant.
See IDR via NICEPAY for the V2 Checkout vs V2 Direct background.
Paid notification: webhook vs polling
Section titled “Paid notification: webhook vs polling”For the cashier to see “Paid” within a second of the buyer paying, you have two options:
Webhook (recommended)
Section titled “Webhook (recommended)”- POS device runs a tiny HTTP server that the gateway POSTs to.
- Gateway delivers
payment.succeededto your endpoint within ~1s of NICEPAY confirming. - Push the result to the cashier display via websocket / SSE / polling-1Hz from the POS device.
This requires the POS to be reachable from the internet OR for your product to have a per-cashier session that proxies through your central gateway-webhook receiver.
Polling
Section titled “Polling”- POS calls
GET /v1/payments/{id}every 1s while waiting. - Stops when
statusbecomes terminal. - Simpler — POS doesn’t need to be reachable from the internet.
Polling is fine for retail — buyers expect a moment of “let me check” after scanning. The 1s poll interval gives sub-second-feeling perception.
For sub-second-actual feel, use webhooks.
Expiry handling
Section titled “Expiry handling”QRIS expires in 20 minutes (NICEPAY hard cap). Behavior:
- If the buyer hasn’t paid within 20 min, the QR becomes invalid even if scanned.
- Polling
GET /v1/payments/{id}after expiry returnsstatus=expired(NICEPAY-side), gateway maps tofailedon the canonical event. - POS should void the transaction and let the cashier issue a fresh request.
Don’t try to extend the expiry past 20 min — NICEPAY rejects with 9031 Date/Time check.
Receipt printing
Section titled “Receipt printing”Receipt should include the gateway’s invoice_number (the human-readable form like POS-000042), not the 32-char hex payment_id. Customers and ops both prefer it for support tickets / refund lookups.
Uncle-Z Coffee ShopOrder POS-000042Total: Rp 50,000Paid: 2026-05-09 14:30 WIB via QRIS
If you need help: support@uncle-z.com reference: POS-000042Refund flow
Section titled “Refund flow”Buyer wants their money back? Cashier triggers POST /v1/refunds from the POS. The gateway issues the refund through NICEPAY’s cancel API.
For QRIS specifically:
- On-Us refunds (same wallet that paid): immediate, free, within 1 day of the original transaction. After 1 day, becomes a manual refund (NICEPAY ops involvement).
- Off-Us refunds (cross-wallet): online refund supported up to 365 days from original transaction.
The gateway exposes both as POST /v1/refunds — the underlying NICEPAY cancel call handles the on-us / off-us distinction transparently. See Refunds.
Connectivity
Section titled “Connectivity”POS devices in retail typically have flaky WiFi. The gateway’s idempotency support is your friend:
- Generate an
Idempotency-KeyUUID per cashier transaction. - Persist it on the POS device alongside the transaction id.
- Retry the
POST /v1/paymentscall with the SAME key if the network drops mid-call. The gateway recognizes the replay and returns the original response.
See Idempotency.
What posz actually does
Section titled “What posz actually does”posz lives at /Volumes/ZT9/codes/posz/ (uncle-z’s reference POS implementation). Pattern:
- Cashier app (Tauri / Electron) on the device.
- Backend service (Go / Node — pick one) running at
pos.uncle-z.comthat:- Receives webhooks from
payment.uncle-z.com. - Holds open websocket per cashier device.
- Pushes
payment.succeededevents down to the matching cashier in real time.
- Receives webhooks from
- Product app keys (
(pk_, secret, webhook_secret)) stored on the backend, not the cashier device.
This indirection means cashier devices don’t carry payment credentials and don’t need to be internet-reachable. Same pattern recommended for any retail POS integration.