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
- 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.
- The portal generates an
appIdandappSecret. The secret is shown once. - The integrator authenticates via the same token endpoint as terminals:
POST /api/v1/oauth/token Authorization: Basic base64(appId:appSecret) - 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. - Terminal users (BRANCH, MERCHANT, ADMIN) are exempt from scope checks — they continue working exactly as before.
Available Scopes
| Scope | What It Allows | Typical User |
|---|---|---|
| payments:direct | Call payNow + saveTransaction | POS software, kiosks |
| payments:trigger | Send terminal triggers (PRINT, FORCE_REFRESH) | POS software, companion apps |
| refunds:write | Call refund + cancelTransaction | POS software |
| transactions:read | Read transaction details, recent transactions, refund status | All integrators |
| reports:read | Read channel summaries, settlement reports | ERPs, companion apps |
| terminals:read | List terminals (see assigned terminals + online status) | All integrators |
| terminals:trigger | Send trigger commands to terminals | POS software |
| ws:subscribe | Open WebSocket and subscribe to terminal events | POS software, kiosks |
Endpoint → Scope Mapping
| Endpoint | Required Scope |
|---|---|
| POST /{system}/payNow | payments:direct |
| POST /{system}/saveTransaction | payments:direct |
| POST /{system}/refund | refunds:write |
| POST /{system}/cancelTransaction | refunds:write |
| POST /{system}/getTransactionDetails | transactions:read |
| POST /{system}/getRecentTransactions | transactions:read |
| POST /{system}/find | transactions:read |
| GET /{system}/refundStatus | transactions:read |
| POST /{system}/getChannelSummary | reports:read |
| POST /{system}/overallSummary | reports:read |
| POST /{system}/settle | reports:read |
| POST /{system}/settlementReport | reports:read |
| GET /{system}/listTerminals | terminals:read |
| POST /{system}/requestTerminal | terminals:trigger |
| POST /{system}/requestStatus | terminals: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.
| Action | Why Blocked |
|---|---|
| Create/delete/edit users or roles | Admin-only — no scope exists |
| View/edit gateway configs | Contains merchant keys — admin-only |
| Activate/deactivate terminals | Terminal lifecycle is admin-managed |
| Upload APK releases | SYSTEM_ADMIN only |
| Access other merchants' data | TenantOwnershipValidator blocks cross-tenant |
| Modify terminal settings/fees | Admin portal only |
| Access webhook logs, monitoring, dev tools | SYSTEM_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 scopeTerminal 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.
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)
| Amount | Simulated Outcome | Use Case |
|---|---|---|
| $0.01 – $499.99 | SUCCESS instantly | Happy path |
| $500.00 | PENDING → SUCCESS after 10s | QR code flow — test polling |
| $500.01 | PENDING forever | Test poll timeout handling |
| $999.00 | SUCCESS with 3s delay | Test client timeout config |
| $1,000.00 | FAILED — Declined by issuer | Decline scenario |
| $1,001.00 | FAILED — ERR_PAYMENT_TIMEOUT | Gateway timeout |
| $1,002.00 | FAILED — ERR_SYSTEM_ERROR | Gateway crash (tests SDK retry) |
| $1,003.00 | FAILED — ERR_RATE_LIMITED | Rate 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.
| Prefix | Forces |
|---|---|
| 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.