# Tap to Pay on Android

> Accept contactless card payments in person on Android devices with Moov's Android SDK, from terminal configuration through authorization and transfer.

> AI agents: search and read all Moov docs with the docs MCP server at https://docs.moov.io/mcp (no API key). Setup: https://docs.moov.io/guides/developer-tools/mcp-ai/docs-mcp/index.md

Accept contactless cards and mobile wallets in person on an Android device's built-in NFC reader by integrating Moov's [Android SDK](https://moovfinancial.github.io/moov-android/moov-android/io.moov.sdk/index.html).

## Prerequisites

| Requirement               | Details                                                                                                                                                                                                                                               |
|---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| OS                        | Android 10 (API 29) or later (`minSdk = 29`), an official Android release, and Google Play Services v11+                                                                                                                                              |
| Hardware                  | Active NFC antenna, ARM processor, secure element for key storage, unmodified manufacturer firmware                                                                                                                                                   |
| Device security           | Passes Google Play Integrity verification, not rooted, original bootloader intact, accurate system time                                                                                                                                               |
| SDK                       | `io.moov:android-sdk` from Maven Central plus `io.moov:kotlin-sdk` (Android SDK `0.26.2` is built against Kotlin SDK `0.8.0`)                                                                                                                         |
| Private Maven credentials | A username and token for MyPinPad's Maven repository, from [Moov support](mailto:support@moov.io)                                                                                                                                                     |
| Moov capability           | The merchant needs `collect-funds.card-payments` and `wallet.balance`, the same as for online card payments ([capabilities](/guides/accounts/capabilities/index.md)). The buyer needs no capability; transfers use the `card-present-payment` payment method. |
| Moov registration         | A terminal application registered with Moov, `enabled`, and linked to the merchant account (step 2 below)                                                                                                                                             |
| Test mode                 | A test mode terminal configuration gives you a `SandboxTerminal` with simulated test cards; see [test mode](#test-mode)                                                                                                                               |

Before processing in production:

- Prevent the screen or app from sleeping while a transaction is in flight. If it sleeps, processing may be interrupted or fail.
- Turn off **developer mode** on the device.

Setup has two phases: register your terminal application with Moov and link it to a merchant, then configure the merchant's app to accept payments.

## 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.

1. Log in to [Google Play Console](https://play.google.com/console/)
2. Navigate to **Release &gt; App Integrity &gt; Store listing visibility**
3. 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](https://moovfinancial.github.io/moov-android/moov-android/io.moov.sdk/index.html). The process for application registration is as follows:

1. Create a terminal application via the `POST` terminal application [endpoint](/api/sources/terminal-applications/create/index.md).

```zsh
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"
}'
```

2. The application will move from `pending` to `enabled` once approved. You can subscribe to the `terminalAppRegistration.updated` webhook to be notified of the status updates.
3. Once the application is enabled, you must link the app's `terminalApplicationID` to a merchant account via the link `POST` terminal application [endpoint](/api/sources/terminal-applications/link/index.md).

```zsh
curl -X POST "https://api.moov.io/accounts/{accountID}/terminal-applications" \
  -H "Authorization: Bearer {token}" \
  -H "x-moov-version: v2026.07.00" \
  --data-raw '{
    "terminalApplicationID": "12345678-1234-1234-1234-123456789012"
  }'
```

4. When a new version of the application is published to Google Play, register this version via the terminal applications version [endpoint](/api/sources/terminal-applications/create-versions/index.md) `POST /terminal-applications/{terminalApplicationID}/versions`.

See the [Android developer](https://developer.android.com/studio/publish/versioning) docs for more information on app versioning.

```zsh
curl -X POST "https://api.moov.io/terminal-applications/{terminalApplicationID}/versions" \
  -H "Authorization: Bearer {token}" \
  -H "x-moov-version: v2026.07.00" \
  --data-raw '{
    "version": "20440059"
  }'
```

## Add the SDK to your project

The Android SDK is published to [Maven Central](https://central.sonatype.com/) 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](mailto:support@moov.io) to obtain a username and access token for MyPinPad's Maven repository.

1. Store the credentials outside your source tree, either in `~/.gradle/gradle.properties` or as the `MPP_PACKAGES_USERNAME` and `MPP_PACKAGES_TOKEN` environment variables. Do not commit them to version control.

```properties
# ~/.gradle/gradle.properties
mppPackagesUsername=<username from Moov support>
mppPackagesToken=<token from Moov support>
```

2. Declare MyPinPad's repository alongside Google and Maven Central in `settings.gradle.kts`.

```kotlin
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()
      }
    }
  }
}
```

3. Add the SDK to your app module's `build.gradle.kts` using the latest version from [Maven Central](https://central.sonatype.com/artifact/io.moov/android-sdk). The SDK uses types such as `MID` from Moov's [Kotlin SDK](https://central.sonatype.com/artifact/io.moov/kotlin-sdk) in its public API, but does not declare that dependency, so you will need to add it. Android SDK `0.26.2` is built against Kotlin SDK `0.8.0`.

```kotlin
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.

```kotlin
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](https://moovfinancial.github.io/moov-android/moov-android/io.moov.sdk/-moov-s-d-k/-factory/create-terminal.html). 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](/api/sources/terminal-applications/get-configuration/index.md). 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.

```kotlin
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 test mode terminals (`SandboxTerminal`), whose configurations do not expire.

```kotlin
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](/guides/accept-payments/cards/surcharges/index.md), otherwise the flow emits `TapAuthorizationError.SurchargeNotEnabled`.

```kotlin
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 `transfer` from the SDK
- Create an authorization via the SDK and create the `transfer` separately

**Directly create transfer**

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](https://moovfinancial.github.io/moov-android/moov-android/io.moov.sdk/-moov-s-d-k/-factory/create-terminal.html) reference.

```kotlin
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")
    }
  }
}
```

**SDK authorization & transfer API**

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/money-movement/transfers/create/index.md) 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.

```kotlin
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")
    }
  }
}
```

The transfer's `source.cardDetails.status` moves to `confirmed` once the card network authorizes it. Subscribe to the `transfer.updated` webhook to track it to `completed`. See [card transfer statuses](/guides/accept-payments/cards/timing/index.md#card-transfer-statuses).

## Test mode

[Test mode](/guides/get-started/api-authentication/test-mode/index.md) 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:

```kotlin
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](/guides/get-started/api-authentication/test-mode/index.md#link-cards--card-acceptance) and [decline](/guides/get-started/api-authentication/test-mode/index.md#declined-card-transfers) scenarios. See the [Tap to Pay](/guides/get-started/api-authentication/test-mode/index.md#tap-to-pay-on-mobile) 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.

