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 | |
|---|---|---|
| Runs | On the POS device, beside PayUs | Anywhere (device or your server) |
| Who owns the UI | PayUs (card/QR/tip/receipt screens) | You — or use the optional Compose UI |
| Card / EFTPOS | ✓ via PayUs on-device acquirer SDK | ✗ (non-card QR only) |
| Credentials | None — uses the activated terminal | OAuth app or terminal activation |
| Transport | Android intents (no network from you) | HTTPS REST (+ optional WebSocket) |
| Best for | A POS/ordering app on a PayUs terminal | Your 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 6200554991e1686d409045408cbc35431f8453f703098a732d6508140ad29516Maven 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") }| Composable | Call it with | Returned success subtype |
|---|---|---|
| rememberPayUsPayLauncher | PayUsPayRequest | PayUsResult.PaymentDone |
| rememberPayUsGenerateQrLauncher | PayUsQrRequest | PayUsResult.QrGenerated |
| rememberPayUsQueryLauncher | String (referenceId) | PayUsResult.TransactionStatus |
| rememberPayUsCancelLauncher | String (referenceId) | PayUsResult.TransactionStatus |
| rememberPayUsRefundLauncher | PayUsRefundRequest | PayUsResult.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-activatedActions
| Action | paycore method | UI? | Purpose |
|---|---|---|---|
| ACTION_PAY | pay() | Yes | Full card/QR payment, PayUs owns the UX |
| ACTION_GENERATE_QR | generateQr() | No | Mint a QR payload to render yourself |
| ACTION_QUERY_TRANSACTION | query() | No | Status of a prior transaction |
| ACTION_CANCEL_TRANSACTION | cancel() | No | Cancel a pending QR |
| ACTION_REFUND | refund() | No | Refund a completed transaction |
| ACTION_LIST_GATEWAYS | listGateways() | No | Enumerate configured gateways |
| ACTION_GET_TERMINAL_INFO | getTerminalInfo() | No | Merchant / currency / activation |
ACTION_PAY request fields
Only amountis required — everything else falls back to the terminal's configured data.
| Extra | Type | Fallback if omitted |
|---|---|---|
| amount | long (minor units) | — required, must be > 0 |
| currency | string | Terminal configured currency |
| gateway | string | Shows the gateway picker |
| allowed_gateways | string[] | 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_id | string | Auto-generated reference |
| tip_amount | long | Terminal tip config (may show tip dialog) |
| fee_amount / donation_amount / discount | long (minor units) | 0 — modifier portions of the charge; used to reconcile line_items |
| line_items | string (JSON array) | No basket — see Itemised basket below |
| timeout_seconds | int | 300 (clamped 30–600) |
| merchant_name | string | Terminal configured name |
| brand_color | #AARRGGBB | PayUs primary color |
| skip_tip | bool | false → tip dialog shown per terminal config |
| skip_review | bool | false → 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.
| Action | Success subtype |
|---|---|
| ACTION_PAY | PayUsResult.PaymentDone |
| ACTION_GENERATE_QR | PayUsResult.QrGenerated |
| ACTION_QUERY_TRANSACTION | PayUsResult.TransactionStatus |
| ACTION_CANCEL_TRANSACTION | PayUsResult.TransactionStatus |
| ACTION_REFUND | PayUsResult.RefundDone |
| ACTION_LIST_GATEWAYS | PayUsResult.Gateways |
| ACTION_GET_TERMINAL_INFO | PayUsResult.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
| Field | Type | Notes |
|---|---|---|
| transactionId | string | Store this in your DB and show on your receipt |
| referenceId | string | The order_id you supplied, or the one PayUs generated |
| gateway | string | wechat / alipay / card / unionpay / centrapay / … |
| amountMinor | long | Base amount in cents |
| tipAmountMinor | long | Tip in cents (0 if no tip) |
| feeAmountMinor | long | Surcharge / fee in cents |
| totalAmountMinor | long | amount + tip + fee |
| currency | string | ISO 4217 |
| paymentMethod | string | Card interaction mode (TAP / INSERT / etc.) or wallet type |
| timestamp | string | ISO 8601 |
| cardType | string? | Visa / Mastercard / UnionPay / … (card only) |
| maskedCard | string? | e.g. ****1234 (card only) |
| receipt | string? | Terminal-formatted receipt text |
| gatewayMessage | string? | Gateway's own message, if any |
PayUsResult.QrGenerated fields
| Field | Type | Notes |
|---|---|---|
| qrData | string | Raw payload — render with your QR library of choice |
| qrUrl | string? | Server-hosted PNG fallback if you don't want to render locally |
| referenceId | string | Poll query(referenceId) to know when it's paid |
| expiresAtMs | long | Epoch milliseconds the gateway expires the QR at |
| gateway | string | The gateway you asked for |
| amountMinor | long | Amount in cents |
| currency | string | ISO 4217 |
PayUsResult.TransactionStatus fields
Returned by both query() and cancel(). status is one of PENDING, SUCCESS, FAILED, CANCELLED.
| Field | Type |
|---|---|
| status | Status enum (PENDING / SUCCESS / FAILED / CANCELLED) |
| referenceId | string |
| transactionId | string? (null until SUCCESS) |
| tradeNo | string? (gateway's own id) |
| gateway | string? |
| amountMinor | long |
| currency | string? |
| gatewayMessage | string? |
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 field | Type |
|---|---|
| refundId | string |
| referenceId | string |
| refundedAmountMinor | long — acquirer may clamp vs what you asked for |
| currency | string? |
| gatewayMessage | string? |
Gateways + TerminalInfo
PayUsResult.Gateways
gateways: List<Gateway> — each entry has:
id— stable key, pass back topay()'sgatewaydisplayName— localized label for your UIsupportsQr— can mint a QRsupportsCard— can run a card tap/insertstaticQrAvailable— merchant has a pre-generated QR for this gateway
PayUsResult.TerminalInfo
merchantName— show in your UIcurrency— ISO 4217terminalId— opaque idactivated—false→ merchant hasn't paired PayUs yetintentApiEnabled—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_ERRORPrerequisites
- PayUs is installed and activated on the device (paired to a merchant/terminal)
- The merchant has enabled the Intent API (PayUs Settings → Advanced → Enable Intent API). If off, calls return
ApiDisabled - The SDKs contribute the required Android 11+
<queries>block automatically so your app can resolve the PayUs intents
See also
- Download the Intent API sample app (.zip)
- REST SDK (server-mediated QR / your own UI)
- REST payment endpoints
- Full intent contract:
docs/intent-api-specification.yaml(OpenAPI)