On-Device Integration — Intent API

Launch the PayUs terminal app directly from another app on the same device via Android intents. PayUs owns the payment UX (card tap, QR display, tipping, receipts) and hands your app back a typed result — no backend credentials to manage, and card/EFTPOS works because PayUs drives the on-device acquirer SDK.

Two ways to integrate

Pick the path that matches where your code runs. Many partners use both — the Intent API on the POS device, the REST SDK from their backend.

 Intent API (this page)REST SDK
RunsOn the POS device, beside PayUsAnywhere (device or your server)
Who owns the UIPayUs (card/QR/tip/receipt screens)You — or use the optional Compose UI
Card / EFTPOS✓ via PayUs on-device acquirer SDK✗ (non-card QR only)
CredentialsNone — uses the activated terminalOAuth app or terminal activation
TransportAndroid intents (no network from you)HTTPS REST (+ optional WebSocket)
Best forA POS/ordering app on a PayUs terminalYour own checkout UI, or server-side flows

Android — paycore SDK

com.payus.paycore wraps the intent handshake in a typed, coroutine-friendly client. Download the paycore (and, for Compose, paycore-compose) AAR from the v1.0.0 release — verify it against the published SHA256SUMS— drop it into your module's libs/, then:

// app/build.gradle.kts
dependencies {
    implementation(files("libs/paycore-1.0.0.aar"))
    implementation(files("libs/paycore-compose-1.0.0.aar")) // only if using Compose
}
// paycore-1.0.0.aar        sha256 26585e8208510049f36422025c358a6d6ab73d3e14cd0fd27736333b4039bef0
// paycore-compose-1.0.0.aar sha256 6200554991e1686d409045408cbc35431f8453f703098a732d6508140ad29516

Maven Central publication (a plain com.payus:paycore:1.0.0 coordinate) is coming; until then the GitHub release above is the authoritative source.

// Kotlin — from any ComponentActivity
val client = PayUsClient.from(this)

val result = client.pay(
    PayUsPayRequest(
        amountMinor = 1250,          // $12.50 in cents (required, > 0)
        currency = "NZD",            // optional → terminal default
        orderId = "ORD-42",          // optional → PayUs auto-generates
        gateway = null,              // null → PayUs shows its picker
    )
)

when (result) {
    is PayUsResult.PaymentDone -> store(result.transactionId, result.referenceId)
    is PayUsResult.Cancelled   -> toast("Cancelled: ${result.reason}")
    is PayUsResult.NotActivated-> promptActivatePayUs()
    is PayUsResult.ApiDisabled -> toast("Enable Intent API in PayUs settings")
    is PayUsResult.NotInstalled-> toast("PayUs is not installed")
    is PayUsResult.Failed      -> toast("${result.errorCode}: ${result.message}")
    else                       -> { /* other actions */ }
}

Other calls: generateQr(), query(referenceId), cancel(referenceId), refund(PayUsRefundRequest(…)), listGateways(), getTerminalInfo(). Java callers use the Callback overloads; non-Activity callers use PayUsClient.forContext(appContext).

Android — paycore-compose (Jetpack Compose)

com.payus.paycore.composewraps paycore in Composable launchers so Compose partners don't juggle rememberLauncherForActivityResult themselves. One remember*Launcherper action — call the returned function to fire. There's also a drop-in PayUsPaymentButton.

// Kotlin — anywhere inside a @Composable
val payLauncher = rememberPayUsPayLauncher { result ->
    when (result) {
        is PayUsResult.PaymentDone  -> store(result.transactionId, result.referenceId)
        is PayUsResult.Cancelled    -> toast("Cancelled: ${result.reason}")
        is PayUsResult.NotActivated -> promptActivatePayUs()
        is PayUsResult.ApiDisabled  -> toast("Enable Intent API in PayUs settings")
        is PayUsResult.NotInstalled -> toast("PayUs is not installed")
        is PayUsResult.Failed       -> toast("${result.errorCode}: ${result.message}")
        else                        -> { /* InvalidRequest / TerminalBusy */ }
    }
}

Button(onClick = {
    payLauncher(PayUsPayRequest(amountMinor = 1250, currency = "NZD", orderId = "ORD-42"))
}) { Text("Pay $12.50") }
ComposableCall it withReturned success subtype
rememberPayUsPayLauncherPayUsPayRequestPayUsResult.PaymentDone
rememberPayUsGenerateQrLauncherPayUsQrRequestPayUsResult.QrGenerated
rememberPayUsQueryLauncherString (referenceId)PayUsResult.TransactionStatus
rememberPayUsCancelLauncherString (referenceId)PayUsResult.TransactionStatus
rememberPayUsRefundLauncherPayUsRefundRequestPayUsResult.RefundDone
rememberPayUsListGatewaysLauncher() (no args)PayUsResult.Gateways
rememberPayUsTerminalInfoLauncher() (no args)PayUsResult.TerminalInfo

