If you want to accept Tap to Pay payments on your Android mobile device but do not want to integrate our POS Mobile SDK, you can download the Adyen Payments app from the Google Play store. You can then use the Payments app to connect your own POS app to the plataforma de pagamentos da Adyen and authenticate payment requests in a compliant way.
This page explains how to build the Payments app solution. Before you begin, see Understand the solutions to help you decide if the Payments app is the right solution for your situation.
Requirements
Before you begin, take into account the following requirements, limitations, and preparations.
Requirement | Description |
---|---|
Integration type | Your POS app must be integrated with Terminal API. |
API credentials | An API key specifically for the Adyen Payments app, with a client key and the Adyen Payments app role. |
Hardware | An Android commercial off-the-shelf (COTS) mobile device with an integrated NFC reader. Must not be a payment terminal. See Android system requirements for the full hardware and software requirements. |
Limitations | See the countries/regions, payment methods, and functionality that we support. |
Setup steps | Before you begin:
|
How it works
To build an Android Payments app solution for Tap to Pay:
- Build the boarding flow. The Payments app must be boarded on every Android mobile device where the app is installed.
- Build the payment flow.
- Implement an API request to revoke an app instance for cases when the mobile device is defective, stolen, or no longer needed for payments.
App links or deep links
When building the boarding flow and the payment flow, you will use Android App Links to call specific parts of the Android Payments app. Older, existing solutions use deep links instead.
- New integrations are required to use App Links.
- Existing integrations can continue to use deep links for the moment, but we strongly recommend migrating to App Links.
The instructions have details for both, until deep links are no longer supported.
1. Board the app
After you download the Payments app from the Google Play store to a mobile device, you need to board the Payments app on that device. Boarding is a three-step process:
-
Check the boarding status.
From your POS app, you call a link to check if the Payments app is boarded. If the Payments app is not boarded, you receive a "boarding request token". -
Authenticate the app instance.
From your back-end, you send an API request that includes the boarding request token. In the response, you receive a one-time boarding token that is unique for the combination of Payments app instance and device. -
Finish boarding the app.
From your POS app, you call a link that includes the boarding token. The response confirms that the Payments app is now boarded, and includes an installation ID that you need to pass in your transaction requests.
1.1 Check the boarding status
To start the boarding flow, you call an App Link from your POS app to check if the Adyen Payments app is already boarded.
Existing solutions using deep links should migrate to Android App Links.
-
From your POS app, call the link:
The App Link format is as follows:
https://www.adyen.com/test/boarded?returnUrl=URLEncoder(your_scheme)
In this format:
https://www.adyen.com/test/boarded
is the test base URL and path (/boarded
) to use for boarding checks.returnUrl
is the URL-encoded URL where you want to receive the response.
The following example shows how you could build the App Link in your Kotlin project.
Build an App Link to check if the Payments app is boardedExpand viewCopy link to code blockCopy codeprivate fun createOnboardingCheckApplink( returnUrl: String, ): Uri { // Encode the returnUrl to ensure it is safe for use as a URL query parameter. val returnUrlEncoded = URLEncoder.encode(returnUrl, Charsets.UTF_8.name()) return Uri.parse("${BuildConfig.APP_LINK_URL}/boarded").buildUpon() .appendQueryParameter("returnUrl", returnUrlEncoded) .build() } App Link exampleExpand viewCopy link to code blockCopy codehttps://www.adyen.com/test/boarded?returnUrl=your-scheme-test%3A%2F%2Fyour_company.example.com%2Fonboarding
-
Check the response and decide what to do next:
- If
boarded
is true: save theinstallationId
, which you will need in your payment requests. You do not need to continue with the boarding process and you are ready to make a payment. - If
boarded
is false: pass theinstallationId
andboardingRequestToken
to your back-end and go to step 2 of the boarding process.
The response consists of the URL-encoded base URL where you want to receive the response and the following parameters:
Parameter Required Type Description boarded
Boolean Indicates if the app is boarded or not. installationId
String The unique identifier of the Payments app instance on the specific device. When creating Terminal API requests on your back-end, use this as thePOIID
in theMessageHeader
of the request.boardingRequestToken
String The app token used to get the boardingToken
from Adyen.Example responseExpand viewCopy link to code blockCopy code{YOUR_SCHEME}%3A%2F%2Fyour_company.example.com%2Fonboarding?boarded=false&installationId=<INSTALLATION_ID>&boardingRequestToken=<BOARDING_REQUEST_TOKEN>
- If
1.2. Authenticate the app instance
If you determined in step 1 of the boarding process that the Payments app is not boarded yet, you send a Payments app API request from your back-end to authenticate your app installation. In the response you receive a boardingToken
. You only need to authenticate the app instance once.
To authenticate the app:
-
Depending on whether you want to accept payments using your merchant account or a store, make a POST request to either /merchants/merchantId/generatePaymentsAppBoardingToken or /merchants/merchantId/stores/storeId/generatePaymentsAppBoardingToken, specifying:
-
Path parameters:
Parameters Required Description merchantId
The unique identifier of the merchant account. storeId
The unique identifier of the store. ThestoreId
is not the same as storereference
. If you only know the reference, you can use it to get thestoreId
with an API call. -
Request parameters:
Parameters Required Description boardingRequestToken
The app token used to request a boardingToken
from Adyen. You received the boarding request token in step 1 of the boarding process.
Create boarding token requestExpand viewCopy link to code blockCopy codecurl https://management-test.adyen.com/v1/merchants/{merchantId}/generatePaymentsAppBoardingToken \ -H 'x-API-key: ADYEN_API_KEY' \ -H 'content-type: application/json' \ -X POST \ -d '{ "boardingRequestToken": "BOARDING_REQUEST_TOKEN" }' -
-
From the response, save the
boardingToken
, which you will need in the last step of the boarding process.
The response consists of the following parameters:Parameter Type Description installationId
String The unique identifier of the Payments app instance on the specific device. boardingToken
String The one-time token used to authenticate the app installation. Valid for one hour. Create boarding token responseExpand viewCopy link to code blockCopy code{ "installationId": "INSTALLATION_ID", "boardingToken": "BOARDING_TOKEN" }
1.3. Finish boarding the app
When you have received the boardingToken
from Adyen, you need to send the boarding token to the Payments app to finish boarding the app and prepare for making payments.
Existing solutions using deep links should migrate to Android App Links.
-
From your POS app, call the link:
The App Link format is as follows:
https://www.adyen.com/test/board?boardingToken=Base64URL{APP_BOARDING_TOKEN}&returnUrl=URLEncoder(your_scheme)
In this format:
https://www.adyen.com/test/board
is the test base URL and path (/board
) to use for boarding requests.boardingToken
is the token generated for thisinstallationId
in step 2 of the boarding process. This token must be Base64URL-encoded.returnUrl
is the URL-encoded URL where you want to receive the onboarding result (the response).
The following example shows how you could build the App Link in your Kotlin project.
Build an App Link to board the Payments appExpand viewCopy link to code blockCopy codeprivate fun createBoardingRequestApplink( returnUrl: String, boardingToken: String, ): Uri { // Encode the boarding token using Base64 URL encoding. val encodedBoardingToken = Base64.getUrlEncoder().encodeToString(boardingToken.toByteArray()) // Encode the return URL to ensure it is safe for URL query parameters. val encodedUrl = URLEncoder.encode(returnUrl, Charsets.UTF_8.name()) return Uri.parse("${BuildConfig.APP_LINK_URL}/board").buildUpon() .appendQueryParameter("boardingToken", encodedBoardingToken) .appendQueryParameter("returnUrl", encodedUrl) .build() } App Link exampleExpand viewCopy link to code blockCopy codehttps://www.adyen.com/test/board?boardingToken=ZXlKaGJHY2lPaUpGVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBZWFFpT2pFM05ERTROalkyT0RFMU9EWXNJbVY0Y0NJNk1UYzBNVGczTURJNE1UVTROaXdpYW1GaGMxVnpaWEpMWlhraU9pSjNjMTg0TXpZeU16RkFRMjl0Y0dGdWVTNVFZWGx0Wlc1MGMwMWhaR1ZGWVhONUlpd2liV1Z5WTJoaGJuUkJZMk52ZFc1MFEyOWtaU0k2SWsxcFkydE9kV2RuWlhSeklpd2lhVzV6ZEdGc2JHRjBhVzl1U1dRaU9pSkdOamxDUWprM05pMDJNRFkxTFRRME5URXRPVUZGTnkwMVFUaEJPVGsyUmpaQk1qa2lMQ0oxUTJGd2NFdGxlU0k2SWsxR2EzZEZkMWxJUzI5YVNYcHFNRU5CVVZsSlMyOWFTWHBxTUVSQlVXTkVVV2RCUlcxTmRqWmhTbTlSWkdabGFXbG5SWGRSVmxjMVlWSkJkM05ZZG1OaGMweGhTVk52ZGpnM05Hc3JUbEZFT0VsRVJXaDRLM1V5UTJkdVVVZDVXalpsU0RKNk5uWTJSVVpEU1U5VU5UWm5kbUpXV1VOeVozQjNQVDBpTENKaWRXNWtiR1ZKWkNJNkltTnZiUzVoWkhsbGJpNXBjSEF1bFc5aWFXeGxMbU52YlhCaGJtbHZiaTVrWlhZaUxDSnViMjVqWlNJNklrVjVTbkpFWkRGWE4zWjZiekJFVURraUxDSm9iM04wY3lJNmV5SmpiMjVtYVdkMWNtRjBhVzl1SWpvaWFIUjBjSE02THk5amFHVmphMjkxZEhCdmN5MTBaWE4wTG1Ga2VXVnVMbU52YlM5amFHVmphMjkxZEhCdmN5SjlMQ0owZVhCbElqb2lRazlCVWtSSlRrZGZWRTlMUlU0aWZRLkxXOS1mZ3g5V201dzVlMFlQdTNzRFAwbFZpNmQyQ21vRmpPQ0plcVlUQTl3ckM5NGNjeVJPaW0wbGJJeU9FbnJpSXVxMlVZWEJ2bGR2dmNPalVpeEhn&returnUrl=your-scheme-test%3A%2F%2Fyour_company.example.com%2Fonboardingresult
-
Check the response and decide what to do next:
- If
boarded
is true: save theinstallationId
. You need this when you make a payment. You do not need board the app again. - If
boarded
is false: check theerror
and try again.
The response consist of the URL-encoded base URL where you want to receive the response and the following parameters:
Parameter Type Description boarded
Boolean Indicates if the app is boarded or not. installationId
String The unique identifier of the Payments app instance on the specific device. When creating Terminal API requests on your back-end, use this as thePOIID
in theMessageHeader
of the request.error
String Additional information in case an error occurred and boarded
is false.Example responseExpand viewCopy link to code blockCopy code{YOUR_SCHEME}%3A%2F%2Fyour_company.example.com%2Fonboardingresult?boarded=true&installationId=APP_INSTALLATION_ID
- If
2. Make a transaction
After the Payments app is boarded, you are ready to make transactions. To do this, you need to create a Terminal API request in Base64URL format and call the request as an App Link from your POS app. The boarded Payments app then communicates with Adyen, receives the response, and returns the response in Base64 format as part of an App Link to your POS app.
Existing solutions using deep links should migrate to Android App Links.
To start a transaction from your POS app:
-
Create a Terminal API request with the
POIID
value set to theinstallationId
value of the boarded Payments app.- For a payment, partial payment, pre-authorization, or unreferenced refund, create a PaymentRequest.
- For a referenced refund, create a ReversalRequest.
-
Encrypt the Terminal API request as described for a Terminal API integration with local communications.
- Set up a shared key in your Customer Area or using Management API.
-
Either write your own code to encrypt and decrypt Terminal API messages, or use a library.
The following example shows how you could encrypt Terminal API requests using the Java library in a Kotlin project. We also have a .NET library and a Node library.
Encrypt using Java libraryExpand viewCopy link to code blockCopy codeprivate fun encryptNexoRequest( serviceId: String = UUID.randomUUID().toString(), saleId: String = "AndroidSampleApp", currency: String, requestedAmount: BigDecimal, installationId: String, ): String { // Create and set up the message header for the Terminal API request val messageHeader = MessageHeader().apply { protocolVersion = "3.0" messageClass = MessageClassType.SERVICE messageCategory = MessageCategoryType.PAYMENT messageType = MessageType.REQUEST serviceID = serviceId saleID = saleId poiid = installationId } // Set up the security key with the values from the Customer Area or Management API val securityKey = SecurityKey().apply { adyenCryptoVersion = 1 keyIdentifier = BuildConfig.KEY_IDENTIFIER passphrase = BuildConfig.PASS_PHRASE keyVersion = BuildConfig.VERSION } // This function creates a Terminal API request and returns the serialized value val nexoRequestJson = createNexoRequest(messageHeader = messageHeader, currency = currency, requestedAmount = requestedAmount) // Encrypt the Terminal API request using the NexoCrypto class // The first parameter is the serialized Terminal API request, and the second parameter is the message header val encrypted = NexoCrypto(securityKey).encrypt( nexoRequestJson, messageHeader, ) // Add the encryption response to the TerminalAPISecureRequest val terminalSecureRequest = TerminalAPISecureRequest() terminalSecureRequest.saleToPOIRequest = encrypted // You can use your own serialization library to convert the encrypted request to JSON // Ensure your serialization library properly handles HMAC and nonce properties: // These properties are byte arrays, and they need to be serialized into Base64-encoded strings. // If you use the Adyen Java library, it provides a specific Gson configuration for this purpose val gson = TerminalAPIGsonBuilder.create() // Adjust if using a different serialization library return gson.toJson(terminalSecureRequest) } // This function creates an example Terminal API request. Include additional fields as needed. // You can use your own model and serialization library. This is just an example. private fun createNexoRequest( messageHeader: MessageHeader, currency: String, requestedAmount: BigDecimal, transactionID: String = "SampleApp-AndroidTx", ): String { val saleTransactionID = TransactionIdentification().apply { timeStamp = getCurrentXMLGregorianCalendar() this.transactionID = transactionID } val saleData = SaleData().apply { this.saleTransactionID = saleTransactionID } val amountsReq = AmountsReq().apply { this.currency = currency this.requestedAmount = requestedAmount } val paymentTransaction = PaymentTransaction().apply { this.amountsReq = amountsReq } val paymentRequest = PaymentRequest().apply { this.saleData = saleData this.paymentTransaction = paymentTransaction } val saleToPOIRequest = SaleToPOIRequest().apply { this.messageHeader = messageHeader this.paymentRequest = paymentRequest } val terminalAPIRequest = TerminalAPIRequest().apply { this.saleToPOIRequest = saleToPOIRequest } val gson = TerminalAPIGsonBuilder.create() return gson.toJson(terminalAPIRequest) } private fun getCurrentXMLGregorianCalendar(): XMLGregorianCalendar { // Create a GregorianCalendar instance with UTC timezone val calendar = GregorianCalendar().apply { time = Date() } calendar.timeZone = java.util.TimeZone.getTimeZone("UTC") // Convert GregorianCalendar to XMLGregorianCalendar return DatatypeFactory.newInstance().newXMLGregorianCalendar(calendar) }
-
Encode the Terminal API request to Base64URL.
-
From your POS app, call the link.
The App Link format is as follows:
`https://www.adyen.com/test/nexo?request=NexoRequest.Base64Url{Encryption{NexoRequest}}&returnUrl=URLEncoder(your_scheme)
In this format:
https://www.adyen.com/test/nexo
is the test base URL and path (/nexo
) to use for transactions.request
is the Base64URL-encoded and encrypted Terminal API payment request.returnUrl
is the URL-encoded URL where you want to receive the Terminal API response.
The following example shows how you could build the App Link in your Kotlin project.
Build an App Link to initiate a paymentExpand viewCopy link to code blockCopy codeprivate fun buildPaymentRequestApplink( returnUrl: String, encryptedTapiRequest: String, ): Uri { val returnUrlEncoded = URLEncoder.encode(returnUrl, Charsets.UTF_8.name()) val encoded = Base64.getUrlEncoder().encodeToString(encryptedTapiRequest.toByteArray()) return Uri.parse("${BuildConfig.APP_LINK_URL}/nexo").buildUpon() .appendQueryParameter("request", encoded) .appendQueryParameter("returnUrl", returnUrlEncoded).build() } App Link exampleExpand viewCopy link to code blockCopy codehttps://www.adyen.com/test/nexo?request=eyJTYWxlVG9QT0lSZXF1ZXN0Ijp7Ik1lc3NhZ2VIZWFkZXIiOnsiTWVzc2FnZUNhdGVnb3J5IjoiUGF5bWVudCIsIk1lc3NhZ2VDbGFzcyI6IlNlcnZpY2UiLCJNZXNzYWdlVHlwZSI6IlJlcXVlc3QiLCJQT0lJRCI6IkY2OUJCOTc2LTYwNjUtNDQ1MS05QUU3LTVBOEE5OTZGNkEyOSIsIlByb3RvY29sVmVyc2lvbiI6IjMuMCIsIlNhbGVJRCI6IkFuZHJvaWRTYW1wbGVBcHAiLCJTZXJ2aWNlSUQiOiI4NzE2MDg0NC02In0sIk5leG9CbG9iIjoiY3UrVGJwOHZWekIvZy9GWkZFMGJTWlpzUVh5Y1lWL3ZrM2ltZk9KbjhyTm1sQ1dEMkRnbS9qK2FQendIK3hXVDZhNU1JYjVSSlp5UkwrQmQ5c01SSStYYUFQYzBBTS9keWNXN2lTN0FJcGprUnZXNnV2OVlnUmdFbGl1NE1Nd1pkZndlNk11YzE1RGhqYko1SjVVc3VCSFFveHdDSjF5MTBSbTdlZUw3RXd1RlppZnU2OVBIWjB2U3ZxTytRL2doK0Eya1dVSThXSG5tTXAzWGlLL1JWdFp5WmJTaStSNkdFSVowVVVhYU85eW5TalpYZTBPTSs3YTdySXlqRmxQQkUvQS90SlRoVEtjZSs4YjZkc3JITFVLeVhmUFJjc1pGc1FUVk9ra2kvSkZnUjJqc2krZzZjUHJ4Y3oxVWpCRENGSTdiYk9pVjhKQ1pkdFlINnAxMUdpRXRJV1AzU2Qvb0ZDUkduK1lKelNRTGR2UFo0R2tHV3liZEoyb0ZaTEhrTmVXa0o1N29YN3RkcVNsajNrMjJCMzdzZ0p4STgzU3hielljRmQya2pQckFHOUpHOFNHL041M1JKTTNSY0lMK2Q1MndVM2MvRDl2TkNqYW01V2dOZldYT3cyeDFoNlR0L1lQNXdkdHFsTFpYNG55b3BKNndZMS9FZTBIUWIzVFJ0WEswWHBEbmphdGJkKzhWVm4xdGQxbmhDTDgzTDYxWXJRWUEyMG9XN25rOVI5dVY0ZDdnc0U2ZENGUzJMSmI4UytiYmVnd2szaGhJM0dRMEFJWHJpOG5ZaHhWNFF1M0o1dFJ3Unl4VDZYWW5oRnRDU3ZQTzNyY0VFcGpqeTZDTitBcHMrcVJrWWY0S1BnWkIyWU9mcnQrUlM1K080Tnp3OFkyOStaMCtHOVVJeHNLMVVIY1psWVFDTkFvOHNqRzdtalNDTm90Q2JTYk9BcVJIZmU5eXI1azRtdXZHenRzcUM4Y3R2aGRLeXpUQk5jTTJzR3NodzVJZis5TUJYRkR0WFJ1OFVFVEN3V2xDNnpNWXZreUphWHB6V2NOOVRJQ29HY2J4RkVMTWRBNjlVSjV2UktsNW9mTzd4UEhNSU9sSzBIYlpheElmeklJeDg1dGxPV2wzbUZnUCtJbzlDangrTXA4TklpaDJEeTkvVlE5L1NPeTVIeHFYYjRWb2JCNHdwUW91YXh0YzNuTEVLYktQaVZxd1R3PT0iLCJTZWN1cml0eVRyYWlsZXIiOnsiQWR5ZW5DcnlwdG9WZXJzaW9uIjoxLCJIbWFjIjoiT2N1aGMvVHJtaDlCL1FWQ2JmbFVsd3E0aTN3T2lKbkVTaC95RmovZDM1Zz0iLCJLZXlJZGVudGlmaWVyIjoidGhpcyIsIktleVZlcnNpb24iOjEyMywiTm9uY2UiOiJFK0VuRVFRUkFjc21YWlpMTng1c0lBPT0ifX19&returnUrl=your-scheme-test%3A%2F%2Fyour_company.example.com
A Tap to Pay screen appears on the mobile device.
If the shopper does not present their payment method within 30 seconds, the payment request times out. If that happens, you need to make another payment request.
-
Use the response to show the payment result in your POS app. The response consist of the
returnUrl
you specified in the link, and the following parameters:Parameter Type Description response
String Base64URL-encoded and encrypted Terminal API payment response. error
String Additional information in case an error occurs. Response exampleExpand viewCopy link to code blockCopy code{YOUR_SCHEME}://your_company.example.com?response=PAYMENT_RESPONSE
-
Decrypt the Terminal API response using the security key.
The following example shows how you could do that using the Adyen Java library in a Kotlin project.
Decrypt using Java libraryExpand viewCopy link to code blockCopy codeprivate fun decryptNexoResponse(response: String): String { // Decode the Base64 URL-encoded response string to retrieve the original encrypted Terminal API response val decoded = String(Base64.getUrlDecoder().decode(response)) val securityKey = SecurityKey() securityKey.adyenCryptoVersion = 1 securityKey.keyIdentifier = BuildConfig.KEY_IDENTIFIER securityKey.passphrase = BuildConfig.PASS_PHRASE securityKey.keyVersion = BuildConfig.VERSION val gson = TerminalAPIGsonBuilder.create() val data = gson.fromJson(decoded, TerminalAPISecuredResponse::class.java) return NexoCrypto(securityKey).decrypt(data.saleToPOIResponse) }
3. Revoke app instance
If your Android mobile device is defective, stolen, or you do not need it anymore for payments processing, you can stop the Payments app from processing transactions with Adyen. To do this, you make an API call to Adyen to revoke the specified installationId
.
To revoke a boarding token:
-
Optionally, make either a GET /merchants/merchantId/paymentsApps or /merchants/merchantId/stores/storeId/paymentsApps request to retrieve the list of Payments app instances for a merchant account or a store. You can then use the
installationId
to revoke boarding tokens. -
Make a POST /merchants/merchantId/paymentsApps/installationId/revoke request specifying:
- As path parameters:
Parameters Required Description installationId
The unique identifier of the Payments app instance on the specific device. merchantId
The unique identifier of the merchant account. Revoke Payments app instance authenticationExpand viewCopy link to code blockCopy codecurl https://management-test.adyen.com/v1/merchants/{merchantId}/paymentsApps/{installationId}/revoke \ -H 'x-API-key: ADYEN_API_KEY' \ -H 'content-type: application/json' \ -X POST \ -
If successful, the response returns 200 OK.
Test your solution
To make test transactions:
-
Make sure you are using the test version of the Payments app.
-
Initiate a test transaction using the following Adyen point-of-sale test cards to complete the payment:
- White-green test card
- Blue-green test card version 2.4 or later
The instructions are the same for both cards; see either of the pages mentioned above.
Go live
When you have finished testing your integration and are ready to go live:
- If new to Adyen, get a live account. You need to have access to your organization's live Customer Area to generate an API credentials for the live environment.
- In your live Customer Area generate a new API key with the Adyen Payments app role for use with the Adyen Payments app.
- Download the Adyen Payments app from the Google Play store.
- Contact us to enable Tap to Pay on Android, and optionally also enable unreferenced refunds.
-
Update your code to use the live URLs for App Links (or deep links):
https://www.adyen.com/boarded
https://www.adyen.com/board
https://www.adyen.com/nexo
- Update your code to use the live Management API endpoints:
https://management-live.adyen.com
Verify app instance
Adyen continuously checks if the app instance on your Android mobile device is authentic. This is to protect your integration against man-in-the-middle attacks, eavesdropping, and tampering. You can also check this yourself manually using a different device and your email address.
To verify the app instance, in the Payments app:
- Go to Settings and tap Verify app.
- Enter your email address and tap Send email.
- Check your email on another device. You should have received an email with a 5-character code.
- In the Payments app, if you received the email, tap Continue. If you did not receive the email, check the email address you entered, and tap Resend email and go back to step 2.
- Under Verify app, check that the 5-character code on the screen matches the code you received in the email.
If the app instance verification is successful, you can continue making payments with the Payments app. If the verification fails, Adyen automatically revokes the app instance. In this case, you need to board the app again on that Android mobile device.