Integrating Tap to Pay in Your Android App

With Mollie’s Tap App, Tap to Pay functionality can be seamlessly integrated into your Android application. The integration enables processing contactless card transactions by invoking Mollie Tap via Android app-to-app intents, ensuring secure communication during payment processing.

The integration consists of the following steps that we will cover in detail in this article:

  1. Register your app with Mollie to receive security credentials
  2. Install and pair Mollie Tap
  3. Implement message signing for secure communication
  4. Initiate payments from your Android app
  5. Handle payment responses and webhooks

Basic payment flow

Basic Tap to Pay flow follows these essential steps:

  • A customer decides to buy something on your website
  • You initiate a transaction in the Tap App
  • Your customer taps a card to finalize a contactless payment
  • A Confirmation page is shown in your App

What you need to do in advance

Before you start, you need to:

  • Have an Android application with a registered bundle ID
  • Have an active Mollie account (merchant or partner account)

Setting up Tap to Pay in your App

Step 1: Register your Android App with Mollie

To begin integration, you need to register your Android App with Mollie:

  1. Submit your Android App bundle ID (e.g., com.example.myapp) to our Support team.
  2. You will receive:
    • A secret key (secure key for message signing)
    • A secret ID (prefixed with possecr_)

These credentials enable secure communication between your App and Mollie Tap.

Step 2: Install and pair Mollie Tap

Activating Mollie Tap is done in a few simple steps. All you need is Mollie Tap (download it here) and a QR code that you can retrieve from Mollie Web App.

Activating Mollie Tap

  1. In Mollie Tap, click Start setup
  2. Scan the QR code in your dashboard with the Tap App, or type in the set-up code
  3. Allow the required permissions
  4. Click Next; your device will complete the setup

After successful pairing, the device appears as a terminal in your Mollie Dashboard.

Retrieving a QR code

  1. In your Mollie Web App, navigate to Point-of-Sale
  2. Click Add Terminal at the top right corner, then select Activate Tap
  3. Choose the profile you want to process payments with along with features you want to enable and click Continue
  4. A QR code will be displayed on your screen.
    You will also find a 20-digits setup code that you can type in manually (if you cannot scan the QR code on your device)

For Merchant Deployments:

If you're developing an app for other Mollie merchants, they need to:

  1. Install both your App and Mollie Tap
  2. Pair the Tap App with their own Mollie account using their pairing code

Multiple devices can be paired using the same pairing code, allowing efficiently onboarding multiple devices.

Step 3: Implement message signing

Secure communication between your App and Mollie Tap requires message signing. This involves:

Signing Outgoing Messages:

  1. Arrange all parameters of the message (including secret ID) in alphabetical order by name
  2. Convert all parameter values to strings
  3. Convert the collection of parameters into a JSON object string
  4. Generate an HMAC signature using SHA-256 and your secret key
  5. Add this signature to the message as a signature parameter

Verifying Incoming Messages:

When receiving messages from Mollie Tap:

  1. Extract the signature parameter from the received message
  2. Follow the signing process using the remaining parameters to generate your own signature
  3. Verify that the received signature matches your generated signature
import java.util.Formatter
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class HmacSignatureHelper(secret: String) {
    private val mac = Mac.getInstance("HmacSHA256")

    init {
        mac.init(SecretKeySpec(secret.toByteArray(), "HmacSHA256"))
    }

    fun sign(input: String): String {
        return formatBytesToString(mac.doFinal(input.toByteArray()))
    }

    fun verify(input: String, signature: String): Boolean {
        return sign(input) == signature
    }

    private fun formatBytesToString(bytes: ByteArray): String {
        val formatter = Formatter()
        bytes.forEach { formatter.format("%02x", it) }
        return formatter.toString()
    }
}

Step 4: Initiate payment

To process a payment, your App needs to send an app-to-app intent to Mollie Tap:

  1. Create a payment request with required details
  2. Sign the payment request
  3. Send an intent to Mollie Tap

Payment Request Parameters

ParameterRequiredTypeDescriptionExample
amountYesObjectPayment amount information
amount.currencyYesStringThree-letter ISO currency codeEUR
amount.valueYesStringString representation of the amount10.00
appIdYesStringYour Android application bundle IDcom.example.myapp
applicationFeeNoObject | nullSpecifies a fee that your application will receive (for Mollie Connect integrations only)
applicationFee.amountYesObjectFee amount information
applicationFee.amount.currencyYesStringThree-letter ISO currency code (must match payment currency)EUR
applicationFee.amount.valueYesStringString representation of the fee amount0.50
applicationFee.descriptionYesStringDescription of what the fee is forService fee
descriptionYesStringCustomer-facing payment descriptionOrder #1234
referenceIdNoStringYour unique reference for this transaction. Maximum 255 characters.order_1234567890
secretIdYesStringThe secret ID provided by Molliepossecr_5SiRqgL7EL3kdj9eLPVUK
webhookUrlNoStringURL that will receive HTTP POST notifications about payment status changes. Must be accessible from the internet.https://example.com/webhook/mollie

