TerminalSDK Library Documentation

The TerminalSDK library provides a set of classes and methods for handling user authentication, terminal connections, and card transactions.

Part 1: Preparing the Required Assets

To use the library, you must provide specific assets and configurations.

Information to Provide to the Nearpay Team

When setting up your integration with Nearpay, ensure that you provide the following details:

  1. Android Package Name

    • This is the unique identifier for your Android application.
    • Example: com.yourcompany.yourapp
  2. PEM Certificate

    • A PEM (Privacy-Enhanced Mail) certificate is required for secure communication.
    • You can generate a PEM certificate by following the steps outlined here (Replace with the actual link to instructions). here.

Ensure these details are accurate before submitting them to Nearpay for a smooth integration process.

Google Play Integrity and Huawei Safety Detect

This library uses Google Play Integrity (Mandatory) and Huawei Safety Detect (Optional) to verify the integrity of the device.

Google Play Integrity (Mandatory)

To make the library work, you need to have a Google Cloud project with Play Integrity enabled and pass the Google Cloud project number in the builder. Here are the steps to do so:

Create a Google Cloud Project:

  1. Get the Project Number:
    • Go to the Google Cloud Console.
    • Click on the project you created.
    • Go to the project settings.
    • Copy the project number.
  2. Enable Play Integrity API:
    • Go to the Google Play Console.
    • Navigate to Release > App Integrity.
    • Under the Play Integrity API, select Link a Cloud project.
    • Choose the Cloud project you want to link to your app, which will enable Play Integrity API responses.
    • This may change in the future, so please refer to the official documentation here: Google Play Integrity documentation
  3. Pass the Project Number in the Builder:
    • Use the googleCloudProjectNumber method in the builder to pass the project number.

Huawei Safety Detect (Optional)

To use Huawei Safety Detect, you need to have a Huawei Developer account and pass the Safety Detect API key in the builder. Here are the steps to do so:

  1. Create a Huawei Developer Account:
  2. Create an App:
    • Go to AppGallery Connect.
    • Create a new app or use an existing one.
  3. Enable Safety Detect:
    • Go to the AppGallery Connect console.
    • Navigate to Develop > Security Detection.
    • Enable Safety Detect.
  4. Get the Safety Detect API Key:
    • Go to the AppGallery Connect console.
    • Navigate to Develop > Security Detection.
    • Click on the Safety Detect tab.
    • Copy the API key.
  5. Pass the API Key in the Builder:
    • Use the safetyDetectApiKey method in the builder to pass the Safety Detect API key.

Configuring the Secure Maven Repository / Dependencies

For the ReaderCore library, you can include the following configuration in your root-level settings.gradle(.kts) file:

if your project is using Groovy settings.gradle, you can use the following configuration:

Very important:

To get the private token , you need to contact Nearpay to get the token for the private repository.

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven(url = "https://developer.huawei.com/repo/")
        maven {
            url = uri("https://git.nearpay.io/api/v4/projects/165/packages/maven")
            credentials(HttpHeaderCredentials::class) {
                name = "Private-Token"
                value = nearpayPosGitlabReadToken // The token provided by Nearpay goes here.
            }
            authentication {
                create<HttpHeaderAuthentication>("header")
            }
        }
    }
}

You should include the following dependencies in your Module level build.gradle file:

implementation("io.nearpay:terminalsdk-release:0.2.2")
implementation("com.google.android.gms:play-services-location:20.0.0")
implementation("com.huawei.hms:location:6.4.0.300")

In addition, you have to change the minSdk version to 28 in your build.gradle file:

    android {
        defaultConfig {
            applicationId = "com.example.example"
            minSdk = 28 
        }
    }

Contact Nearpay to register your applicationId and get the necessary credentials.

AndroidManifest.xml Configuration

In your AndroidManifest.xml file, add the following line:


<application
    android:allowBackup="true"
    tools:replace="android:allowBackup" // Add this line to avoid manifest merger issues


Make sure the following tools namespace is present in your <manifest> tag:// xmlns:tools="http://schemas.android.com/tools"

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"> // this tools namespace

...

Part 2: Getting Started with TerminalSDK

To start using TerminalSDK, initialize it with the necessary configurations.

// initializing the terminalSDK may throw an exception, so wrap it in a try-catch block
try {
    val nearpay = TerminalSDK.Builder()
        .activity(this)  // Sets the activity context
        .environment(SdkEnvironment.SANDBOX)  // Choose SANDBOX or PRODUCTION or INTERNAL
        .googleCloudProjectNumber(12345678)  // Set Google Cloud project number
        .huaweiSafetyDetectApiKey("your_api_key")  // Set Huawei API key
        .country(Country.SA)  // Set country SA, TR, USA, KEN ,
        .networkConfiguration(NetworkConfiguration.DEFAULT) // DEFAULT | SIM_PREFERRED | SIM_ONLY
        .uiDockPosition(UiDockPosition.BOTTOM_CENTER) // Optional: set the location of the Tap to Pay modal 
        .supportSecondDisplay(SupportSecondDisplay.ENABLE) // Optional: enable support for second display
        .secondDisplayDockPosition(UiDockPosition.TOP_RIGHT) // Optional: set the location of the Tap to Pay modal on the second display
        .build()
    } catch (e: Throwable) {
        Timber.e("Error initializing TerminalSDK: $e")
    }

Permissions and Requirements

Check Required Permissions

The SDK provides methods to check and verify required permissions and device capabilities.

Check all required permissions and return a list of PermissionStatus objects.

// Check all required permissions
val permissionStatuses = nearpay.checkRequiredPermissions()
permissionStatuses.forEach { status ->
    if (!status.isGranted) {
        // Handle missing permission
        Log.d("Permissions", "Missing permission: ${status.permission}")
    }
}

The required permissions include:

  • Manifest.permission.ACCESS_FINE_LOCATION
  • Manifest.permission.ACCESS_NETWORK_STATE
  • Manifest.permission.INTERNET
  • Manifest.permission.READ_PHONE_STATE
  • Manifest.permission.NFC

