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, sdk.preauth, and sdk.completePartialAuth 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.
sale and preauth share the same signature:
suspend fun sale(
activity: Activity,
amount: Int, // amount in cents
breakdown: PaymentBreakdown? = null,
buttonProperties: List<KoardButtonProperties>? = null,
currency: String = "USD",
eventId: String? = null,
tapTimeoutMs: Long? = null // auto-cancel the tap after this many ms
): Flow<KoardTransactionResponse>
Pass buttonProperties to customize the on-screen reader buttons, and tapTimeoutMs to automatically cancel the reader session if no card is presented in time.
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– aKoardReaderStatusenum value:preparing,readyForTap,cardDetected,pinEntryRequested,pinEntryCompleted,readCompleted,readNotCompleted,readCancelled,readRetry,processing,complete,developerModeEnabled, orunknowndisplayMessage– Reader-provided instructions ("Present card", "Processing", etc.)statusCode/statusCodeDescription– numeric status (and its description) for advanced troubleshootingactionStatus–OnProgress,OnFailure, orOnComplete(these are the only three action statuses)finalStatus–Approve,Decline,Abort,Failure,AltService, orUnknownafter reader completiontransaction– populated on completion; includes IDs, surcharge state, and breakdown so you can print receipts or prompt for confirmation
Surcharge confirmation is signaled on the transaction, not the action status. There is no OnConfirmSurcharge action status. When the completion event carries a transaction whose transaction.status == KoardTransactionStatus.SURCHARGE_PENDING, prompt the customer and call sdk.confirm(transactionId, confirm = true/false) with their decision.
For a complete reference of every finalStatus, statusCode, display message ID, and error scenario the SDK can return, see the SDK Response Codes guide.
The demo’s TransactionFlowViewModel (see 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.
Completing a partial approval
When an authorization is only partially approved (the issuer approved less than the requested amount), call sdk.completePartialAuth(...) to authorize the remaining amount using the original card data — no new tap is required for the card itself, but because it drives the reader session it returns a Flow<KoardTransactionResponse> just like sale/preauth:
suspend fun completePartialAuth(
activity: Activity,
transactionId: String,
amount: Int,
breakdown: PaymentBreakdown? = null,
buttonProperties: List<KoardButtonProperties>? = null,
currency: String = "USD",
eventId: String? = null,
tapTimeoutMs: Long? = null
): Flow<KoardTransactionResponse>
Warming up the reader
To avoid paying the full authentication + attestation latency on the first sale after launch, call sdk.prepare() early (for example in Application.onCreate). It returns a Flow<KoardPrepareResponse> that emits progress (Called → AuthenticationInProgress → AttestationInProgress → GettingConfigurations → ParsingConfigurations → Done); key off the Done emission and treat any error variant as "retry on next user action".
sdk.prepare().collect { response ->
if (response.status is KoardPrepareStatus.Done) {
Log.i("Checkout", "Reader is warmed up")
}
}
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}")
}
}
}
Confirming a surcharge
When a completion event carries a transaction with status == KoardTransactionStatus.SURCHARGE_PENDING, prompt the customer and submit their decision with sdk.confirm(...):
suspend fun confirmSurcharge(transactionId: String, accepted: Boolean) {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.confirm(transactionId = transactionId, confirm = accepted)
.onSuccess { println("Surcharge ${if (accepted) "accepted" else "declined"}: ${it.totalAmount}") }
.onFailure { println("Confirm failed: ${it.message}") }
}
}
Canceling the reader session
cancelTransaction() is deprecated. The underlying KiC cancelTransaction() API was deprecated in KiC 25.06.10 — the Visa Tap to Pay Ready app now owns the cancel button (rendered as a transparent overlay on top of your UI) and calls the backend directly when the customer taps it. For programmatic teardown — timeouts, app backgrounding, or recovering from a stale reader state — call sdk.resetKernelService() instead, which unbinds the kernel service and clears the SDK's internal transaction guard.
suspend fun resetReader() {
withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
sdk.resetKernelService()
.onSuccess { println("Reader session reset") }
.onFailure { println("Reset failed: ${it.message}") }
}
}
Account, location, and terminal details
Use these suspend lookups to read merchant, location, and terminal configuration (including surcharge and tax details) when building your checkout UI:
sdk.getMerchantAccount() // Result<KoardAccount>
sdk.getLocation(locationId) // Result<KoardLocation>
sdk.getTerminal(terminalId) // Result<KoardTerminal>
Next steps
- Review the Installing the SDK guide for initialization and enrollment
- Explore the Demo App to see
TransactionFlowViewModelin action - See SDK Response Codes for the full reference of final statuses, display messages, and error scenarios
- Consult
KoardPaymentModels.ktforPaymentBreakdown,Surcharge, and other metadata helpers - Check Supported Devices & NFC Tap Location for device compatibility and where customers should tap
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.resetKernelService()for programmatic teardown (timeout, backgrounding, stale state) to keep the thin client in sync. The customer-initiated cancel is handled by the Visa Tap to Pay Ready app's own overlay.