Or skip the launcher entirely and drop in the pre-styled button — same result callback, handles its own lifecycle. hideWhenPayUsMissing = truekeeps the button out of your UI on devices that don't have PayUs installed.

PayUsPaymentButton(
    request = PayUsPayRequest(amountMinor = 1250, currency = "NZD"),
    onResult = { result -> /* same when-match as above */ },
    text = "Pay with PayUs",             // optional — defaults to this
    hideWhenPayUsMissing = true,         // optional — hides vs disables
    // colors / shape / contentPadding / icon all override MaterialTheme
)

Flutter — payus_pay plugin

# pubspec.yaml — from the v1.0.0 git tag (pub.dev publication is a follow-up)
dependencies:
  payus_pay:
    git:
      url: https://github.com/vivekkhare/PayUs.git
      ref: v1.0.0
      path: sdk/payus_pay
// One-line button
PayUsButton(
  request: PayUsPayRequest(amountMinor: 1250, currency: 'NZD', orderId: 'ORD-42'),
  onResult: (r) => switch (r) { /* PayUsResult variants */ },
)

// …or programmatic
final client = PayUsClient();
final result = await client.pay(PayUsPayRequest(amountMinor: 1250));

Raw intent (no SDK)

The SDKs are thin wrappers over a plain startActivityForResult. You can build the intent by hand:

val intent = Intent("com.payus.pos.ACTION_PAY").apply {
    setPackage("com.payus.pos")       // the installed PayUs POS app id ("com.payus.pos.dev" for the dev build)
    putExtra("amount", 1250L)         // minor units (cents) — required
    putExtra("currency", "NZD")       // optional
    putExtra("gateway", "wechat")     // optional — omit to show picker
    putExtra("order_id", "ORD-42")    // optional
}
startActivityForResult(intent, REQUEST_CODE)
// onActivityResult: resultCode 0=cancelled 1=failed 2=invalid 3=not-activated

Actions

Actionpaycore methodUI?Purpose
ACTION_PAYpay()YesFull card/QR payment, PayUs owns the UX
ACTION_GENERATE_QRgenerateQr()NoMint a QR payload to render yourself
ACTION_QUERY_TRANSACTIONquery()NoStatus of a prior transaction
ACTION_CANCEL_TRANSACTIONcancel()NoCancel a pending QR
ACTION_REFUNDrefund()NoRefund a completed transaction
ACTION_LIST_GATEWAYSlistGateways()NoEnumerate configured gateways
ACTION_GET_TERMINAL_INFOgetTerminalInfo()NoMerchant / currency / activation

ACTION_PAY request fields

Only amountis required — everything else falls back to the terminal's configured data.