The Payload JSON Structure

The payment request is sent as a JSON payload within the intent. This payload consists of:

  1. Payment parameter - all the payment parameters (required and optional) organized as a single JSON object.
  2. Signature parameter - the HMAC-SHA256 string signed using the payment request.
{
    "amount": {
        "currency": "EUR",
        "value": "10.00"
    },
    "appId": "com.example.myapp",
    "description": "Order #1234",
    "referenceId": "order_1234567890",
    "secretId": "possecr_5SiRqgL7EL3kdj9eLPVUK",
    "webhookUrl": "https://example.com/webhook/mollie"
}

This complete JSON is then sent to Mollie Tap as a payload string parameter in the intent.

Optional: Monetizing with Application Fee

If you're processing payments on behalf of other Mollie merchants (using Mollie Connect), you can add an Application fee to the payment:

{
    "amount": {
        "currency": "EUR",
        "value": "10.00"
    },
    "appId": "com.example.myapp",
    "applicationFee": {
        "amount": {
            "currency": "EUR",
            "value": "0.50"
        },
        "description": "Service fee"
    },
    "description": "Order #1234",
    "referenceId": "order_1234567890",
    "secretId": "possecr_5SiRqgL7EL3kdj9eLPVUK",
    "webhookUrl": "https://example.com/webhook/mollie"
}

Important considerations for Application Fees:

  • The fee currency must match the payment currency
  • Ensure your fee leaves enough room for Mollie's processing fees
  • The fee amount will be automatically deducted from the merchant's payment and transferred to your account
  • The fee description will appear in your platform transactions
import kotlinx.serialization.json.Json

private const val PAYMENT_REQUEST_CODE = 1015 // Can be any integer value

private val json = Json {
    coerceInputValues = false  // Don't allow type coercion
    encodeDefaults = true  // Always include default values
    explicitNulls = true  // Be explicit about null values
    ignoreUnknownKeys = false  // Don't allow extra fields
    isLenient = false  // Strict JSON parsing
    prettyPrint = false  // Avoid additional whitespace
}

private val signatureHelper = HmacSignatureHelper("your-secret-key")

fun main() {
    val paymentRequest = PaymentRequest(
        amount = Amount("EUR", "0.01"),
        appId = "com.example.myapp",
        applicationFee = null, // Add fee, if needed
        description = "This is an example description",
        referenceId = UUID.randomUUID().toString(),
        secretId = "possecr_5SiRqgL7EL3kdj9eLPVUK",
        webhookUrl = "https://example.com/webhook/mollie"
    )

    val paymentRequestString = json.encodeToString(PaymentRequest.serializer(), paymentRequest)

    startActivityForResult(Intent().apply {
        component = ComponentName(
            "com.mollie.pos",
            "com.mollie.pos.MainActivity"
        )
        putExtra("payload", paymentRequestString)
        putExtra("signature", signatureHelper.sign(paymentRequestString))
    }, PAYMENT_REQUEST_CODE)
}

Step 5: Handle payment results

After processing the payment, Mollie Tap will return the following information:

Payment Result Parameters

ParameterRequiredTypeDescriptionExample
failureMessageYesString | nullCustomer-friendly explanation if payment failedInsufficient funds
failureSupportCodeYesString | nullMachine-readable failure codeT.12500.-81
paymentIdYesString | nullMollie's unique payment ID (may be null, for example, if there’s no connectivity)tr_WDqYK6vllg
referenceIdYesString | nullYour original reference IDorder_1234567890
signatureYesStringMessage signature for verificationa1b2c3d4e5f6g7h8i9j0...
statusYesStringPayment status (paid or failed)paid

Handling the result in your App

