@uqpay/sdk
v3.1.0
Published
Official Node.js SDK for the UQPay API
Downloads
476
Keywords
Readme
UQPay Node.js SDK
SDK/CLI 3.1.0 retains the complete 4.0.0 feature set under the 3.x version line. Review contract migration guidance before upgrading.
Official Node.js SDK for the UQPAY API.
Requirements
- Node.js 22+
- A UQPay account with API credentials
Installation
npm install @uqpay/sdkQuick Start
import { UQPayClient } from '@uqpay/sdk'
const client = new UQPayClient({
clientId: 'your-client-id',
apiKey: 'your-api-key',
environment: 'sandbox', // or 'production'
})
// Retrieve an account
const account = await client.account.accounts.retrieve('acc-123')
console.log(account.status) // 'ACTIVE'Configuration
const client = new UQPayClient({
clientId: 'your-client-id', // required
apiKey: 'your-api-key', // required
environment: 'sandbox', // 'sandbox' | 'production' — default: 'sandbox'
webhookSecret: 'whsec_...', // required for webhook verification
timeout: 30_000, // request timeout in ms — default: 30000
maxRetries: 2, // automatic retry count — default: 2
logLevel: 'info', // 'none' | 'info' | 'debug' — default: 'none'
})Authentication
The SDK handles authentication automatically. It exchanges your clientId and apiKey for a UQPAY Access Token, caches it, and retrieves a new Token before the current one expires. You do not need to manage Tokens manually.
Resources
Account
// Retrieve a single account
const account = await client.account.accounts.retrieve('acc-123')
// List accounts (paginated)
const page = await client.account.accounts.list({ page_number: 1, page_size: 20 })
// Create a sub-account
const subAccount = await client.account.subAccounts.create({
business_type: 'BANKING',
entity_type: 'COMPANY',
nickname: 'My Sub-account',
// ...
})
// Get required additional documents for a country
const docs = await client.account.additionalDocs.get({
country: 'SG',
business_code: 'BANKING',
})Banking
// Balances
const balances = await client.banking.balances.list({ page_number: 1, page_size: 20 })
const transactions = await client.banking.balances.listTransactions({ page_number: 1, page_size: 20 })
// Deposits
const deposits = await client.banking.deposits.list({ page_number: 1, page_size: 20 })
// Transfers
const transfer = await client.banking.transfers.create({
source_account_id: 'acc-123',
destination_account_id: 'acc-456',
currency: 'SGD',
amount: 100,
})
const transfers = await client.banking.transfers.list({ page_number: 1, page_size: 20 })
// Payouts
const payout = await client.banking.payouts.create({
beneficiary_id: 'ben-123',
currency: 'SGD',
amount: 50,
purpose_code: 'PERSONAL',
// ...
})
// Beneficiaries
const beneficiary = await client.banking.beneficiaries.create({ /* ... */ })
const beneficiaries = await client.banking.beneficiaries.list({ page_number: 1, page_size: 20 })
// Conversions
const quote = await client.banking.conversions.createQuote({
sell_currency: 'SGD',
buy_currency: 'USD',
amount: 100,
fixed_side: 'SELL',
})
const conversion = await client.banking.conversions.create({
quote_id: quote.quote_id,
})
// Submit an asynchronous Virtual Account application. Reuse the same key only
// when replaying this normalized request for the same target account.
const accepted = await client.banking.virtualAccounts.create(
{ country: 'SG', currency: 'USD', payment_method: 'SWIFT', nickname: 'Collections' },
{ headers: { 'x-idempotency-key': 'va-application-001' } }
)
console.log(accepted.data.application_id, accepted.data.public_version)
console.log(accepted.data.account_id, accepted.data.direct_id)
// Applications are separate from issued Virtual Account bank details.
const applications = await client.banking.virtualAccountApplications.list({
page_number: 1,
page_size: 50,
status: 'SUBMITTED',
})
const latest = await client.banking.virtualAccountApplications.retrieve(
accepted.data.application_id
)
// This existing endpoint remains the list of issued bank details.
const issuedAccounts = await client.banking.virtualAccounts.list({
page_number: 1,
page_size: 50,
})
// Exchange Rates
const rates = await client.banking.conversions.listCurrentRates({
currency_pairs: ['SGD/USD', 'USD/SGD'],
})
// Payment Methods
const methods = await client.banking.paymentMethods.list({ page_number: 1, page_size: 20 })Issuing
// ── Cardholders ────────────────────────────────────────────────────────────────
const cardholder = await client.issuing.cardholders.create({
email: '[email protected]',
first_name: 'Jane',
last_name: 'Smith',
country_code: 'SG',
phone_number: '+6512345678',
})
// ── Cards ──────────────────────────────────────────────────────────────────────
const card = await client.issuing.cards.create({
card_currency: 'SGD',
cardholder_id: cardholder.cardholder_id,
card_product_id: 'prod-123',
})
const cards = await client.issuing.cards.list({ page_number: 1, page_size: 20 })
const retrieved = await client.issuing.cards.retrieve(card.card_id)
// Freeze / unfreeze
await client.issuing.cards.updateStatus(card.card_id, { card_status: 'FROZEN' })
await client.issuing.cards.updateStatus(card.card_id, { card_status: 'ACTIVE' })
// Recharge and withdraw
await client.issuing.cards.recharge(card.card_id, { amount: 100 })
await client.issuing.cards.withdraw(card.card_id, { amount: 50 })
// ── Secure card display (iFrame) ───────────────────────────────────────────────
// Returns a short-lived URL (60s) that renders card number, CVV, and expiry in a
// PCI-compliant embedded iFrame hosted by UQPay. Open in a browser or embed in
// your frontend. MCP/CLI tools can surface the URL directly to the user.
const { iframeUrl, expires_at } = await client.issuing.cards.getSecureIframeUrl(card.card_id)
console.log(iframeUrl) // https://embedded-sandbox.uqpaytech.com/iframe/card?token=...
console.log(expires_at) // 2026-04-02T18:50:23+08:00
// Optional: set display language and custom styles
const { iframeUrl: styled } = await client.issuing.cards.getSecureIframeUrl(card.card_id, {
lang: 'zh',
styles: { '.card-number': { color: '#333', 'font-size': '18px' } },
})
// ── Balances ───────────────────────────────────────────────────────────────────
const balances = await client.issuing.balances.list({ page_number: 1, page_size: 20 })
const balance = await client.issuing.balances.retrieve('SGD')
const txns = await client.issuing.balances.listTransactions({ page_number: 1, page_size: 20 })
// ── Transactions ───────────────────────────────────────────────────────────────
const transactions = await client.issuing.transactions.list({ page_number: 1, page_size: 20 })
// ── Transfers ──────────────────────────────────────────────────────────────────
const transfer = await client.issuing.transfers.create({
source_account_id: 'acc-123',
destination_account_id: 'acc-456',
currency: 'SGD',
amount: 100,
})
// ── Products ───────────────────────────────────────────────────────────────────
const products = await client.issuing.products.list({ page_number: 1, page_size: 20 })
// ── Reports ────────────────────────────────────────────────────────────────────
const report = await client.issuing.reports.create({
report_type: 'SETTLEMENT',
start_time: '2026-01-01T00:00:00Z',
end_time: '2026-01-31T23:59:59Z',
})Payment
// ── Payment Intents ────────────────────────────────────────────────────────────
const intent = await client.payment.paymentIntents.create({
amount: '100.00',
currency: 'SGD',
// ...
})
await client.payment.paymentIntents.confirm(intent.payment_intent_id, { /* ... */ })
await client.payment.paymentIntents.capture(intent.payment_intent_id, { /* ... */ })
await client.payment.paymentIntents.cancel(intent.payment_intent_id)
const intents = await client.payment.paymentIntents.list({ page_number: 1, page_size: 20 })
// ── Refunds ────────────────────────────────────────────────────────────────────
const refund = await client.payment.refunds.create({
payment_intent_id: intent.payment_intent_id,
amount: '50.00',
})
const refunds = await client.payment.refunds.list({ page_number: 1, page_size: 20 })
// ── Bank Accounts ──────────────────────────────────────────────────────────────
const bankAccount = await client.payment.bankAccounts.create({
currency: 'USD',
bank_account_number: '1234567890',
bank_routing_number: '021000021',
bank_address: '12 Marina Blvd, Singapore',
// ...
})
// ── Payouts ────────────────────────────────────────────────────────────────────
const payout = await client.payment.payouts.create({
bank_account_id: bankAccount.bank_account_id,
amount: '100.00',
currency: 'USD',
})
// ── Balances ───────────────────────────────────────────────────────────────────
const balances = await client.payment.balances.list({ page_number: 1, page_size: 20 })
// ── Payment Attempts ───────────────────────────────────────────────────────────
const attempts = await client.payment.paymentAttempts.list({ page_number: 1, page_size: 20 })
// ── Settlements ────────────────────────────────────────────────────────────────
const settlements = await client.payment.settlements.list({ page_number: 1, page_size: 20 })Supporting (File Upload/Download)
import { readFileSync } from 'fs'
// Upload a file
const uploaded = await client.supporting.files.upload(
readFileSync('./kyc-document.pdf'),
{ filename: 'kyc-document.pdf', mimeType: 'application/pdf' }
)
console.log(uploaded.file_id)
// Get download links
const links = await client.supporting.files.downloadLinks([uploaded.file_id])
console.log(links)Simulator (sandbox only)
The simulator is only available in the sandbox environment. Calling any simulator method in production throws a SimulatorNotAvailableError.
// Simulate a card authorization
const auth = await client.simulator.issuing.authorize({
card_id: 'card-123',
amount: 25,
currency: 'SGD',
merchant_name: 'Coffee Shop',
// ...
})
// Simulate a reversal
await client.simulator.issuing.reverse({
transaction_id: auth.transaction_id,
})
// Simulate a deposit
const deposit = await client.simulator.deposits.simulate({
account_id: 'account-id',
sender_swift_code: 'WELGBE22',
currency: 'SGD',
amount: 500,
})Pagination
All list methods return a PaginatedResponse<T> with data, total_pages, and total_items.
const page = await client.issuing.cards.list({ page_number: 1, page_size: 50 })
console.log(page.data) // Card[]
console.log(page.total_pages) // total number of pages
console.log(page.total_items) // total number of itemsTo paginate manually, increment page_number until page_number >= total_pages.
Webhooks
Verify incoming webhook signatures from UQPay:
import express from 'express'
const app = express()
// IMPORTANT: Use raw body parser — do NOT use express.json() for webhook routes
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
let event
try {
event = client.webhooks.constructEvent(req.body, req.headers)
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`)
}
switch (event.event_name) {
case 'card.create.succeeded':
// handle event
break
}
res.json({ received: true })
})The rawBody passed to constructEvent must be the original request body string or Buffer — not a parsed JSON object.
Virtual Account application events use virtual.account.create,
virtual.account.update, and virtual.account.closed. Their source_id equals
data.application_id. Deduplicate deliveries by event_id, then apply an event
only when its data.public_version is greater than the version already stored.
Webhook data restores the required routing fields account_id and direct_id
alongside the application fields. They are also required on successful Gateway
Create and Retrieve application data, and on every List application summary.
close_reason is always present and may be empty even when the bank detail is
CLOSED. The verifier is version-agnostic and preserves the Hub's application
DTO for supported subscription versions V1.5.1, V1.5.2, and V1.6.0.
import type { VirtualAccountApplicationWebhookEvent } from '@uqpay/sdk'
const event = client.webhooks.constructEvent<VirtualAccountApplicationWebhookEvent>(
rawBody,
headers,
)
console.log(event.data.account_id) // UUID of the account that owns the application
console.log(event.data.direct_id) // "0" for main; main account ID for connected accountAcross REST and webhook data, account_id is the UUID of the account that owns
the application. Treat direct_id as an ordinary string: it is "0" for a main
account and the main account ID for a connected account, and must not be parsed
or validated as a UUID.
New deliveries should use VirtualAccountApplicationWebhookEvent, where both
routing fields are required. To read archived payloads captured before the Hub
fields were restored, use the explicit
LegacyVirtualAccountApplicationWebhookEvent compatibility type. Signature
verification remains pass-through and does not mutate or synthesize missing
payload fields.
Authorization Decision (PGP)
Handle real-time card transaction authorization decisions. UQPAY sends PGP-encrypted transactions to your endpoint; you decrypt, decide, and return an encrypted response.
Generate PGP Key Pair
import { generateAuthDecisionKeyPair } from '@uqpay/sdk'
const { publicKey, privateKey } = await generateAuthDecisionKeyPair({
name: 'Acme Corp',
email: '[email protected]',
})
// Send publicKey to UQPAY, save privateKey securelyConfigure and Create Handler
// Configure PGP keys (once after client initialization)
// Accepts file paths (.asc/.pgp/.gpg) or armored key strings
await client.issuing.authDecision.configure({
privateKey: './keys/my-private.asc',
uqpayPublicKey: './keys/uqpay-public.asc',
passphrase: process.env.PGP_PASSPHRASE, // optional
})
// Create handler — only write your business logic
const handler = client.issuing.authDecision.createHandler({
decide: async (transaction) => {
// transaction contains: billing_amount, merchant_name, card_id, etc.
if (transaction.billing_amount > 10000) {
return { response_code: '51' } // Insufficient Funds
}
return { response_code: '00', partner_reference_id: 'ref-001' }
},
onError: (err) => console.error('Auth decision error:', err),
})
// Mount to Express
app.post('/auth-decision', handler)The SDK handles all PGP encryption/decryption and automatically injects transaction_id into the response.
Error Handling
All API errors throw typed error classes:
import {
UQPayClient,
AuthenticationError,
ValidationError,
NotFoundError,
RateLimitError,
ServerError,
NetworkError,
ReconcileRequiredError,
SimulatorNotAvailableError,
} from '@uqpay/sdk'
try {
const account = await client.account.accounts.retrieve('acc-123')
} catch (err) {
if (err instanceof NotFoundError) {
console.log('Account not found')
} else if (err instanceof ValidationError) {
console.log('Bad request:', err.message)
} else if (err instanceof RateLimitError) {
console.log('Rate limited, will retry automatically')
} else if (err instanceof NetworkError) {
console.log('Network error:', err.message)
} else if (err instanceof ReconcileRequiredError) {
console.log('Check remote state before retrying:', {
method: err.method,
path: err.path,
idempotencyKey: err.idempotencyKey,
reason: err.reason,
})
} else if (err instanceof SimulatorNotAvailableError) {
console.log('Simulator is only available in sandbox')
}
}All UQPayError subclasses expose:
| Field | Type | Description |
|-------|------|-------------|
| message | string | Human-readable error message |
| httpStatus | number | HTTP status code |
| type | string | API error type string |
| code | string | API error code |
Idempotency
All requests automatically include a UUID idempotency key. The SDK reuses the same key for every safe retry belonging to one logical request. To provide your own:
await client.account.subAccounts.create(params, {
headers: { 'x-idempotency-key': 'your-uuid-v4' },
})Acting on Behalf of a Sub-account
await client.account.accounts.retrieve('acc-123', {
headers: { 'x-on-behalf-of': 'sub-account-id' },
})Retries
The SDK automatically retries read requests after rate limits, server errors,
and network errors. Mutating requests retry only when the API explicitly rejects
them before processing, such as an expired token or HTTP 429. A write with an
ambiguous network, timeout, response-read, or HTTP 5xx outcome throws
ReconcileRequiredError; inspect remote state before deciding whether to retry.
Configure the retry limit globally or per request:
// Global
const client = new UQPayClient({ ..., maxRetries: 3 })
// Per-request
await client.account.accounts.retrieve('acc-123', { maxRetries: 0 })Escape Hatch
For endpoints not yet covered by the SDK:
const result = await client.request('POST', '/v1/some/endpoint', {
body: { key: 'value' },
})Partner / Platform Info
Append your platform info to the User-Agent header:
client.setAppInfo('MyPlatform', '2.0.0', 'https://myplatform.com')
// User-Agent: uqpay-node/0.1.0 node/20.0.0 MyPlatform/2.0.0 (https://myplatform.com)Logging
const client = new UQPayClient({
...,
logLevel: 'debug', // logs all requests and responses
})Sensitive fields (apiKey, card_number, cvc, iban, etc.) are automatically redacted from logs. Add custom fields:
const client = new UQPayClient({
...,
logLevel: 'debug',
redactFields: ['my_secret_field'],
})Documentation
Full API reference: https://developers.uqpay.com/
License
MIT
Contract migration notes
See PIN, RFI, deposit simulation and settlement contracts for request changes and response interpretation.
