OAuth Scopes

Third-party integrators authenticate with an OAuth app (appId + appSecret) and receive a scoped token. Each scope gates a specific set of API endpoints. The admin assigns scopes when creating the OAuth app — integrators cannot request scopes they weren't granted.

How It Works

  1. Admin creates an OAuth app in the portal (Sidebar → OAuth Apps). Assigns a display name, an owner (merchant or branch), and one or more scopes.
  2. The portal generates an appId and appSecret. The secret is shown once.
  3. The integrator authenticates via the same token endpoint as terminals:
    POST /api/v1/oauth/token
    Authorization: Basic base64(appId:appSecret)
  4. The returned token includes the granted scopes. Every API call checks the token's scopes — if the required scope is missing, the server returns 403 ERR_FORBIDDEN.
  5. Terminal users (BRANCH, MERCHANT, ADMIN) are exempt from scope checks — they continue working exactly as before.

Available Scopes

ScopeWhat It AllowsTypical User
payments:directCall payNow + saveTransactionPOS software, kiosks
payments:triggerSend terminal triggers (PRINT, FORCE_REFRESH)POS software, companion apps
refunds:writeCall refund + cancelTransactionPOS software
transactions:readRead transaction details, recent transactions, refund statusAll integrators
reports:readRead channel summaries, settlement reportsERPs, companion apps
terminals:readList terminals (see assigned terminals + online status)All integrators
terminals:triggerSend trigger commands to terminalsPOS software
ws:subscribeOpen WebSocket and subscribe to terminal eventsPOS software, kiosks

Endpoint → Scope Mapping

EndpointRequired Scope
POST /{system}/payNowpayments:direct
POST /{system}/saveTransactionpayments:direct
POST /{system}/refundrefunds:write
POST /{system}/cancelTransactionrefunds:write
POST /{system}/getTransactionDetailstransactions:read
POST /{system}/getRecentTransactionstransactions:read
POST /{system}/findtransactions:read
GET /{system}/refundStatustransactions:read
POST /{system}/getChannelSummaryreports:read
POST /{system}/overallSummaryreports:read
POST /{system}/settlereports:read
POST /{system}/settlementReportreports:read
GET /{system}/listTerminalsterminals:read
POST /{system}/requestTerminalterminals:trigger
POST /{system}/requestStatusterminals:read
STOMP /queue/terminal/{configId}ws:subscribe

Blocked for Integrators (No Scope Available)

These endpoints are gated by role (BRANCH+, ADMIN, or SYSTEM_ADMIN). No OAuth scope can grant access to them — they are permanently blocked for the INTEGRATOR role.

ActionWhy Blocked
Create/delete/edit users or rolesAdmin-only — no scope exists
View/edit gateway configsContains merchant keys — admin-only
Activate/deactivate terminalsTerminal lifecycle is admin-managed
Upload APK releasesSYSTEM_ADMIN only
Access other merchants' dataTenantOwnershipValidator blocks cross-tenant
Modify terminal settings/feesAdmin portal only
Access webhook logs, monitoring, dev toolsSYSTEM_ADMIN only

Integrator Quick Start

# 1. Get your appId + appSecret from the PayUs admin
#    (Admin creates it in Portal → OAuth Apps)

# 2. Authenticate
curl -X POST https://payus.co.nz/api/v1/oauth/token \
  -H "Authorization: Basic $(echo -n 'YOUR_APP_ID:YOUR_APP_SECRET' | base64)"

# Response: { "token": "...", "type": "bearer", "expiryDate": "..." }

# 3. Use the token for scoped API calls
curl -X GET "https://payus.co.nz/api/v1/pos/listTerminals?branchId=123" \
  -H "Authorization: Bearer YOUR_TOKEN"

# 4. If you call an endpoint outside your granted scopes:
# → 403 { "errorCode": "ERR_FORBIDDEN", "message": "Insufficient scope — requires payments:direct" }

Using the SDK as an Integrator

The existing SDKs (Android, Web, iOS) work unchanged for integrators. Just use your appId/appSecret instead of a terminal's clientId/clientSecret:

