Tap to Pay on Android
This guide outlines how to set up Tap to Pay for Android. For the best results, merchants should implement code that prevents the screen or application from sleeping. If the screen or application sleeps while a transaction is in-flight, processing may be interrupted or fail. Additionally, ensure developer mode on the terminal device is turned off before starting any transaction in production mode.
To use Tap to Pay, you'll need to integrate with Moov's SDKs. Integration consists of two main phases:
- Onboard Terminal Applications – Register terminal applications then link them with a merchant
- Accept and Process Payments – Configure the merchant’s mobile application to process contactless payments
View the Android SDK documentation.
Prerequisites
Devices must meet the requirements laid out in the prerequisite section below.
Operating System
- Android 10.0 (API 29) minimum
- Google Play Services v11+
- Official Android release
Hardware Features
- Active NFC antenna
- ARM processor architecture
- Secure element for key storage
- Unmodified manufacturer firmware
Security Status
- Passes Google Play Integrity verification
- No root access detected
- Original bootloader intact
- Accurate system time
Set up app through Google Play
When distributing the application through Google Play, we recommend limiting the store listing visibility to devices that pass strong integrity checks.
Google Play can verify that devices pass integrity checks before the store listing is made visible to users. Enable this feature to limit your app's distribution to unreliable or unknown devices. This will not affect the app's Google Play discoverability or search ranking.
- Log in to Google Play Console
- Navigate to Release > App Integrity > Store listing visibility
- Enable Strong integrity checks
This process prevents installation on compromised devices that could pose security risks.
Register application
Applications must be registered with Moov to be authorized to use our Tap to Pay SDK. The process for application registration is as follows:
- Create a terminal application via the
POSTterminal application endpoint.
curl -X POST "https://api.moov.io/terminal-applications" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
--data '{
"platform": "android",
"packageName": "com.example.app",
"sha256Digest": "AA:BB:CC:DD:EE:FF:AA:BB:CC:DD:EE:FF:AA:BB:CC:DD:AA:BB:CC:DD:EE:FF:AA:BB:CC:DD:EE:FF:AA:BB:CC:DD",
"versionCode": "20332277"
}'
-
The application will move from
pendingtoenabledonce approved. You can subscribe to theterminalAppRegistration.updatedwebhook to be notified of the status updates. -
Once the application is enabled, you must link the app's
terminalApplicationIDto a merchant account via the linkPOSTterminal application endpoint.
curl -X POST "https://api.moov.io/accounts/{accountID}/terminal-applications" \
-H "Authorization: Bearer {token}" \
--data '{
"terminalApplicationID": "string"
}'
- When a new version of the application is published to Google Play, register this version via the terminal applications version endpoint
POST /terminal-applications/{terminalApplicationID}/versions.
See the Android developer docs for more information on app versioning.
curl -X POST "https://api.moov.io/terminal-applications/{terminalApplicationID}/versions" \
-H "Authorization: Bearer {token}" \
--data-raw '{
"version": "20440059"
}'
Add the SDK to your project
The Android SDK is published to Maven Central as io.moov:android-sdk. It depends on MyPinPad's terminal and EMV libraries, which are hosted in MyPinPad's private Maven repository. Gradle cannot resolve the SDK until the private Maven repository and its credentials are configured.
Contact Moov support at support@moov.io to obtain a username and access token for MyPinPad's Maven repository.
- Store the credentials outside your source tree, either in
~/.gradle/gradle.propertiesor as theMPP_PACKAGES_USERNAMEandMPP_PACKAGES_TOKENenvironment variables. Do not commit them to version control.
# ~/.gradle/gradle.properties
mppPackagesUsername=<username from Moov support>
mppPackagesToken=<token from Moov support>
- Declare MyPinPad's repository alongside Google and Maven Central in
settings.gradle.kts.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/myPinPad/*")
credentials {
username = providers.gradleProperty("mppPackagesUsername")
.orElse(providers.environmentVariable("MPP_PACKAGES_USERNAME"))
.get()
password = providers.gradleProperty("mppPackagesToken")
.orElse(providers.environmentVariable("MPP_PACKAGES_TOKEN"))
.get()
}
}
}
}
- Add the SDK to your app module's
build.gradle.ktsusing the latest version from Maven Central. The SDK uses types such asMIDfrom Moov's Kotlin SDK in its public API, but does not declare that dependency, so you will need to add it. Android SDK0.26.2is built against Kotlin SDK0.8.0.
android {
defaultConfig {
minSdk = 29
}
}
dependencies {
implementation("io.moov:android-sdk:<version>")
implementation("io.moov:kotlin-sdk:0.8.0")
}
Initialize SDK
SDK initialization requires MoovSDK.onApplicationCreated to be called in the app's Application.onCreate method. Additionally, MoovSDK.isApplicationProcess should be used to determine whether or not the app should proceed with any initialization logic in its onCreate method.
onApplicationCreated takes an SDKLogger, which receives log lines for debugging failed taps. These lines never contain PCI or PII data. Include them when reporting production tap issues to Moov support.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// initialize the Moov SDK
MoovSDK.onApplicationCreated(this, SDKLogger { line -> Log.d("MoovSDK", line) })
// return if not being called from the main process
if (!MoovSDK.isApplicationProcess()) {
return
}
// proceed to initialize the app
}
}
Create terminal
The main entry point is a factory method: MoovSDK.createTerminal. This initializes a terminal instance which ensures a secure environment. This method should be called as early in the application's lifecycle as is practical, as it may take a significant amount of time to complete the device attestation.
Pass it the terminal configuration returned by the get terminal application configuration endpoint. Fetch the configuration from your backend. createTerminal returns a TerminalCreationResult. On success, TerminalCreationResult.Created holds the Terminal. Other results describe why creation failed, such as a failed device attestation.
val terminalConfig = fetchTerminalConfig() // fetch the terminal config from your backend
val terminal = when (val result = MoovSDK.createTerminal(applicationContext, terminalConfig)) {
is TerminalCreationResult.Created -> result.terminal
else -> return // handle the failure
}
Call terminal.dispose() when the app no longer needs the terminal, for example when the user leaves the area of the app that accepts taps. You do not need to dispose of the terminal when the app moves to the background.
Refresh the terminal configuration
Terminal configurations expire. Fetch a fresh configuration rather than caching one indefinitely.
If the configuration has expired, createTerminal returns TerminalCreationResult.TerminalConfigurationExpired. Fetch a new configuration and call createTerminal again.
To refresh before that happens, read Terminal.configurationExpiresAt while the terminal is idle. It is null for sandbox terminals, in which configurations do not expire.
val expiresAt = terminal.configurationExpiresAt
if (expiresAt != null && Instant.now().isAfter(expiresAt.minus(Duration.ofHours(2)))) {
// refresh during a quiet moment rather than with a customer waiting
}
If the configuration expires while the app is running, createTapAuthorization and createTapTransfer emit TapAuthorizationError.ConfigurationExpired without reading a card. The terminal cannot be reused. Dispose of it, create a new terminal from a fresh configuration, and authorize again. Creating a terminal repeats device attestation, so refreshing ahead of time keeps a customer from waiting on it.
The SDK holds the configuration in memory only. After process death, or a long period in the background, call createTerminal again.
Accept a payment
Creating an EMV authorization will grab exclusive access to the device's NFC reader, allowing tap of an EMV card. Both paths below return a Kotlin Flow of events. UiMessageEvent values, such as PresentCard and RemoveCard, tell you what to display to the customer during the tap.
Amounts are a TapAmount in the smallest currency unit. For example, 100uL is $1.00 USD. To record a tip or surcharge, set TapAmount.tip or TapAmount.surcharge. Both are portions of amount, not additions to it, and together they cannot exceed amount. A surcharge requires the merchant to be enabled for surcharging, otherwise the flow emits TapAuthorizationError.SurchargeNotEnabled.
TapAmount(
amount = 1150uL, // 1000 base + 100 tip + 50 surcharge
currency = Currency.getInstance("USD"),
tip = 100uL,
surcharge = 50uL,
)
There are two main paths to accept EMV payments:
- Directly create a
transferfrom the SDK - Create an authorization via the SDK and create the
transferseparately
This method accepts the EMV tap, authorizes the transaction with the card network and creates a transfer on the Moov platform. The flow emits CreateTransferEvent.Success with the created transfer.
Creating the transfer requires a Moov API client in the SDK. Create the terminal with the createTerminal overload that accepts a ClientConfiguration and the merchant's accountID instead of a configuration string. The overload fetches the terminal configuration. See the createTerminal reference.
lifecycleScope.launch {
terminal.createTapTransfer(
CreateTapTransferParams(
tapAuthorizationParams = TapAuthorizationParams(
tapAmount = TapAmount(
amount = 100uL,
currency = Currency.getInstance("USD"),
),
customerAccountId = MID("962aa592-5993-49d6-ae54-d6d6ab917c66"),
),
partnerAccountId = MID("fd35bc72-fa52-4fef-a0f5-f99fa1280aeb"),
merchantAccountId = MID("3c1a3d1e-0f3c-4a5e-9a3e-6c2f0b7c5d11"),
destinationPaymentMethodId = MID("19a76ccb-be73-4394-a9ca-701c0a20fd8a"),
description = "Moov T-shirt",
)
).collect { event ->
when (event) {
is UiMessageEvent -> showMessage(event)
is CreateTransferEvent.Success -> Log.d("MoovSDK", "Transfer created: ${event.transfer}")
else -> Log.d("MoovSDK", "Event: $event")
}
}
}
This method accepts the EMV tap, authorizes the transaction with the card network, and emits AuthorizationResultEvent.TransactionAuthorized with a token which can be passed into the create transfer API.
If the authorization token expires before creating a transfer, the card authorization is reverted (voided). You should ensure transfers are created promptly after receiving the token and within the token’s validity period (set in the exp claim of the JWT). JWTs with an expired exp claim will not be accepted for processing.
lifecycleScope.launch {
terminal.createTapAuthorization(
TapAuthorizationParams(
tapAmount = TapAmount(
amount = 100uL,
currency = Currency.getInstance("USD"),
),
customerAccountId = MID("962aa592-5993-49d6-ae54-d6d6ab917c66"),
)
).collect { event ->
when (event) {
is UiMessageEvent -> showMessage(event)
is AuthorizationResultEvent.TransactionAuthorized -> {
// now, create the transfer externally using event.token as the source
}
else -> Log.d("MoovSDK", "Event: $event")
}
}
}
Test mode
Test mode allows you to explore and verify your integration with the Moov SDK before processing live payments. The terminal configuration fetched from the Moov API determines whether the SDK operates in test or production mode. When a terminal is created with a test configuration, the returned Terminal implements the SandboxTerminal interface, which provides access to testing specific functionality.
To use test mode, you'll need to use a bearer token, which is generated when you create a Moov account. You can check for test mode and access specific methods to test different card scenarios:
val result = MoovSDK.createTerminal(context, config)
if (result is TerminalCreationResult.Created) {
val terminal = result.terminal
if (terminal is SandboxTerminal) {
// Access sandbox-specific methods
terminal.getTestCases() // Browse available test cards
terminal.selectTestCard("4000000000000010") // Select decline card
}
}
Additionally, you can add a dropdown menu to your application's UI that allows users to select a test card. We recommend placing this dropdown on the same screen as the transaction amount entry field. Note that this feature is available only in test mode — when running in production, users must tap a physical card on the device.
Test cards
Use getTestCases() to retrieve the list of available test cards. Each test card is tied to a specific authorization outcome, allowing you to test both approve and decline scenarios. See the Tap to Pay section of the test mode data reference for available test cards.
Call selectTestCard() with the desired card number before initiating a tap authorization. The SDK will simulate that card being tapped, returning the corresponding approve or decline response without requiring a physical card tap.