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 sale and preauth emit 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 developerModeEnabled if it’s left on)
  • Observe KoardMerchantSdk.getInstance().readinessState and block the UI unless isReadyForTransactions is true
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 – a KoardReaderStatus enum value: preparing, readyForTap, cardDetected, pinEntryRequested, pinEntryCompleted, readCompleted, readNotCompleted, readCancelled, readRetry, processing, complete, developerModeEnabled, or unknown
  • displayMessage – Reader-provided instructions ("Present card", "Processing", etc.)
  • statusCode / statusCodeDescription – numeric status (and its description) for advanced troubleshooting
  • actionStatusOnProgress, OnFailure, or OnComplete (these are the only three action statuses)
  • finalStatusApprove, Decline, Abort, Failure, AltService, or Unknown after reader completion
  • transaction – 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 (CalledAuthenticationInProgressAttestationInProgressGettingConfigurationsParsingConfigurationsDone); 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 TransactionFlowViewModel in action
  • See SDK Response Codes for the full reference of final statuses, display messages, and error scenarios
  • Consult KoardPaymentModels.kt for PaymentBreakdown, 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 eventId into sale, 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 readinessState to 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 KoardTransactionResponse carries statusCode, readerStatus, and finalStatus. 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.