// Same SDK, different credentials
const client = new OneMyPosMateClient({
  baseUrl: 'https://payus.co.nz',
  system: 'pos',
});

// Authenticate with your OAuth app credentials
await client.auth.loginWithClientCredentials('YOUR_APP_ID', 'YOUR_APP_SECRET');

// Call endpoints within your granted scopes
const terminals = await client.triggers.listSiblings(branchId);
// ✅ Works if you have terminals:read scope

await client.payments.payNow({ ... });
// ❌ 403 if you DON'T have payments:direct scope

Terminal WebSocket Connection Control

⚡ Terminal WebSocket connections are admin-controlled.

A terminal's ability to receive real-time triggers from integrators (via requestTerminal) depends on the terminal being online and connected to the WebSocket server. Admins can:

  • Deactivate a terminal — kicks the WebSocket session, generates a new activation code, blocks all triggers until re-activated
  • Send SESSION_KICK — disconnects the WebSocket session without deactivating
  • Send FORCE_REFRESH — forces the terminal to re-fetch its config (may change gateway/channel assignments)

Integrators should handle the case where a terminal is offline gracefully — the requestTerminalendpoint will still accept the trigger, but it won't be delivered until the terminal reconnects.

Sample Apps

Minimal single-file demos showing the full integration flow. Each covers: authenticate → list terminals → send trigger → process payment → query transactions.

Web

TypeScript · Single file · No deps

npx tsx integrator-demo.ts⬇ Download integrator-demo.ts

Android

Kotlin · Single file · No deps

kotlin IntegratorDemo.kt⬇ Download IntegratorDemo.kt

iOS

Swift · Single file · No deps

swift IntegratorDemo.swift⬇ Download IntegratorDemo.swift

Sandbox Mode

🧪 Test your integration without real money

Ask your admin to enable Sandbox Mode on your OAuth app. Sandbox apps authenticate and call APIs exactly like production — but all gateway calls are simulated. Transactions are saved to the database so queries and reports work, but no real payment is processed.

Payment Scenario Triggers (via amount)

AmountSimulated OutcomeUse Case
$0.01 – $499.99SUCCESS instantlyHappy path
$500.00PENDING → SUCCESS after 10sQR code flow — test polling
$500.01PENDING foreverTest poll timeout handling
$999.00SUCCESS with 3s delayTest client timeout config
$1,000.00FAILED — Declined by issuerDecline scenario
$1,001.00FAILED — ERR_PAYMENT_TIMEOUTGateway timeout
$1,002.00FAILED — ERR_SYSTEM_ERRORGateway crash (tests SDK retry)
$1,003.00FAILED — ERR_RATE_LIMITEDRate limit scenario

Reference ID Prefix Overrides

For fine-grained control, prefix your referenceId to force any outcome regardless of amount. Prefixes take priority over amount-based triggers.

PrefixForces
SBX-OK-*SUCCESS (any amount)
SBX-FAIL-*FAILED — decline
SBX-TIMEOUT-*FAILED — ERR_PAYMENT_TIMEOUT
SBX-ERROR-*FAILED — ERR_SYSTEM_ERROR
SBX-PENDING-*PENDING (never resolves)
SBX-SLOW-*SUCCESS with 5s delay

What still validates normally

  • • OAuth scope enforcement → 403 if missing scope
  • • Tenant isolation → can't access other merchants' data
  • • Duplicate referenceId detection → 409
  • • Amount format and range checks → 400
  • • Refund cooldown (60s) and balance checks → 400
  • • Rate limiting → still enforced globally

Sandbox Transaction Isolation

Sandbox transactions are tagged sandbox=true in the database. Sandbox apps only see sandbox transactions in query results, and production apps never see sandbox transactions. Reports and summaries are computed separately.

Token Lifecycle

  • • Tokens expire after 7 days. The SDK auto-refreshes.
  • • If the admin revokes the OAuth app, new token requests are blocked immediately. Existing tokens work until expiry (max 7 days).
  • • If the admin changes scopes, the integrator must get a new token to pick up the new scopes.
  • Secret rotation: the admin can regenerate the appSecret. The old secret stops working for new token mints immediately.