Installing the SDK
Install the Koard Merchant SDK to enable tap-to-pay functionality in your Android application.
If you're ready to start developing, see our Android SDK implementation guide.
What you learn
In this guide, you'll learn:
- How to add the Koard Merchant SDK to your Android project
- How to configure Gradle dependencies
- How to initialize the SDK in your application
- How to verify your installation is working correctly
Prerequisites
Before you begin, ensure you have:
- Android Studio Hedgehog or newer (required for modern Gradle support)
- JDK 21 (required for compilation)
- Android SDK 36 (minimum SDK 31 - Android 12)
- Physical Android device with NFC support (Android 12+) — see Supported Devices & NFC Tap Location
- Valid Koard merchant account (configured in Koard MMS)
- Install the Visa Kernel app on every NFC-enabled test device before running tap-to-pay flows (download from the Google Play Store)
Physical Device Required: Tap to Pay on Android requires a physical device with NFC hardware. The Android emulator does not support NFC contactless payments.
Developer Mode Workflow:
When working with Tap to Pay on Android, follow this important workflow:
- Enable Developer Mode - Turn on developer mode to install and test app revisions
- Install/Update Your App - Deploy your application updates
- Disable Developer Mode - You MUST turn off developer mode before running tap to pay transactions
- Run Transactions - Process payments with developer mode disabled
Tap to Pay transactions will NOT work if developer mode is enabled. This is a security requirement from the payment processor.
Step 1: Add the SDK Dependency
The Koard Android SDK is published to Maven Central. The current release is com.koard:koard-android-sdk:1.0.6 (packaging aar). No credentials and no custom repository are required — you only need mavenCentral() in your repositories, which most Android projects already have.
1a. Ensure Maven Central is a repository
Most Android projects already resolve dependencies from Maven Central. If yours doesn't, add mavenCentral() to dependencyResolutionManagement in your settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
1b. Add the dependency
Add the Koard SDK to your app module's build.gradle.kts:
dependencies {
implementation("com.koard:koard-android-sdk:1.0.6")
}
1c. Declare the libraries the SDK does not bring in
The 1.0.6 artifact is a shaded fat AAR. OkHttp, Okio, Retrofit, kotlinx.serialization, and Ktor are packaged inside the AAR (relocated under com.koardlabs.*) and are deliberately dropped from the published POM — this isolates the SDK from whatever versions your app uses. The published POM declares only:
| Dependency | Version |
|---|---|
org.jetbrains.kotlin:kotlin-stdlib |
2.2.21 |
org.jetbrains.kotlin:kotlin-parcelize-runtime |
2.2.21 |
androidx.security:security-crypto |
1.1.0 |
com.google.android.gms:play-services-safetynet |
18.0.1 |
com.nimbusds:nimbus-jose-jwt |
10.0.2 |
Nothing else resolves transitively. AndroidX, Kotlin coroutines, and any networking or serialization library your own code uses must be declared in your app module. If you previously relied on the SDK to pull OkHttp/Retrofit/kotlinx.serialization in for you (1.0.5 and earlier shipped them as ordinary transitive dependencies), declare them directly now.
dependencies {
implementation("com.koard:koard-android-sdk:1.0.6")
// Declare what YOUR code needs — the SDK no longer supplies these transitively.
implementation("androidx.core:core-ktx:1.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
Fallback: Manual AAR Installation
If you have been supplied the SDK as a raw .aar file (for example, an offline or pre-release build), you can side-load it instead of resolving from Maven Central:
- Create a
libsdirectory in your app module if it doesn't exist - Copy the AAR file (e.g.,
koard-android-release.aar) to thelibs/directory - Add the dependency in your
build.gradle.kts:
dependencies {
implementation(files("libs/koard-android-release.aar"))
// A raw file dependency carries no POM, so declare everything the SDK
// would otherwise pull in — plus whatever your own code needs.
// The five dependencies the published POM declares:
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.21")
implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.21")
implementation("androidx.security:security-crypto:1.1.0")
implementation("com.google.android.gms:play-services-safetynet:18.0.1")
implementation("com.nimbusds:nimbus-jose-jwt:10.0.2")
// Plus whatever your own code needs:
implementation("androidx.core:core-ktx:1.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
Do not add OkHttp, Retrofit, Okio, kotlinx.serialization, or Ktor "for the SDK" — they are already shaded inside the AAR under com.koardlabs.*. Add them only if your own code uses them; the shaded copies will not conflict with yours.
Step 2: Configure AndroidManifest.xml Permissions
Add the required permissions, the <queries> tag, and the NFC intent filter to your AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.REBOOT" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<queries>
<package android:name="com.visa.kic.app.kernel" />
</queries>
<application ...>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Required: lets the tap-to-pay kernel dispatch card reads to this activity -->
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>
The <queries> tag declares visibility to the Tap to Pay kernel app (com.visa.kic.app.kernel), which is required for the SDK to detect and communicate with the Visa kernel during tap-to-pay flows.
Where each permission comes from. INTERNET, ACCESS_NETWORK_STATE, NFC, and WAKE_LOCK are declared in the SDK's own manifest and merged into your app, but declaring them explicitly keeps your permission surface auditable. ACCESS_WIFI_STATE and REBOOT are required by Visa's Kernel-in-Cloud (KiC) integration spec (§3.3.3, "mandatory minimum permissions") for apps that integrate the Tap to Pay Ready kernel — they are not contributed by the Koard SDK, so you must declare them yourself. The SDK also contributes <uses-feature android and android.hardware.nfc.hce, both with required="false". The <queries> tag and the NFC intent filter are likewise not contributed by the SDK.
If your activity sets android, also declare <uses-feature android so Google Play does not filter out POS devices (Sunmi D3, iMin Swan 1 Pro, and similar) that do not report that feature.
Add the NFC Tech Filter Resource
Create res/xml/nfc_tech_filter.xml referenced by the meta-data above:
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
</tech-list>
</resources>
Step 3: Configure Build Settings
Set Minimum SDK Version
Ensure your app's minimum SDK is set to Android 12 (API level 31):
android {
compileSdk = 36 // belongs on `android { }`, NOT inside defaultConfig
defaultConfig {
minSdk = 31
targetSdk = 36
}
}
Configure Java Compatibility
Set Java version to 21:
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = "21"
}
}
Add Build Flavors (Optional)
The SDK ships exactly two built-in environments — KoardEnvironment.UAT and KoardEnvironment.PROD — so mirror the demo app and define one flavor per environment:
android {
flavorDimensions += "environment"
productFlavors {
create("uat") {
dimension = "environment"
isDefault = true
buildConfigField("String", "ENVIRONMENT", "\"UAT\"")
applicationIdSuffix = ".uat"
// UAT: https://api.uat.koard.com
}
create("prod") {
dimension = "environment"
buildConfigField("String", "ENVIRONMENT", "\"PROD\"")
// Production: https://api.koard.com
}
}
}
There is no dev environment constant. To target a non-standard backend, use KoardEnvironment.Custom(koardApiUrl, enrollmentUrl, visaCloudPosUrl, name) rather than adding a dev flavor that has nothing to map onto.
Step 4: Initialize the SDK
Create Application Class
Initialize the SDK in your Application.onCreate() method on a worker thread:
import android.app.Application
import com.koardlabs.merchant.sdk.KoardMerchantSdk
import com.koardlabs.merchant.sdk.domain.KoardEnvironment
import com.koardlabs.merchant.sdk.domain.KoardLogLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize SDK synchronously during app startup
runBlocking {
withContext(Dispatchers.IO) {
try {
// The only environments are UAT, PROD, and Custom(...)
val environment = KoardEnvironment.PROD
KoardMerchantSdk.initialize(
application = this@MyApplication,
apiKey = "your-api-key",
environment = environment,
timeoutSeconds = 30L,
logLevel = KoardLogLevel.DEBUG
)
println("Koard SDK initialized successfully")
} catch (e: Exception) {
// Handle initialization error
println("Failed to initialize SDK: ${e.message}")
}
}
}
}
}
Important:
- SDK initialization must be performed on a worker thread (not the main UI thread). Always use
Dispatchers.IOwith coroutines. - The
initialize()method takesapplication,apiKey,environment, and the optionaltimeoutSeconds(default30L) andlogLevel- NOT merchantCode/merchantPin. - Merchant authentication is done separately using
login(merchantCode, merchantPin)after initialization. logLevelis the only place SDK logging verbosity can be set — there is no runtime setter. It defaults toKoardLogLevel.DEBUGin debug builds andKoardLogLevel.NONEin release.
Register Application in Manifest
Add your custom Application class to AndroidManifest.xml:
<application
android:name=".MyApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.MyApp">
<!-- Your activities here -->
</application>
Step 5: Register Your Activity for NFC
Android dispatches TECH_DISCOVERED to whatever app claims it, so an unrelated NFC app can jump into the foreground the moment the customer presents a card. registerActivityForNfc(this) claims NFC foreground dispatch for your activity so it stays in front for the duration of the tap — including the receipt or animation screens that follow, while the card may still be in the field (KiC integration guide §3.4.14). Call it from onResume() and unregisterActivityForNfc(this) from onPause(). Both are suspend functions that must run on a worker thread.
Visa marks both APIs Implementation: Optional (§3.3.15.1, §3.3.15.2) — a tap can complete without them — but skipping them leaves the tap exposed to being interrupted by another app, so Koard recommends wiring them up on every activity that can host a tap.
class MainActivity : ComponentActivity() {
private val nfcMutex = Mutex()
override fun onResume() {
super.onResume()
lifecycleScope.launch(Dispatchers.IO) {
nfcMutex.withLock {
KoardMerchantSdk.getInstance().registerActivityForNfc(this@MainActivity)
}
}
}
override fun onPause() {
lifecycleScope.launch(Dispatchers.IO) {
nfcMutex.withLock {
KoardMerchantSdk.getInstance().unregisterActivityForNfc(this@MainActivity)
}
}
super.onPause()
}
}
Guard the pair with a mutex (as the demo app does) so a rapid resume/pause cycle cannot interleave the two calls. Also add the TECH_DISCOVERED intent filter and the @xml/nfc_tech_filter tech-list to the same activity in your manifest — per KiC §3.4.14 the registration APIs are meant to be used alongside that manifest declaration, and it is not contributed by the Koard SDK.
unregisterActivityForNfc() is safe to call from onPause() even during a tap: the SDK detects an in-flight tap and skips the unregister. This matters because the Visa kernel foregrounds its own secure Online-PIN keypad, which pauses your activity — tearing down the registration there would abort PIN entry and decline the sale.
Step 6: Authenticate Merchant
After SDK initialization, authenticate the merchant using their credentials:
import androidx.lifecycle.lifecycleScope
import com.koardlabs.merchant.sdk.KoardMerchantSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch(Dispatchers.IO) {
try {
val sdk = KoardMerchantSdk.getInstance()
// Login with merchant credentials
val success = sdk.login(
merchantCode = "your-merchant-code",
merchantPin = "your-merchant-pin"
)
if (success) {
println("Merchant authenticated successfully")
} else {
println("Authentication failed")
}
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
}
}
The login() method authenticates the merchant and retrieves an access token. This must be done before processing transactions. login() returns false rather than throwing when the credentials are rejected.
If your integration has already resolved the merchant identity into a single opaque string (a QR scan, an SSO callback, or a server-issued provisioning token), use the login(alias: String): Boolean overload instead. It hits the same login route and yields the same session token; the alias itself is never stored.
Call logout() to end the session. Logout clears the session token and the active location but preserves enrollment, so the same merchant can log back in without re-enrolling. Switching the device to a different merchant requires an explicit unenrollDevice() first.
Test Basic Import
Add this import statement to verify the SDK is accessible:
import com.koardlabs.merchant.sdk.KoardMerchantSdk
import com.koardlabs.merchant.sdk.domain.KoardEnvironment
import com.koardlabs.merchant.sdk.domain.KoardLocation
import com.koardlabs.merchant.sdk.domain.exception.KoardException
If the imports succeed without errors, your installation is working correctly.
Step 7: Select a Location and Enroll the Device
Both of these steps are mandatory. Enrollment is not automatic — nothing happens as a side effect of login(). Until the device has an active location and is enrolled, sale(), preauth(), completePartialAuth(), and refundEmv() throw KoardException with error.errorType of KoardErrorType.NotReady.NoActiveLocation or KoardErrorType.NotReady.NotEnrolled.
The order is fixed: authenticate, choose a location, then enroll.
import com.koardlabs.merchant.sdk.KoardMerchantSdk
import com.koardlabs.merchant.sdk.domain.exception.KoardException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun setUpDevice() = withContext(Dispatchers.IO) {
val sdk = KoardMerchantSdk.getInstance()
// 1. Fetch the merchant's locations and pick one.
val locations = sdk.getLocations().getOrElse { error ->
println("Could not load locations: ${error.message}")
return@withContext
}
val location = locations.firstOrNull() ?: return@withContext
try {
// 2. Set the active location. Backend-authoritative: it re-binds the
// terminal on the backend and only commits locally if that succeeds.
sdk.setActiveLocation(location.id)
// 3. Enroll the device. Returns a String status; throws on failure.
val status = sdk.enrollDevice()
println("Enrollment status: $status")
} catch (e: KoardException) {
println("Setup failed: ${e.error.shortMessage} (${e.error.errorType})")
}
}
setActiveLocation(locationId)
- Suspend, worker thread only, returns
Unitand throws on failure — it no longer silently no-ops. - On an unenrolled device it simply records the selection and flips readiness to
hasActiveLocation = true. It does not enroll for you; you still have to callenrollDevice(). - If the device is already enrolled and you switch to a different location, the SDK re-binds the terminal on the backend first and commits the new location locally only if the backend approves. A failed re-bind throws and leaves the previous location intact. It then pushes the new location's terminal/CVM profile down to the kernel.
- Switching locations on an enrolled device throws
InvalidRequestwhen developer mode is on — the Visa kernel will not accept a new profile in that state, so the SDK refuses the switch rather than half-applying it. - During the switch, readiness reports
NotReady.Preparing, so a tap started mid-switch is rejected instead of running against a terminal that is still moving.
Observe the resolved location reactively rather than caching your own copy — a private cache survives logout and goes stale, which blocks the next enrollment with "no active location set":
sdk.activeLocation.collect { location -> // StateFlow<KoardLocation?>
updateUi(location?.name)
}
// On a cold start, resolve the persisted id into a full object once:
sdk.refreshActiveLocation() // suspend, returns KoardLocation?
sdk.activeLocationId // String? — the raw persisted id
enrollDevice()
suspend fun enrollDevice(): String — worker thread only. It returns a status String and throws KoardException when it cannot proceed. Before calling it, the SDK refreshes its readiness checks and rejects the call when:
- Developer mode is enabled —
InvalidRequest, "Disable developer mode before enrolling device." - The device is already enrolled —
InvalidRequest, "Clear enrollment data before enrolling again." CallclearEnrollmentState()(local only) orunenrollDevice()(clears the enrollment data held by the Tap to Pay Ready app, then clears local state) first. Note thatunenrollDevice()does not fully deprovision the device on Visa's backend — see Troubleshooting. - No active location is set — enrollment requires one, which is why Step 7 runs in this order.
Because it is a user-visible, failure-prone operation, surface it as an explicit action in your UI (the demo app renders an Enroll Device button whenever readinessState.enrollmentState is not Enrolled) rather than firing it silently on login.
Track progress through sdk.readinessState, whose enrollmentState moves NotEnrolled → Enrolling → Enrolled (or Failed). Use readiness.notReadyReason() to get the typed blocking reason without attempting a transaction.
Step 8: Transaction Processing
With installation complete you can now run Tap to Pay sessions. Keep these guardrails in mind:
sdk.sale(...),sdk.preauth(...),sdk.completePartialAuth(...),sdk.refundEmv(...), andsdk.prepare()are the APIs that drive a reader session, so they return a coldFlowthat drives the reader UI. Other calls reach the thin client too (isKernelAppInstalled(),checkKiCEligibility(),getTransactionConfig(),cancelTransaction(),resetKernelService(),unenrollDevice(), the NFC registration pair), but they are one-shot and emit no reader events.sdk.refund(...)is backend-only — it returnsResult<KoardTransaction>and never involves a tap. Usesdk.refundEmv(...)when the customer must present a card.- Every other payment action (capture, reverse, incremental auth, receipts, tip adjustment) is a suspend function that returns a
Result<T>and never emits reader events. - The SDK enforces readiness (
sdk.readinessState.value.isReadyForTransactions) and blocks taps if the required tap-to-pay dependency is missing, developer mode is on, the device is not enrolled, or no active location is set.
Rather than duplicating code here, the dedicated Running Payments guide shows:
- How the demo’s
MainScreenViewModelbuildsPaymentBreakdown, collects the Flow, and updates UI state - Mapping
KoardReaderStatus+ reader status codes to your own UX - Handling surcharge confirmation, canceling the reader, and orchestrating post-reader calls (capture/refund/reverse/incremental/tip adjust/receipt)
Use that guide as the canonical reference when wiring payments into your application.
Troubleshooting
Common Installation Issues
SDK Not Initialized Error
IllegalStateException: Instance is null. Did you forget to call initialize?
Solution: Ensure KoardMerchantSdk.initialize() is called in Application.onCreate() on a worker thread before accessing getInstance().
Thread Enforcement Error
Solution: All SDK operations must be called from a worker thread. Use Dispatchers.IO:
lifecycleScope.launch(Dispatchers.IO) {
// SDK operations here
}
Dependency Conflicts
Solution: Ensure you're using compatible versions of AndroidX and Kotlin libraries. Check for dependency conflicts with:
./gradlew app:dependencies
Build Errors
Solution:
- Verify you're using JDK 21
- Ensure minimum SDK is set to 31 (Android 12)
- Clean and rebuild:
./gradlew clean build
NFC Transactions Failing
Solution:
- Disable developer mode before running transactions - This is required for security
- Developer mode blocks tap to pay functionality
- Workflow: Enable dev mode → Install app → Disable dev mode → Run transactions
- Go to Settings > System > Developer Options and toggle OFF
- Restart device after disabling developer mode
Verification Checklist
- Android Studio Hedgehog or newer installed
- JDK 21 configured
- Minimum SDK set to 31 (Android 12)
-
mavenCentral()present in repositories - SDK dependency added to build.gradle.kts
- Application class created and registered
- SDK.initialize() called on worker thread
- Required permissions added to manifest (INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE, REBOOT, NFC, WAKE_LOCK)
-
<queries>tag added withcom.visa.kic.app.kernel - NFC
TECH_DISCOVEREDintent filter and@xml/nfc_tech_filtermeta-data added to your tap activity -
registerActivityForNfc()/unregisterActivityForNfc()wired intoonResume()/onPause() -
setActiveLocation(locationId)called after login -
enrollDevice()exposed as an explicit user action and completed successfully - Project builds without errors
- Import statements work correctly
- Tap-to-pay kernel dependency installed on test device (Visa Tap to Pay Ready)
- Physical device with NFC available for testing
- Aware of developer mode workflow (enable → install → disable → run transactions)
Next Steps
Once the SDK is installed and configured:
- Run the Demo App - Test SDK functionality with the demo application
- Supported Devices & NFC Tap Location - Compatible devices, requirements, and where to tap
- Running Payments - Implement tap-to-pay flows, surcharging, and post-reader actions
- SDK Response Codes - Understand transaction outcomes, display messages, and error handling
- Troubleshooting - Fix enrollment failures and taps that cancel instantly
- Understand Payment Lifecycle - Learn about the complete payment flow
See also
This wraps up the SDK installation. See the links below for next steps in your integration:
- Demo App Setup - Run the demo application
- SDK Response Codes - Transaction outcomes, error codes, and display messages
- Payment Lifecycle - Complete payment flow guide
- Supported Devices & NFC Tap Location - Device compatibility and tap guidance
- Troubleshooting - Device-side fixes for enrollment and tap failures
Best Practices
- Idempotency: Generate a unique
eventId(UUID) for every transaction mutation (sale, refund, adjust) and persist it with your POS records. - Threading: Always hop to
Dispatchers.IObefore calling any SDK method; return results to the main thread only after the call completes. - Environment Flavors: Mirror the demo’s
uat/prodflavors so each build points at the correct Koard environment and credential set. UseKoardEnvironment.Custom(...)for anything else. - Logging: The SDK writes to Logcat under the fixed tag
KoardSDK. Set verbosity once viainitialize(..., logLevel = KoardLogLevel.VERBOSE)— there is no runtime setter, and the SDK no longer depends on Timber. Secrets (API key and token headers, device certificates, enrollment key material) are never logged at any level. - Device Re-provisioning: When wiping devices or reinstalling the tap-to-pay kernel app, call
refreshDeviceCertificates()to regenerate keys and rerun enrollment.