ExtraTypeFallback if omitted
amountlong (minor units)— required, must be > 0
currencystringTerminal configured currency
gatewaystringShows the gateway picker
allowed_gatewaysstring[]Restricts the gateway picker to this set (intersected with the terminal's configured gateways). An explicit gateway always wins; an empty/omitted list shows the full picker; a single surviving match auto-selects; no overlap returns INVALID_REQUEST; unknown entries are ignored
order_idstringAuto-generated reference
tip_amountlongTerminal tip config (may show tip dialog)
fee_amount / donation_amount / discountlong (minor units)0 — modifier portions of the charge; used to reconcile line_items
line_itemsstring (JSON array)No basket — see Itemised basket below
timeout_secondsint300 (clamped 30–600)
merchant_namestringTerminal configured name
brand_color#AARRGGBBPayUs primary color
skip_tipboolfalse → tip dialog shown per terminal config
skip_reviewboolfalse → pre-charge review dialog shown

Itemised basket (line_items)

Optionally send the sale's line items with ACTION_PAY. They are persisted and shown against the transaction, and can be itemised on the printed receipt. Omit the extra entirely for no basket — behaviour is unchanged. Android intent extras can't carry a list of maps, so line_items is a single JSON-encoded string.

putExtra("line_items", JSONArray(listOf(
    mapOf("name" to "Flat White", "quantity" to "2",
          "unitPriceCents" to 550, "lineTotalCents" to 1100,
          "sku" to "COFFEE-FW", "category" to "Beverages"),
    mapOf("name" to "Muffin", "quantity" to "1",
          "unitPriceCents" to 450, "lineTotalCents" to 450, "taxCents" to 59)
).map { JSONObject(it) }).toString())

Per line: name (≤255), quantity (decimal string, weight-capable e.g. "1.25", > 0),unitPriceCents / lineTotalCents(integer cents, lineTotal tax-inclusive & after line discount), optionaltaxCents, sku (≤128),category (≤128). Max 200 lines.

Money format on the Intent surface is integer cents throughout — line amounts and the modifier extras (tip_amount, fee_amount,donation_amount, discount) are alllong cents, matching amount. (On the cloud REST surface — payNow / requestTerminal — those same modifiers are decimal strings. Don't mix the two: the same tip is 250 here and "2.50" there.)

Reconciliation is server-side and strict (±1c): sum oflineTotalCents must equalamount − tip_amount − fee_amount − donation_amount + discount. A basket is merchandise and never contains the tip, so declare the tip when it's in the charge. The PayUs backend is the sole reconciler — the terminal only checks JSON well-formedness, per-line fields and the 200-line cap before sending. A malformed basket is refused pre-send withRESULT_INVALID_REQUEST + INVALID_LINE_ITEMS; a reconciliation failure comes back as RESULT_PAYMENT_FAILED +LINE_ITEM_MISMATCHwith the server's arithmetic inerror_message.

Results per action

Every call returns a PayUsResult. The success-case subtype depends on the action — all actions can also return the shared outcomes Cancelled, Failed, InvalidRequest, NotActivated, TerminalBusy, ApiDisabled, NotInstalled.

ActionSuccess subtype
ACTION_PAYPayUsResult.PaymentDone
ACTION_GENERATE_QRPayUsResult.QrGenerated
ACTION_QUERY_TRANSACTIONPayUsResult.TransactionStatus
ACTION_CANCEL_TRANSACTIONPayUsResult.TransactionStatus
ACTION_REFUNDPayUsResult.RefundDone
ACTION_LIST_GATEWAYSPayUsResult.Gateways
ACTION_GET_TERMINAL_INFOPayUsResult.TerminalInfo

cancel() returns the latest TransactionStatus because a late-paid QR can be SUCCESS even after you cancel — trust status over the action name.

PayUsResult.PaymentDone fields

FieldTypeNotes
transactionIdstringStore this in your DB and show on your receipt
referenceIdstringThe order_id you supplied, or the one PayUs generated
gatewaystringwechat / alipay / card / unionpay / centrapay / …
amountMinorlongBase amount in cents
tipAmountMinorlongTip in cents (0 if no tip)
feeAmountMinorlongSurcharge / fee in cents
totalAmountMinorlongamount + tip + fee
currencystringISO 4217
paymentMethodstringCard interaction mode (TAP / INSERT / etc.) or wallet type
timestampstringISO 8601
cardTypestring?Visa / Mastercard / UnionPay / … (card only)
maskedCardstring?e.g. ****1234 (card only)
receiptstring?Terminal-formatted receipt text
gatewayMessagestring?Gateway's own message, if any

PayUsResult.QrGenerated fields

FieldTypeNotes
qrDatastringRaw payload — render with your QR library of choice
qrUrlstring?Server-hosted PNG fallback if you don't want to render locally
referenceIdstringPoll query(referenceId) to know when it's paid
expiresAtMslongEpoch milliseconds the gateway expires the QR at
gatewaystringThe gateway you asked for
amountMinorlongAmount in cents
currencystringISO 4217

PayUsResult.TransactionStatus fields

Returned by both query() and cancel(). status is one of PENDING, SUCCESS, FAILED, CANCELLED.

FieldType
statusStatus enum (PENDING / SUCCESS / FAILED / CANCELLED)
referenceIdstring
transactionIdstring? (null until SUCCESS)
tradeNostring? (gateway's own id)
gatewaystring?
amountMinorlong
currencystring?
gatewayMessagestring?

PayUsRefundRequest + RefundDone

Only referenceId is required. Omit refundAmountMinor for a full refund; set it for a partial.

client.refund(
    PayUsRefundRequest(
        referenceId = "ORD-42",
        refundAmountMinor = 500,       // null → full refund
        refundReason = "customer_return",
        refundPassword = null,         // null → terminal's default; "" → opt out
    )
)
PayUsResult.RefundDone fieldType
refundIdstring
referenceIdstring
refundedAmountMinorlong — acquirer may clamp vs what you asked for
currencystring?
gatewayMessagestring?

Gateways + TerminalInfo

PayUsResult.Gateways

gateways: List<Gateway> — each entry has:

  • id — stable key, pass back to pay()'s gateway
  • displayName — localized label for your UI
  • supportsQr — can mint a QR
  • supportsCard — can run a card tap/insert
  • staticQrAvailable — merchant has a pre-generated QR for this gateway

PayUsResult.TerminalInfo

  • merchantName — show in your UI
  • currency — ISO 4217
  • terminalId — opaque id
  • activated — false → merchant hasn't paired PayUs yet
  • intentApiEnabled — false → merchant disabled intents in PayUs settings

Error codes

Stable error_code values inside PayUsResult.Failed / InvalidRequest.

NOT_ACTIVATEDTERMINAL_BUSYINTENT_API_DISABLEDINVALID_AMOUNTINVALID_CURRENCYUNKNOWN_GATEWAYGATEWAY_DISABLEDINVALID_REFERENCEALREADY_REFUNDEDINVALID_PASSWORDTIMEOUTUSER_CANCELLEDPROVIDER_ERROR

Prerequisites

  1. PayUs is installed and activated on the device (paired to a merchant/terminal)
  2. The merchant has enabled the Intent API (PayUs Settings → Advanced → Enable Intent API). If off, calls return ApiDisabled
  3. The SDKs contribute the required Android 11+ <queries> block automatically so your app can resolve the PayUs intents

See also