The following code snippets show how to check permissions, then ask for missing permissions if needed, and also check if NFC and WiFi are enabled.


        var PERMISSIONS_REQUEST_CODE = 0
    // Check permissions
        val missingPermissions =
            nearpay.checkRequiredPermissions().filter { !it.isGranted }.map { it.permission }

        if (missingPermissions.isNotEmpty()) {
    // Request missing permissions
            ActivityCompat.requestPermissions(
                this, missingPermissions.toTypedArray(), PERMISSIONS_REQUEST_CODE //0 for example
            )
        }

    // Check NFC
        if (!nearpay.isNfcEnabled(this)) {
            Log.d("MainActivity", "NFC is disabled");
        }

    // Check WiFi
        if (!nearpay.isWifiEnabled(this)) {
            Log.d("MainActivity", "WiFi is disabled");
        }

Part 3: Authentication

Send OTP

The SDK supports both mobile and email OTP authentication.

Mobile Authentication

val mobileLogin = MobileLogin("+966500000000")
nearpay.sendOTP(mobileLogin, object : SendOTPMobileListener {
    override fun onSendOTPMobileSuccess(otpResponse: OtpResponse) {
        Log.d("Login", " onSendOTPMobileSuccess ($otpResponse)")
    }
    override fun onSendOTPMobileFailure(otpMobileFailure: OTPMobileFailure) {
        Log.d("Login", " onSendOTPMobileFailure ($otpMobileFailure)")
    }
})

Email Authentication

val emailLogin = EmailLogin("[email protected]")
nearpay.sendOTP(emailLogin, object : SendOTPEmailListener {
    override fun onSendOTPEmailSuccess(otpResponse: OtpResponse) {
        Log.d("Login", " onSendOTPEmailSuccess ($otpResponse)")
    }
    override fun onSendOTPEmailFailure(otpEmailFailure: OTPEmailFailure) {
        Log.d("Login", " onSendOTPEmailFailure ($otpEmailFailure)")
    }
})

Verify OTP

After sending the OTP, verify it to authenticate the user.

Mobile Verification

val loginData = LoginData(
    mobile = "+966500000000",
    code = "123456"
)
nearpay.verify(loginData, object : VerifyMobileListener {
    override fun onVerifyMobileSuccess(user: User) {
        Log.d("Login", " onVerifyMobileSuccess ${user.name}")
        userUuid = user.userUUID()  // Retrieve the UUID of the currently logged-in user. This value can be reused later to fetch user details using getUserByUUID().
    }
    override fun onVerifyMobileFailure(verifyMobileFailure: VerifyMobileFailure) {
        Log.d("Login", " onVerifyMobileFailure ($failure)")
    }
})

Email Verification

val loginData = LoginData(
    email = "[email protected]",
    code = "123456"
)
nearpay.verify(loginData, object : VerifyEmailListener {
    override fun onVerifyEmailSuccess(user: User) {
        Log.d("Login", " onVerifyEmailSuccess ($user.name)")
        userUuid = user.userUUID()  // Retrieve the UUID of the currently logged-in user. This value can be reused later to fetch user details using getUserByUUID().
    }
    override fun onVerifyEmailFailure(verifyEmailFailure: VerifyEmailFailure) {
        Log.d("Login", " onVerifyEmailFailure ($verifyMobileFailure)")
    }
})

JWT Verification

For creating a JWT token, please visit this link : JWT Token Generator

val loginData = JWTLoginData(
    jwt = "jwt_token"
)
nearpay.jwtLogin(loginData, object : JWTLoginListener {
            override fun onJWTLoginSuccess(terminal: Terminal) {
                Log.d("Login", "JWT Login success + ${terminal.terminalUUID}")
                terminalUUID = terminal.terminalUUID  // Retrieve the UUID of the currently terminal session. This value can be reused later to fetch terminal session details using getTerminal().
            }

            override fun onJWTLoginFailure(jwtLoginFailure: JWTLoginFailure) {
                Log.d("Login", "JWT Login failure: $jwtLoginFailure")
            }
        })

Get User

You can also get a User instance after calling the getUserByUUID method if the user has already been authenticated before using the SDK and you have their UUID from the previously returned User instance.

Saving the user UUID is the responsibility of the developer, not the SDK.

try{
    val user = nearpay.getUserByUUID(userUuid)
} catch (e: Exception) {
    Log.d("UserSDK", "User connection failed: ${e}")
}
// The returned user object also becomes the active user

Logout User

To log out a user and delete its instance from memory, call the logout method.

Takes a User UUID as a parameter and logs out the user.

try {
    nearpay.logout(uuid)
} catch (e: Exception) {
    Log.d("UserSDK", "User logout failed: ${e}")
}

Part 4: User Operations

The User class is initialized internally by the SDK and provides methods for managing terminals.

List Terminals

Retrieves a paginated list of terminals associated with the user.

Usage

lateinit var fetchedTerminals : List<TerminalConnection>
userInstance.listTerminals(
    page = 1,
    pageSize = 10,
    filter = null, // You can pass the terminal ID to get a specific terminal
    object : GetTerminalsListener {
        override fun onGetTerminalsSuccess(terminalsConnection: List<TerminalConnection>, pagination: Pagination) {

            fetchedTerminals = terminalsConnection // list of terminals that assigned to the connected user

        }

        override fun onGetTerminalsFailure(getTerminalsFailure: GetTerminalsFailure) {
            // Handle failure
            Log.d("Terminals", "Terminals list failure: $getTerminalsFailure")
        }
    },
    )

Part 5: Terminal Connection Operations

Connect Terminal

Establishes a connection with a terminal.

Usage


var firstTerminal = fetchedTerminals.terminals[0] ; // Retrieve the specific terminal connection from the list of fetched terminals. This terminal will be used to perform operations.

