Running Payments on Android
Learn how to drive live Tap to Pay on Android sessions with the Koard SDK and how to handle every follow-up transaction step.
What you learn
- Why only
saleandpreauthemit reader events - How to validate SDK readiness before launching the thin client
- Handling
Flow<KoardTransactionResponse>to power custom UIs - Managing surcharge confirmation, tip/tax/surcharge breakdowns, and telemetry
- Post-reader operations such as capture, refund, reverse, incremental auth, and tip adjustments
Step 1: Install the Visa Kernel App
Before writing any Tap to Pay code, install the Visa Kernel app on every NFC-enabled development device. Download it directly from the Google Play Store, launch it once to complete setup, and keep it installed for all future builds.
Prerequisites
Before starting any NFC session:
- Initialize the SDK in
Application.onCreate()and authenticate the merchant - Disable developer mode on the device (reader emits
developerModeEnabledif it’s left on) - Observe
KoardMerchantSdk.getInstance().readinessStateand block the UI unlessisReadyForTransactionsistrue
val sdk = KoardMerchantSdk.getInstance()
val readiness = sdk.readinessState.value
if (!readiness.isReadyForTransactions) {
Log.w("Checkout", "SDK not ready: ${readiness.getStatusMessage()}")
return
}
Launching a Sale Session
Only sdk.sale and sdk.preauth interact with the thin client, so they return cold Flow<KoardTransactionResponse> streams. Collect the flow on a worker thread, and feed each event back to the UI.
fun startSale(activity: Activity, amountInCents: Int) {
val sdk = KoardMerchantSdk.getInstance()
val readiness = sdk.readinessState.value
if (!readiness.isReadyForTransactions) {
Log.w("Checkout", "SDK not ready: ${readiness.getStatusMessage()}")
return
}
val eventId = UUID.randomUUID().toString()
lifecycleScope.launch(Dispatchers.IO) {
sdk.sale(
activity = activity,
amount = amountInCents,
breakdown = PaymentBreakdown(subtotal = amountInCents),
currency = "USD",
eventId = eventId
).collect { response ->
handleTransactionEvent(response)
}
}
}
private fun handleTransactionEvent(response: KoardTransactionResponse) {
when (response.actionStatus) {
KoardTransactionActionStatus.OnProgress -> {
showStatus(response.readerStatus.toString(), response.displayMessage)
}
KoardTransactionActionStatus.OnFailure -> {
showError(
code = response.statusCode,
message = response.displayMessage ?: "Transaction failed",
finalStatus = response.finalStatus
)
}
KoardTransactionActionStatus.OnComplete -> {
val transaction = response.transaction ?: return
showReceipt(transaction)
}
else -> Unit
}
}
Event payloads
Every emitted KoardTransactionResponse contains the data you need to build a bespoke reader UI:
readerStatus– enumerated statuses likepreparing,readyForTap,processing,developerModeEnableddisplayMessage– Reader-provided instructions ("Tap card", "Processing", etc.)statusCode– numeric status for advanced troubleshootingactionStatus–OnProgress,OnFailure,OnComplete, and surcharge confirmation eventsfinalStatus–Approve,Decline,Failure, etc. after reader completiontransaction– populated on completion; includes IDs, surcharge state, and breakdown so you can print receipts or prompt for confirmation (e.g.,SURCHARGE_PENDING→ callsdk.confirm(...))
The demo’s TransactionFlowViewModel (see koard-android-demo/src/main/java/com/koard/android/ui/TransactionFlowViewModel.kt) shows a complete Compose-based implementation that you can adapt for your UI stack.
Preauthorization
Call sdk.preauth(...) when you need a hold + later capture. The signature matches sale, the Flow emits the same events, and response.transaction?.transactionId is the ID you’ll pass into capture/refund APIs later.
Post-Reader Operations
All backend-only operations are suspend functions that return Result<KoardTransaction> (or Result<Unit>), so they never emit reader events and can run from any worker thread.
Capture
suspend fun captureTransaction(transactionId: String, amountOverride: Int? = null) {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.capture(transactionId = transactionId, amount = amountOverride)
.onSuccess { println("Capture successful: ${it.transactionId}") }
.onFailure { println("Capture failed: ${it.message}") }
}
}
Incremental authorization
suspend fun incrementalAuth(transactionId: String, additionalAmountCents: Int) {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.incrementalAuth(transactionId, additionalAmountCents)
.onSuccess { println("Incremental auth approved") }
.onFailure { println("Incremental auth failed: ${it.message}") }
}
}
Reverse / void
suspend fun reverseTransaction(transactionId: String, amountCents: Int? = null) {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.reverse(transactionId, amountCents)
.onSuccess { println("Reverse successful: ${it.transactionId}") }
.onFailure { println("Reverse failed: ${it.message}") }
}
}
Refund
suspend fun refundTransaction(transactionId: String, amount: Int? = null) {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.refundTransaction(
transactionId = transactionId,
amount = amount,
eventId = UUID.randomUUID().toString()
).onSuccess {
println("Refund successful: ${it.transactionId}")
}.onFailure {
println("Refund failed: ${it.message}")
}
}
}
Tip adjustments
suspend fun adjustTip(transactionId: String, newTipCents: Int) {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.adjust(
transactionId = transactionId,
type = AmountType.FIXED,
amount = newTipCents
).onSuccess {
println("Tip adjusted. New total: ${it.totalAmount}")
}.onFailure {
println("Tip adjustment failed: ${it.message}")
}
}
}
Canceling the reader session
If the customer backs out while the reader is active, call sdk.cancelTransaction() from a worker thread to stop the thin client. The method returns Result<Unit> so you can confirm cancellation or surface any error.
Next steps
- Review the Installing the SDK guide for initialization and enrollment
- Explore the Demo App to see
TransactionFlowViewModelin action - Consult
KoardPaymentModels.ktforPaymentBreakdown,Surcharge, and other metadata helpers
Best Practices
- Idempotency everywhere: Pass a stable
eventIdintosale,preauth, refunds, reversals, and adjustments so Koard can safely dedupe retried requests. - UI threading: Collect the payment Flow on
Dispatchers.IO, but dispatch UI updates (Compose state, views, dialogs) back to the main thread to avoid lifecycle crashes. - Surface readiness early: Bind
readinessStateto visible UI (like the demo Settings screen) so operators see why the reader is blocked (developer mode, missing tap-to-pay dependency, no active location). - Telemetry hooks: Each
KoardTransactionResponsecarriesstatusCode,readerStatus, andfinalStatus. Log or export these fields for device fleet monitoring and support. - Lifecycle hygiene: Cancel the Flow when the hosting Activity/Fragment stops, and call
sdk.cancelTransaction()if the user backs out mid-flow to keep the thin client in sync.