iOS SDK (Swift)

Integrate PayUs payments into your application. Authenticate with OAuth credentials, discover terminals, and process payments — all with built-in token management and error handling.

On a PayUs terminal? You can also launch the PayUs app directly via the Intent API — PayUs owns the UI and handles card/EFTPOS as well as QR. This REST SDK is for server-mediated QR payments and your own checkout UI.

Download SDK

Pre-built SDK package ready to drop into your project (~18 KB).

Download .zip

Prerequisites

  1. Ask your PayUs admin to create an OAuth App in the portal (Admin → OAuth Apps)
  2. Get your App ID and App Secret (the secret is shown once — save it)
  3. Note which scopes were granted (e.g. payments:direct, terminals:read)
  4. Note your Branch ID (visible in the admin portal under Users)
  5. Supported currencies: NZD (New Zealand Dollar) and AUD (Australian Dollar)
  6. (Optional) Ask for Sandbox Mode to test with simulated responses — no real money

Sandbox mode lets you test every scenario (success, decline, timeout, rate limit) using magic test amounts.

Installation

// A standalone SwiftPM repo is being published. Until it's live,
// add the SDK as a local package (source: clients/ios, or download below):
//
// Package.swift
.package(path: "path/to/OneMyPosMate")          // local, works today
// Published form (coming shortly):
// .package(url: "https://github.com/onemyposmate/sdk-ios", from: "1.0.0")
// Then depend on the "OneMyPosMate" product

Quick Start

// 1. Create the client
let client = OneMyPosMateClient(
    baseUrl: "https://payus.co.nz",
    system: "pos"
)

// 2. Authenticate with your OAuth app credentials
try await client.auth.loginWithClientCredentials(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET"
)

// 3. Discover terminals in your branch
let terminals = try await client.triggers.listSiblings(branchId: 17)

// 4. Process a payment through a terminal
let resp = try await client.payments.payNow(
    PayNowRequest(
        grandTotal: "10.00",
        referenceId: "REF-\(Int(Date().timeIntervalSince1970))",
        branchId: 17,
        configId: terminals[0].configId,
        channel: "WECHAT"
    )
)

Line items (optional)

payNow (and cloud requestTerminal) accept an optional itemised basket via lineItems. Optional and additive — omit it and nothing changes. The server reconciles it and rejects a bad basket with HTTP 400 (ERR_LINE_ITEM_MISMATCH / ERR_INVALID_LINE_ITEMS). Full field + reconciliation reference: payNow and requestTerminal.

// payNow request body — lineItems is an optional array alongside the usual fields
{
  "grandTotal": "11.00",          // decimal string; includes the tip below
  "referenceId": "ORD-42",        // the refund key — always send one
  "channel": "ALIPAY",
  "branchId": 17, "configId": 123,
  "tipAmount": "1.00",            // decimal string; declared so the basket reconciles
  "lineItems": [                  // amounts in integer cents; sum(lineTotalCents) == merchandise (1100 - 100 = 1000)
    { "name": "Flat White", "quantity": "2", "unitPriceCents": 300, "lineTotalCents": 600, "sku": "FW" },
    { "name": "Muffin",     "quantity": "1", "unitPriceCents": 400, "lineTotalCents": 400, "taxCents": 52 }
  ]
}

Per line: name (≤255), quantity (decimal string, weight-capable), unitPriceCents / lineTotalCents(integer cents), optional taxCents / sku / category; max 200 lines. Reconciliation (strict, ±1c): sum(lineTotalCents) = grandTotal − tip − fee − donation + discount. Line amounts are integer cents on every surface; the modifier amounts (tipAmountetc.) are decimal strings on this REST path and integer cents on the Intent/LAN surfaces — don't mix them. Integrating on-device via the Intent/paycore SDK instead? It ships a typed PayUsLineItem with validation — see the Intent API page.

Authentication

The SDK handles OAuth token exchange, caching, and auto-refresh. You provide your appId and appSecret — the SDK does the rest.

// Authenticate with OAuth app credentials (appId + appSecret)
// Obtain these from your PayUs admin (Portal → OAuth Apps)
let client = OneMyPosMateClient(
    baseUrl: "https://payus.co.nz",
    system: "pos"
)

try await client.auth.loginWithClientCredentials(
    appId: "YOUR_APP_ID",
    appSecret: "YOUR_APP_SECRET"
)

// Token cached in Keychain (KeychainTokenStore)
// Every call auto-refreshes if within 60s of expiry
let token = try await client.auth.currentToken()

// Actor-isolated single-flight Task prevents concurrent refreshes
// If token is revoked, loginWithClientCredentials() must be called again

Token Lifecycle

Tokens expire after 7 days. The SDK auto-refreshes 60 seconds before expiry. If an admin revokes your OAuth app or changes your scopes, you need to re-authenticate.

// Tokens expire after 7 days — the SDK auto-refreshes
// If the admin revokes your OAuth app, new tokens are blocked
// If scopes change, get a new token to pick up new permissions

// Get the current token — currentToken() refreshes transparently when near expiry
let token = try await client.auth.currentToken()

// Custom token storage (e.g. for macOS without Keychain)
let client = OneMyPosMateClient(
    baseUrl: "https://payus.co.nz",
    system: "pos",
    tokenStore: InMemoryTokenStore()
)

Error Handling

All SDK methods throw typed exceptions with structured error codes. The SDK auto-retries on ERR_UNAUTHORIZED (once) and ERR_SYSTEM_ERROR (3x).

do {
    try await client.payments.payNow(request)
} catch let error as OneMyPosMateError {
    switch error.errorCode {
    case .ERR_FORBIDDEN:
        // Missing OAuth scope — ask admin to grant it
        break
    case .ERR_GATEWAY_NOT_FOUND:
        // Terminal has no gateway configured
        break
    case .ERR_DUPLICATE_TRANSACTION:
        // referenceId already used — generate a unique one
        break
    case .ERR_REFUND_EXCEEDED:
        // Refund amount exceeds remaining balance
        break
    case .ERR_UNAUTHORIZED:
        // Token expired and auto-refresh failed
        // Call loginWithClientCredentials() again
        break
    case .ERR_RATE_LIMITED:
        // Too many requests — back off and retry
        break
    case .ERR_SYSTEM_ERROR:
        // Server error — SDK retries 3x automatically
        break
    default: break
    }
}

Error Codes Reference

CodeHTTPMeaningSDK Behavior
ERR_FORBIDDEN403Missing required OAuth scopeThrows immediately — check your granted scopes
ERR_GATEWAY_NOT_FOUND422Terminal has no gateway configuredThrows immediately — contact admin
ERR_DUPLICATE_TRANSACTION400referenceId already usedThrows immediately — use a unique referenceId
ERR_REFUND_EXCEEDED400Refund amount exceeds remaining balanceThrows immediately — check remaining balance
ERR_UNAUTHORIZED401Token expired or revokedAuto-retries once with fresh token
ERR_RATE_LIMITED429Too many requestsThrows immediately — implement backoff
ERR_SYSTEM_ERROR500Server errorAuto-retries 3x with exponential backoff

Required Scopes

Each SDK method requires a specific OAuth scope. If your app doesn't have the required scope, the server returns 403 ERR_FORBIDDEN. Ask your admin to update your app's scopes.

SDK MethodRequired Scope
client.payments.payNow()payments:direct
client.payments.saveTransaction()payments:direct
client.payments.refund()refunds:write
client.payments.cancel()refunds:write
client.query.getTransactionDetails()transactions:read
client.triggers.listSiblings()terminals:read
client.triggers.sendTrigger()terminals:trigger