firstTerminal.connect(
    activity = this,
    listener = object : ConnectTerminalListener {
        override fun onConnectTerminalSuccess(terminal: Terminal) {
            // Terminal connected successfully
            // Store terminal instance for future operations
            terminalInstance = terminal
            terminalUUID = terminalInstance.terminalUUID  // Retrieve the UUID of the currently terminal session. This value can be reused later to fetch terminal session details using getTerminal().

        }

        override fun onConnectTerminalFailure(connectTerminalFailure: ConnectTerminalFailure) {
            // Handle failure
            Log.d("Terminal", "Terminal connection failed: $connectTerminalFailure")
        }
    }
)

Part 6: Terminal Operations

Before getting into Terminal class functions, you can also get a Terminal instance after calling the getTerminal method and passing the terminal's ID instead of using a TerminalConnection instance.

Get Terminal

Retrieves a Terminal instance for a specific terminal ID.

try {

private var terminalUUID: String = "7d74bc33-d14a-4111-bba3-b33707d57ae4"

val terminalInstance = nearpay.getTerminal(
activity = this,
uuid = terminalUUID
)
} catch (e: Exception) {
Log.d("TerminalSDK", "Terminal connection failed: ${e}")
}

Purchase

Initiates a purchase transaction by reading the card and sending the transaction.

    var amount = 100
    var intentUuid = UUID.randomUUID().toString() // the intent UUID should be unique for each transaction and managed by the developer to communicate with the SDK
    var customerReferenceNumber = "" //[optional] any number you want to add as a refrence

    terminal.purchase(
        amount = amount,
        scheme = null, // eg.PaymentScheme.VISA, specifying this as null will allow all schemes to be accepted
        intentUUID = transactionUUID,
        customerReferenceNumber = customerReferenceNumber,
        readCardListener = object : ReadCardListener {
            override fun onReaderDismissed() {
                // Reader dismissed by user
                Log.d("ReadCard", "Reader dismissed by user")
            }   
            override fun onReadCardSuccess() {
                // Card read successfully
                Log.d("ReadCard", "Card read successfully")
            }

            // Called when the card reading process fails - issues with the specific card or its interaction
            // Examples: card removed too quickly, unreadable card, wrong card orientation
            override fun onReadCardFailure(readCardFailure: ReadCardFailure) {
                // Handle card read failure
                Log.d("ReadCard", "Card read failure: $readCardFailure")
            }

            override fun onReaderDisplayed() {
                Log.d("ReaderCard", "Reader Displayed")
            }

            override fun onReaderClosed() {
                Log.d("ReaderCard", "Reading Closed")
            }

            override fun onReaderWaiting() {
                // Reader waiting for card
                Log.d("ReadCard", "Reader waiting for card")
            }

            override fun onReaderReading() {
                // Reading card in progress
                Log.d("ReadCard", "Reading card in progress")
            }

            override fun onReaderRetry() {
                // Reader retry needed
                Log.d("ReadCard", "Reader retry needed")
            }

            override fun onPinEntering() {
                // PIN entry in progress
                Log.d("ReadCard", "PIN entry in progress")
            }

            override fun onReaderFinished() {
                // Card read completed
                Log.d("ReadCard", "Card read completed")
            }
            override fun onReadingStarted() {
                // Card read started
                Log.d("ReadCard", "Card read started")
            }

            // Called when the card reader device itself encounters an error
            // Examples: hardware malfunction, connection issues, device not ready
            override fun onReaderError(error: String?) {
                // Handle reader error
                Log.d("ReadCard", "Reader error: $error")
            }
        },
        sendTransactionListener = object : SendTransactionListener {
            override fun onSendTransactionCompleted(purchaseResponse: PurchaseResponse) {
                // Handle completed transaction

                //PurchaseResponse will return all transaction with same intent id "transactionUUID"
                //purchaseResponse.status will return the status of last transaction of the same intent id


                Log.d("Transaction", "Transaction completed: $purchaseResponse")
                // To get the approved receipt based on the country you can got it like :
                // purchaseResponse.getLastReceipt().getMadaReceipt() for Saudi Arabia
                // purchaseResponse.getLastReceipt().getEPXReceipt() for USA
                // purchaseResponse.getLastReceipt().getBKMReceipt() for Turkey
            }

            override fun onSendTransactionFailure(failure: SendTransactionFailure) {
                // Handle transaction failure
                Log.d("Transaction", "Transaction failure: $failure")
            }
        }
    )

Find PurchaseResponse model here.
Find SendTransactionFailure model here.

Purchase with QR code

Initiates a purchase transaction by reading the QR code and sending the transaction.

    var amount = "100"
    var id = UUID.randomUUID().toString() // the intent UUID should be unique for each transaction and managed by the developer to communicate with the SDK
    var customerReferenceNumber = "" //[optional] any number you want to add as a refrence
    var enableUiDismiss = true // set to true to allow user to dismiss the QR code UI

    terminal.generateQrCodeWithUi(
            id = id,
            amount = amount,
            customerReferenceNumber = customerReferenceNumber,
            enableUiDismiss = enableUiDismiss,
            qrPaymentUiListener = object : QrPaymentUiListener {

                override fun onQrUiPresented(qrCode: GeneratedQRCode) {
                  
    
                }

                override fun onQrPaymentStatusUpdated(status: QrStatusResponse) {
                 
    
                }

                override fun onQrPaymentStatusExpired(status: QrStatusResponse) {
                   
            
                }

                override fun onQrPaymentStatusCompleted(status: QrStatusResponse) {
                   
            
                }

                override fun onQrPaymentApproved(intent: IntentDetails) {
                   
                }

                override fun onQrPaymentDeclined(intent: IntentDetails?) {
            
              
                }

                override fun onQrPaymentFailure(message: String) {

                }

                override fun onQrPaymentStatusFailed(status: QrStatusResponse) {
                   
       
                }

            }

        )