override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
    super.onActivityResult(requestCode, resultCode, intent)
    
    if (intent == null || requestCode != PAYMENT_REQUEST_CODE) {
        return
    }

    val failureMessage = intent.getStringExtra("failureMessage")
    val failureSupportCode = intent.getStringExtra("failureSupportCode")
    val paymentId = intent.getStringExtra("paymentId")
    val referenceId = intent.getStringExtra("referenceId")
    val signature = intent.getStringExtra("signature")
    val status = intent.getStringExtra("status")
    
    if (signature == null) {
        // handleMissingSignature()
        return
    }

    val paymentResult = PaymentResult(failureMessage, failureSupportCode, paymentId, referenceId, status)

    val paymentResultString = json.encodeToString(PaymentResult.serializer(), paymentResult)
    
    if (!signatureHelper.verify(paymentResultString, signature)) {
        // handleInvalidSignature()
        return
    }

    when (status) {
        "paid" -> {
            // Do something in your codebase when the payment is paid
            handleStatusPaid(paymentId, referenceId)
        }
        "failed" -> {
            // Do something in your codebase when the payment has failed
            handleStatusFailed(failureMessage, failureSupportCode)
        }
    }
}

Important Considerations when handling results:

  1. Verify the signature: always verify the signature to ensure the response is authentic
  2. Rely on webhooks: the app-to-app intent might not return if the payment process is interrupted
  3. Handle all statuses: implement handling for all possible payment statuses
  4. Check for app installation: the intent fails if Mollie Tap is not installed

Step 6: Implement webhook handling

Webhooks allow your application to receive real-time updates about the status of payments and other objects in the Mollie system. Instead of constantly polling for status changes, webhooks push notifications to your application when events occur.

How webhooks work with Tap to Pay integration

When integrating with our partner platform, webhooks work as follows:

  1. During payment creation, the webhook URL is automatically configured based on your platform settings
  2. When a payment status changes (e.g., from pending to paid), Mollie sends a POST request to your webhook URL
  3. The POST request contains a single parameter: id with the value of the payment ID (e.g., id=tr_d0b0E3EA3v)
  4. Your application uses this id to fetch the latest payment status from Mollie's API
  5. Based on the updated status, you can trigger actions in your system (e.g., fulfill orders, update inventory etc)

Example webhook request

POST /payments/webhook HTTP/1.0
Host: yourdomain.example.org
Content-Type: application/x-www-form-urlencoded
Content-Length: 16

id=tr_d0b0E3EA3v

Implementation best practices:

  • Always verify latest payment status: always fetch payment's current (latest) status using the Get payment API endpoint
  • Return 200 OK response: Mollie expects a 200 OK response to confirm receipt of the webhook
  • Handle idempotency: the same webhook might be sent multiple times, so ensure your processing logic can handle duplicates
  • Implement error handling: if your endpoint is temporarily unavailable, Mollie will retry several times
  • Asynchronous handling: it’s possible that the webhook will be sent earlier than the intent result

For more detailed information about webhooks (including advanced configuration options and troubleshooting), refer to Webhooks.

Testing your integration

Before deploying to production, test your integration thoroughly:

  1. Test app-to-app communication with Mollie Tap
  2. Verify signature generation and validation
  3. Test all payment scenarios (success, failure, cancellation etc)
  4. Verify webhook handling on your backend
  5. Test error scenarios (app not installed, network issues, etc.)

Troubleshooting

If the problem you are facing is not listed below, please contact our Support team.

Mollie Tap not found

If the intent fails because Mollie Tap is not installed, direct the user to install it from the Google Play Store.

Payment flow interrupted

If the payment flow is interrupted (device shutdown, app crash, etc.), always check the webhook status before considering a payment failed. Currently, we don’t have support for app-to-app intent to request a payment status.

Signature validation errors

Double-check that:

  • All parameters are ordered alphabetically by name
  • All values are properly converted to strings
  • The correct secret key is being used

Device connectivity

Payment processing requires internet connectivity. Implement proper error handling for offline scenarios.

Version compatibility

Ensure you're using the latest version of Mollie Tap. Some features may not work with older versions.

Data Structures reference

import kotlinx.serialization.Serializable

@Serializable
data class Amount(
    val currency: String,
    val value: String
)

@Serializable
data class ApplicationFee(
    val amount: Amount,
    val description: String
)

@Serializable
data class PaymentRequest(
    val amount: Amount,
    val appId: String,
    val applicationFee: ApplicationFee?,
    val description: String,
    val referenceId: String,
    val secretId: String,
    val webhookUrl: String?
)

@Serializable
data class PaymentResult(
    val failureMessage: String?,
    val failureSupportCode: String?,
    val paymentId: String?,
    val referenceId: String?,
    val status: String,
)

Additional support and resources

If you encounter issues with your integration, refer to the following resource: Android Intent Documentation or contact our Support team.