You can find QrStatusResponse model here.
You can find GeneratedQRCode model here.
You can find IntentDetails model here.

Purchase Void with QR code

Initiates a purchase void transaction by reading the QR code and sending the transaction.

val voidIntentId = UUID.randomUUID().toString()
terminal.qrVoid(
intentId = voidIntentId,
originalIntentId = originalIntentId,
qrVoidListener = object : QrVoidListener {
    override fun onQrVoidSuccess(response: QrStatusResponse) {
        Timber.d("QR void completed successfully.")
    }

    override fun onQrVoidFailure(failure: QrVoidFailure) {
        val message = when (failure) {
            is QrVoidFailure.GeneralFailure ->
                failure.message ?: "Unknown error"
        }   
        Timber.e("QR void failed: $message")
    }

    override fun onQrVoidQrCodeGenerated(qrCode: GeneratedQRCode) {
        Timber.d("QR code generated successfully." +
            "Void Intent ID: ${qrCode.intentId}" +
            "Amount: ${qrCode.amount}" +
            "Transaction ID: ${qrCode.transactionId}" +
            "Payload: ${qrCode.qrCode}"
        )
    }
}

// write in english You can find GeneratedQRCode model here.
You can find QrStatusResponse model here.

Refund with QR code

Initiates a refund transaction by reading the QR code and sending the transaction.

    val refundIntentId = UUID.randomUUID().toString()
    terminal.qrRefund(
        intentUUID = originalIntentId,
        refundUUID = refundIntentId,
        qrRefundListener = object : QrRefundListener {
            override fun onQrRefundSuccess(response: QrStatusResponse) {
                Timber.d("QR refund completed: $response")
            }

            override fun onQrRefundFailure(failure: QrRefundFailure) {
                val message = when (failure) {
                    is QrRefundFailure.GeneralFailure ->
                        failure.message ?: "Unknown error"
                }
                Timber.e("QR refund failed: $message")
            }
        }
    )

You can find QrStatusResponse model here.
You can find the QrRefundFailure model here.

Purchase with Installments

Initiates a purchase transaction with installments by reading the card and sending the transaction.

    var amount = 100
    var numberOfInstallments = 3
    var intentUuid = UUID.randomUUID().toString() // the intent UUID should be unique for each transaction and managed by the developer to communicate with the SDK
    var customerReferenceNumber = "" //[optional] any number you want to add as a refrence

    terminal.installments(
        amount = amount,
        numberOfInstallments = numberOfInstallments,
        scheme = null, // eg.PaymentScheme.VISA, specifying this as null will allow all schemes to be accepted
        intentUUID = transactionUUID,
        customerReferenceNumber = customerReferenceNumber,
        readCardListener = object : ReadCardListener {
            override fun onReaderDismissed() {
                // Reader dismissed by user
                Log.d("ReadCard", "Reader dismissed by user")
            }   
            override fun onReadCardSuccess() {
                // Card read successfully
                Log.d("ReadCard", "Card read successfully")
            }

            // Called when the card reading process fails - issues with the specific card or its interaction
            // Examples: card removed too quickly, unreadable card, wrong card orientation
            override fun onReadCardFailure(readCardFailure: ReadCardFailure) {
                // Handle card read failure
                Log.d("ReadCard", "Card read failure: $readCardFailure")
            }

            override fun onReaderDisplayed() {
                Log.d("ReaderCard", "Reader Displayed")
            }

            override fun onReaderClosed() {
                Log.d("ReaderCard", "Reading Closed")
            }

            override fun onReaderWaiting() {
                // Reader waiting for card
                Log.d("ReadCard", "Reader waiting for card")
            }

            override fun onReaderReading() {
                // Reading card in progress
                Log.d("ReadCard", "Reading card in progress")
            }

            override fun onReaderRetry() {
                // Reader retry needed
                Log.d("ReadCard", "Reader retry needed")
            }

            override fun onPinEntering() {
                // PIN entry in progress
                Log.d("ReadCard", "PIN entry in progress")
            }

            override fun onReaderFinished() {
                // Card read completed
                Log.d("ReadCard", "Card read completed")
            }
            override fun onReadingStarted() {
                // Card read started
                Log.d("ReadCard", "Card read started")
            }

            // Called when the card reader device itself encounters an error
            // Examples: hardware malfunction, connection issues, device not ready
            override fun onReaderError(error: String?) {
                // Handle reader error
                Log.d("ReadCard", "Reader error: $error")
            }
        },
        installmentsListener = object : InstallmentsListener {
            override fun onInstallmentsCompleted(purchaseResponse: PurchaseResponse) {
                // Handle completed transaction

                //PurchaseResponse will return all transaction with same intent id "transactionUUID"
                //purchaseResponse.status will return the status of last transaction of the same intent id


                Log.d("Transaction", "Transaction completed: $purchaseResponse")
                // To get the approved receipt based on the country you can got it like :
                // purchaseResponse.getLastReceipt().getMadaReceipt() for Saudi Arabia
                // purchaseResponse.getLastReceipt().getEPXReceipt() for USA
                // purchaseResponse.getLastReceipt().getBKMReceipt() for Turkey
            }

            override fun onInstallmentsFailure(failure: SendTransactionFailure) {
                // Handle transaction failure
                Log.d("Transaction", "Transaction failure: $failure")
            }
        }
    )

Find PurchaseResponse model here.
Find SendTransactionFailure model here.

Payment Flow with External Gateway and BIN Routing

In BIN-routed transactions, after reading the card, the BIN number is queried by the SDK and, according to predefined routing rules, the transaction is routed to an external payment gateway (External Gateway) instead of the Nearpay infrastructure.

This is a two-step process: first, retrieve the routing profile, then perform routing lookup and routing execute operations.

  1. Retrieve Routing Profile: The RoutingProfile contains information such as the registered terminals at the bank and their properties. This information is used to route the transaction.
    terminal.getRoutingProfile(
                object : GetRoutingProfileListener {
                    override fun onGetRoutingProfileSuccess(profile: RoutingProfile) {
                        // Routing profile retrieved successfully
                        // You can use the profile information to determine how to route the transaction
                        Log.d("RoutingProfile", "Routing profile retrieved: $profile")
                        terminalProfile = profile
                    }

                    override fun onGetRoutingProfileFailure(error: GetRoutingProfileFailure) {
                        val errorMessage = when (error) {
                            is GetRoutingProfileFailure.GeneralFailure -> error.message
                        }
                        Log.e("RoutingProfile", "Failed to retrieve routing profile: $errorMessage")
                    }
                }
            )

You can find the RoutingProfile model here.
You can find the GetRoutingProfileFailure model here.

  1. Routing Lookup and Execute: After the card is read, call the terminal.routingLookup() method. This method is used to obtain appropriate routing options based on the card's BIN number. Then, you can perform the transaction using the selected routing option with the terminal.routingExecute() method.
    var amount = 100
    var intentUUID = UUID.randomUUID().toString() // Benzersiz intent UUID
    var customerReferenceNumber = "" // [opsiyonel] referans numarasΔ±

    terminal.routingLookup(
        amount = amount,
        intentUUID = intentUUID,
        readCardListener = object : ReadCardListener {
            // Kart okuma geri çağırımları (ânceki ârneklerde gâsterildiği gibi uygulayın)
        },
        routingLookupListener = object : RoutingLookupListener {
            override fun onRoutingLookupCompleted(
                response: RoutingResponse,
                session: RoutingSession
            ) {
                // SeΓ§ilen acquirer terminal ID'sini alΔ±n
                // select terminal based on issuer id matching (if available) to increase chances of successful routing execute, otherwise fallback to random selection
                val issuerId = response.binInformation?.issuerId
                val matchedTerminal =
                    terminalProfile.terminals.firstOrNull { terminal ->
                        terminal.terminalInfo.acquirerId.equals(
                            issuerId,
                            ignoreCase = true
                        )
                    }
                val selectedTid: String? =
                    matchedTerminal?.terminalInfo?.acquirerTerminalId?.trim()
                        ?: terminalProfile.terminals
                            .shuffled()
                            .firstOrNull()
                            ?.terminalInfo
                            ?.acquirerTerminalId
                            ?.trim()

                terminal.routingExecute(
                    session = session,
                    selectedAcquirerTerminalId = selectedTid,
                    customerReferenceNumber = customerReferenceNumber,
                    sendTransactionListener = object : SendTransactionListener {
                        override fun onSendTransactionCompleted(
                            purchaseResponse: PurchaseResponse
                        ) {
                            // İşlem tamamlandı
                            // PurchaseResponse, aynı intent id'ye sahip tüm işlemleri dândürür
                            // purchaseResponse.status, son işlemin durumunu dândürür

                            Log.d("Transaction", "Transaction completed: $purchaseResponse")
                            // Onaylanan slipi ΓΌlkeye gΓΆre almak iΓ§in:
                            // purchaseResponse.getLastReceipt().getMadaReceipt() (Suudi Arabistan)
                            // purchaseResponse.getLastReceipt().getEPXReceipt() (ABD)
                            // purchaseResponse.getLastReceipt().getBKMReceipt() (TΓΌrkiye)
                        }

                        override fun onSendTransactionFailure(
                            sendTransactionFailure: SendTransactionFailure
                        ) {
                            // İşlem başarısız oldu
                            Log.d("Transaction", "Transaction failure: $sendTransactionFailure")
                            val errorMessage = when (sendTransactionFailure) {
                                is SendTransactionFailure.TransactionFailure ->
                                    "Routing execute failed for $selectedTid: ${sendTransactionFailure.message}"
                                else -> "Routing execute failed for $selectedTid."
                            }
                            // Hata mesajΔ±nΔ± burada kullanabilirsiniz
                        }
                    }
                )
            }

            override fun onRoutingLookupFailure(routingLookupFailure: RoutingLookupFailure) {
                val errorMsg = when (routingLookupFailure) {
                    is RoutingLookupFailure.Failure -> routingLookupFailure.message
                }
                Log.d("RoutingLookup", "Routing lookup failure: $errorMsg")

            }
        }
    )

You can find the RoutingResponse model here.
You can find the RoutingSession model here.
You can find the PurchaseResponse model here.
You can find the SendTransactionFailure model here.
You can find the RoutingLookupFailure model here.

Payment Flow with External Gateway and BIN Routing with Installments

In BIN-routed transactions, after reading the card, the BIN number is queried by the SDK and, according to predefined routing rules, the installment transaction is routed to an external payment gateway (External Gateway) instead of the Nearpay infrastructure.

This is a two-step process: first, retrieve the routing profile, then perform routing lookup and routingInstallments operations.

  1. Retrieve Routing Profile: The RoutingProfile contains information such as the registered terminals at the bank and their properties. This information is used to route the transaction.
    terminal.getRoutingProfile(
                object : GetRoutingProfileListener {
                    override fun onGetRoutingProfileSuccess(profile: RoutingProfile) {
                        // Routing profile retrieved successfully
                        // You can use the profile information to determine how to route the transaction
                        Log.d("RoutingProfile", "Routing profile retrieved: $profile")
                        terminalProfile = profile
                    }

                    override fun onGetRoutingProfileFailure(error: GetRoutingProfileFailure) {
                        val errorMessage = when (error) {
                            is GetRoutingProfileFailure.GeneralFailure -> error.message
                        }
                        Log.e("RoutingProfile", "Failed to retrieve routing profile: $errorMessage")
                    }
                }
            )

RoutingProfili modelini buradan bulabilirsiniz.
GetRoutingProfileFailure modelini buradan bulabilirsiniz.

  1. Routing Lookup and Installments: After the card is read, call the terminal.routingLookup() method. This method is used to obtain appropriate routing options based on the card's BIN number. Then, you can perform the transaction using the selected routing option and present installment options to the customer with the terminal.routingInstallments() method.
    var amount = 100
    var intentUUID = UUID.randomUUID().toString() // Unique intent UUID
    var customerReferenceNumber = "" // [optional] reference number

    terminal.routingLookup(
        amount = amount,
        intentUUID = intentUUID,
        readCardListener = object : ReadCardListener {
            // Card reading callbacks (implement as shown in previous examples)
        },
        routingLookupListener = object : RoutingLookupListener {
            override fun onRoutingLookupCompleted(
                response: RoutingResponse,
                session: RoutingSession
            ) {
                // Get the selected acquirer terminal ID
                // select terminal based on issuer id matching (if available) to increase chances of successful routing execute, otherwise fallback to random selection
                val issuerId = response.binInformation?.issuerId
                val matchedTerminal =
                    terminalProfile.terminals.firstOrNull { terminal ->
                        terminal.terminalInfo.acquirerId.equals(
                            issuerId,
                            ignoreCase = true
                        )
                    }
                val selectedTid: String? =
                    matchedTerminal?.terminalInfo?.acquirerTerminalId?.trim()
                        ?: terminalProfile.terminals
                            .shuffled()
                            .firstOrNull()
                            ?.terminalInfo
                            ?.acquirerTerminalId
                            ?.trim()

                val sampleOptions = listOf(
                        InstallmentOption(installmentCount = 1, installmentPercentage = 0.0, paymentPlan = "1 x 100 = 100", totalCalculatedAmount = 100.0),
                        InstallmentOption(installmentCount = 2, installmentPercentage = 2.0, paymentPlan = "2 x 51 = 102", totalCalculatedAmount = 102.0),
                        InstallmentOption(installmentCount = 3, installmentPercentage = 3.0, paymentPlan = "3 x 34.33 = 103", totalCalculatedAmount = 103.0),
                        InstallmentOption(installmentCount = 6, installmentPercentage = 6.0, paymentPlan = "6 x 17.67 = 106", totalCalculatedAmount = 106.0),
                        InstallmentOption(installmentCount = 8, installmentPercentage = 2.0, paymentPlan = "2 x 51 = 102", totalCalculatedAmount = 102.0),
                        InstallmentOption(installmentCount = 9, installmentPercentage = 9.0, paymentPlan = "9 x 12.11 = 109", totalCalculatedAmount = 109.0)
                    )

                terminal.routingInstallments(
                    session = session,
                    installmentOptions = sampleOptions,
                    selectedAcquirerTerminalId = selectedTid,
                    customerReferenceNumber = customerReferenceNumber,
                    sendTransactionListener = object : SendTransactionListener {
                        override fun onSendTransactionCompleted(
                            purchaseResponse: PurchaseResponse
                        ) {
                            // Transaction completed
                            // PurchaseResponse returns all transactions with the same intent id
                            // purchaseResponse.status returns the status of the last transaction with the same intent id

                            Log.d("Transaction", "Transaction completed: $purchaseResponse")
                            // To get the approved receipt based on the country:
                            // purchaseResponse.getLastReceipt().getMadaReceipt() (Saudi Arabia)
                            // purchaseResponse.getLastReceipt().getEPXReceipt() (USA)
                            // purchaseResponse.getLastReceipt().getBKMReceipt() (Turkey)
                        }

                        override fun onSendTransactionFailure(
                            sendTransactionFailure: SendTransactionFailure
                        ) {
                            // Transaction failed
                            Log.d("Transaction", "Transaction failure: $sendTransactionFailure")
                            val errorMessage = when (sendTransactionFailure) {
                                is SendTransactionFailure.TransactionFailure ->
                                    "Routing execute failed for $selectedTid: ${sendTransactionFailure.message}"
                                else -> "Routing execute failed for $selectedTid."
                            }
                            // You can use the error message here
                        }
                    }
                )
            }

            override fun onRoutingLookupFailure(routingLookupFailure: RoutingLookupFailure) {
                val errorMsg = when (routingLookupFailure) {
                    is RoutingLookupFailure.Failure -> routingLookupFailure.message
                }
                Log.d("RoutingLookup", "Routing lookup failure: $errorMsg")

            }
        }
    )

You can find the RoutingResponse model here.
You can find the RoutingSession model here.
You can find the PurchaseResponse model here.
You can find the SendTransactionFailure model here.
You can find the RoutingLookupFailure model here.
You can find the InstallmentOption model here.

Purchase Void

Voids a purchase transaction by providing the transaction ID.

var intentUUID = "intent-uuid"
    terminal.purchaseVoid(
                amount = amount,
                scheme = scheme,
                customerReferenceNumber = customerReferenceNumber,
                intentUUID = intentUuid,
                readCardListener = object : ReadCardListener {
                    override fun onReaderClosed() {
                         Log.d("PurchaseVoidOperation", "Card read successfully")

                    }

                    override fun onReaderDisplayed() {
                        Log.d("PurchaseVoidOperation", "Reader Displayed")
                    }

                    override fun onReadCardSuccess() {
                        Log.d("PurchaseVoidOperation", "Card read successfully")
                    }

                    override fun onReadCardFailure(readCardFailure: ReadCardFailure) {
                        Log.d("PurchaseVoidOperation", "Card read failure: $readCardFailure")
                    }

                    override fun onReaderWaiting() {
                        Log.d("PurchaseVoidOperation", "Reader waiting for card")
                    }

                    override fun onReaderReading() {
                        Log.d("PurchaseVoidOperation", "Reading card in progress")
                    }

                    override fun onReaderRetry() {
                        Log.d("PurchaseVoidOperation", "Reader retry needed")
                    }

                    override fun onPinEntering() {
                        Log.d("PurchaseVoidOperation", "PIN entry in progress")
                    }

                    override fun onReaderFinished() {
                        Log.d("PurchaseVoidOperation", "Card read completed")
                    }

                    override fun onReaderError(error: String?) {
                        Log.d("PurchaseVoidOperation", "Reader error: $error")
                    }

                    override fun onReadingStarted() {
                        Log.d("PurchaseVoidOperation", "Card read started")
                    }
                },
                sendPurchaseVoidListener = object : SendPurchaseVoidListener {


                    override fun onSendPurchaseVoidCompleted(purchaseVoidResponse: IntentResponseTurkey) {
                      Log.d("PurchaseVoidOperation", "Purchase void completed: $purchaseVoidResponse"

                    }


                    override fun onSendPurchaseVoidFailure(sendPurchaseVoidFailure: SendTransactionFailure) {
                        Log.d("PurchaseVoidOperation", "Purchase void failure: $sendPurchaseVoidFailure")
                    }
                },
            )

You can find the IntentResponseTurkey model here.
You can find the SendTransactionFailure model here.

Refund

Initiates a refund transaction by reading the card and sending the transaction.

    var amount = 1000L
    var intentUuid = "1234567890" // the same transaction UUID used in the purchase transaction "Intent ID"
    var refundUUID = UUID.randomUUID().toString() // the refund UUID should be unique for each refund transaction and managed by the developer to communicate with the SDK
    var customerReferenceNumber = "" //[optional] any number you want to add as a refrence

terminal.refund(
    amount = amount,
    scheme = null, // eg.PaymentScheme.VISA, specifying this as null will allow all schemes to be accepted
    intentUUID = transactionUUID,
    refundUUID = refundUUID,
    customerReferenceNumber = customerReferenceNumber,
    readCardListener = object : ReadCardListener {
        // Card reading callbacks
        // Same implementation as purchase
    },
    refundTransactionListener = object : RefundTransactionListener {
        override fun onRefundTransactionCompleted(refundResponse: RefundResponse) {
            // Handle successful refund
            Log.d("Refund", "Refund success: $refundResponse.getLastReceipt()")
        }

        override fun onRefundTransactionFailure(refundTransactionFailure: RefundTransactionFailure) {
            // Handle refund failure
            Log.d("Refund", "Refund failure: $refundTransactionFailure")
        }
    }
)

Find RefundResponse model here.
Find RefundTransactionFailure model here.

Reverse Transaction

Reverses a transaction by providing the intent ID.

terminal.reverseTransaction(
        intentId = intentUuid,
        object : ReverseTransactionListener {
            override fun onReverseTransactionCompleted(reverseResponse: ReverseResponse) {
                CoroutineScope(Dispatchers.Main).launch {
                    Log.d("Transaction", "Transaction reversed: $reverseResponse")
                }
            }

            override fun onReverseTransactionFailure(reverseTransactionFailure: ReverseTransactionFailure) {
                    Log.d("Transaction", "Transaction reverse failure: $reverseTransactionFailure")

            }

        }
    )

Find ReverseResponse model here.
Find ReverseTransactionFailure model here.

Reconcile

Reconciles a terminal's unreconciled transactions.

terminal.reconcile(
    reconcileListener = object : ReconcileListener {

        override fun onReconcileCompleted(reconciliationReceiptsResponse: ReconciliationReceiptsResponse) {
            // Handle success
            Log.d("Reconcile", "Reconcile Success: $reconciliationReceiptsResponse")
        }

        override fun onReconcileFailure(reconcileFailure: ReconcileFailure) {
            // Handle failure
            Log.d("Reconcile", "Reconcile Failure: $reconcileFailure")
        }
    }
)

Find ReconciliationReceiptsResponse model here.
Find ReconcileFailure model here.

Add Tip to purchase

Add tip to purchase transaction


    var amount = 100
    var transactionUuid = "123456" // the purchase UUID that will add tip to
terminal.tipTransaction(
    id = transactionUuid,
    amount = amount,
    tipTransactionListener = object : TipTransactionListener{
        override fun onTipTransactionSuccess(purchaseResponse: PurchaseResponse) {
            // Handle completed transaction
            Log.d("Tip", "Tip completed: ${purchaseResponse.getLastReceipt()}" )
        }

        override fun onTipTransactionFailure(failure: TipTransactionFailure) {
            // Handle transaction failure
            Log.d("Tip", "Tip failure: $failure")
        }

    }
)

Find PurchaseResponse model here.
Find TipTransactionFailure model here.

Get Intents List

Retrieves a paginated list of intents.

terminal.getIntentsList(
    page = 1,
    pageSize = 10,
    isReconciled = true, // do not specify it if all transactions are needed
    startDate: "2025-09-27 18:00:00.000",
    endDate: "2025-10-04 18:00:00.000",
    customerReferenceNumber = "customer_reference_number", // Optional customer reference number
    getIntentsListListener = object : GetIntentsListListener {

        override fun onGetIntentsListSuccess(intentsList: IntentsListResponse) {
            Log.d("intentsList", "intents List : $intentsList")
        }

        override fun onGetIntentsListFailure(error: GetIntentsListFailure) {
            Log.d("intentsList", "intents List Failure: $error")
        }
    }
)

Find GetIntentsList model here.
Find GetIntentsListFailure model here.

Get Intent Details

Retrieves the details of a specific intent by providing the intent ID.

var intentID = "cc8b303d-b448-4ef9-80e6-e8e51bb43766"
terminal.getIntent(
    intentID, // intent ID
        getIntentListener = object : GetIntentListener {

        override fun onGetIntentSuccess(intent: IntentDetails) {
            Log.d("intent", "Intent Details: $intent")
        }

        override fun onGetIntentFailure(error: GetIntentFailure) {
            Log.d("Transaction", "Transaction Details Failure: $error")
        }
    }
)

Find IntentDetails model here.
Find GetIntentsFailure model here.

Get Reconciliation List

Retrieves a paginated list of reconciliations.

terminal.getReconciliationList(
    page = 1,
    pageSize = 10,
    startDate = null, //in timestamp format
    endDate = null, //in timestamp format
    getReconciliationListListener = object : GetReconciliationListListener {
        override fun onGetReconciliationListSuccess(reconciliationListResponse: ReconciliationListResponse) {
            // Handle success
            Log.d("Reconciliation", "Reconciliation List: $reconciliationListResponse")
        }

        override fun onGetReconciliationListFailure(error: GetReconciliationListFailure) {
            // Handle failure
            Log.d("Reconciliation", "Reconciliation List Failure: $error")
        }
    }
)

Find ReconciliationListResponse model here.
Find GetReconciliationListFailure model here.

Get Reconciliation Details

Retrieves the details of a specific reconciliation by providing the reconciliation ID.

terminal.getReconciliation(
    "reconciliation-id", // Reconciliation ID
    getReconciliationListListener = object : GetReconciliationListener {
        override fun onGetReconciliationSuccess(reconciliationReceiptsResponse: ReconciliationReceiptsResponse) {
            // Handle success
            Log.d("handleReadCard", "GetReconciliation success $reconciliationReceiptsResponse")
        }

        override fun onGetReconciliationFailure(error: GetReconciliationFailure) {
            // Handle failure
            Log.d("handleReadCard", "GetReconciliation failure $error")
        }
    }
)

Find ReconciliationReceiptsResponse model here.
Find GetReconciliationFailure model here.

Converting receipt to image for printing

You can convert the receipt to an image so you can print it.

val madaReceipt = receipt.getMadaReceipt()
val receiptWidth = 384 // this is the width of the image
val fontSize = 1 // this is the size of the receipt text, it is from 1 to 10

madaReceipt.toImage(this@MainActivity, 384, 1, { bitmap ->
    if (bitmap != null) {
        printBitmap(bitmap)
    }
})

Part 7: Callback Listeners

Callback Listeners

SendOTPMobileListener

onSendOTPMobileSuccess

Called when OTP is successfully sent.

  • Parameter: OtpResponse

onSendOTPMobileFailure

Called when OTP sending fails.

  • Parameter: OTPMobileFailure

SendOTPEmailListener

onSendOTPEmailSuccess

Called when OTP is successfully sent to email.

  • Parameter: OtpResponse

onSendOTPEmailFailure

Called when OTP sending to email fails.

  • Parameter: OTPEmailFailure

VerifyMobileListener

onVerifyMobileSuccess

Called when OTP verification and user authentication succeed.

  • Parameter: User

onVerifyMobileFailure

Called when OTP verification fails.

  • Parameter: VerifyMobileFailure

VerifyEmailListener

onVerifyEmailSuccess

Called when email OTP verification succeeds.

  • Parameter: User

onVerifyEmailFailure

Called when email OTP verification fails.

  • Parameter: VerifyEmailFailure

GetTerminalsListener

onGetTerminalsSuccess

Called when terminals are successfully retrieved.

  • Parameter: List<TerminalConnection>

onGetTerminalsFailure

Called when fetching terminals fails.

  • Parameter: GetTerminalsFailure

ConnectTerminalListener

onConnectTerminalSuccess

Called when terminal connection is successful.

  • Parameter: Terminal

onConnectTerminalFailure

Called when terminal connection fails.

  • Parameter: ConnectTerminalFailure

ReadCardListener

onReadCardSuccess

Called when card reading is successful.

onReadCardFailure

Called when card reading fails.

  • Parameter: ReadCardFailure

onReaderDisplayed

Called when the reader is displayed.

onReaderClosed

Called when the reader is closed.

onReaderWaiting

Called when the reader is waiting for a card.

onReaderReading

Called when the reader is actively reading the card.

onReaderRetry

Called when the reader is retrying.

onPinEntering

Called when the reader prompts for PIN entry.

onReaderFinished

Called when the reader operation is completed.

onReaderError

Called when an error occurs in the reader.

  • Parameter: String

SendTransactionListener

onSendTransactionCompleted

Called when the transaction is successfully processed.

  • Parameter: TransactionResponse

onSendTransactionFailure

Called when the transaction fails.

  • Parameter: SendTransactionFailure

GetTransactionsListListener

onGetTransactionsListSuccess

Called when the transactions list is successfully retrieved.

  • Parameter: TransactionsResponse

onGetTransactionsListFailure

Called when fetching transactions list fails.

  • Parameter: GetTransactionsListFailure

GetTransactionListener

onGetTransactionSuccess

Called when a specific transaction's details are successfully retrieved.

  • Parameter: ReceiptsResponse

onGetTransactionFailure

Called when fetching transaction details fails.

  • Parameter: GetTransactionFailure

ReconcileListener

onReconcileCompleted

Called when transaction reconciliation is successful.

  • Parameter: ReconciliationReceiptsResponse

onReconcileFailure

Called when transaction reconciliation fails.

  • Parameter: ReconcileFailure

GetReconciliationListListener

onGetReconciliationListSuccess

Called when the reconciliation list is successfully retrieved.

  • Parameter: ReconciliationListResponse

onGetReconciliationListFailure

Called when fetching reconciliation list fails.

  • Parameter: GetReconciliationListFailure

GetReconciliationListener

onGetReconciliationSuccess

Called when specific reconciliation details are successfully retrieved.

  • Parameter: ReconciliationReceiptsResponse

onGetReconciliationFailure

Called when fetching reconciliation details fails.

  • Parameter: GetReconciliationFailure

CancelTransactionListener

onCancelTransactionSuccess

Called when transaction cancellation is successful.

  • Parameter: Canceled

onCancelTransactionFailure

Called when transaction cancellation fails.

  • Parameter: CancelTransactionFailure

RefundTransactionListener

onRefundTransactionCompleted

Called when refund transaction is successful.

  • Parameter: TransactionResponse

onRefundTransactionFailure

Called when refund transaction fails.

  • Parameter: RefundTransactionFailure