App-to-App integration

1. Introduction & Overview

1.1 What is App-to-App Integration?

App-to-App integration is the solution that allows your application (the Electronic Cash Register or POS system) to securely communicate with the Market Pay Payment Application on the same device.

This is achieved using Inter-Process Communication (IPC). In the Android environment, IPC enables communication between two separate applications running simultaneously on the same device.

1.2 Why Use the Market Pay SDK?

Integrating the Market Pay SDK is the simplest way to establish App-to-App communication. The SDK's primary purpose is to abstract away the complex, low-level details of Android IPC, such as AIDL (Android Interface Definition Language) and Binder connectivity.

Instead of dealing with raw data marshalling and process binding, the SDK provides a clear, high-level Client interface (a Plain Old Java Object or POJO). This interface allows your developers to initiate actions like payments and log in using simple, intuitive method calls, such as sendPaymentRequest().

The SDK also includes robust features for message handling and state management:

  • Interprotocol Conversion: Automatically converts messages between different protocol standards (e.g., Nexo Retailer Protocol) as needed.
  • Connectivity Handling: Manages connecting, disconnecting, and reconnecting to the Payment Application service.
  • Asynchronous Feedback: Uses an Interceptor to deliver transaction responses and an EventObserver to notify your application of state changes (e.g., IdleTransactionState, PaymentTransactionState).

1.3 Prerequisite Checklist

Before starting the SDK setup, please ensure the following requirements are met:

RequirementValue / ActionNotes
Android Min SDK22Required for the application building the SDK integration.
Android Target SDK30Required for the application building the SDK integration.
SDK Repository AccessUsername & PasswordRequired to fetch the SDK from the remote Novelpay repository.

2. Setup: Installing and Initializing the SDK

The manual file installation method has been replaced by a remote Maven repository integration.

2.1 Step 1: Configure the SDK Repository

Instead of adding a local file, you must now configure your project to pull the SDK from the Novelpay servers.

A. Define Credentials

Add your credentials to your project's gradle.properties file to keep them secure and reusable:

MAVEN_LOGIN=your_login
MAVEN_PASSWORD=your_password

B. Update settings.gradle

Add the remote repositories to the dependencyResolutionManagement block in your settings.gradle file.

Note: Because these repositories use HTTP, you must set allowInsecureProtocol = true. If you encounter build errors, you may need to comment out the FAIL_ON_PROJECT_REPOS line

dependencyResolutionManagement {
    // Comment this line if it causes problems with third-party repositories
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 
    
    repositories {
        google()
        mavenCentral()
        
        // Novelpay Release Repository
        maven {
            url = "http://public.novelpay.pl:8088/repository/novelpay-android-release/"
            credentials {
                username "your_login"
                password "your_password"
            }
            allowInsecureProtocol = true
        }

        // Novelpay Android Repository
        maven {
            url = "http://public.novelpay.pl:8088/repository/novelpay_android_repository/"
            credentials {
                username "$MAVEN_LOGIN"
                password "$MAVEN_PASSWORD"
            }
            allowInsecureProtocol = true
        }
    }
}

2.2 Step 2: Add the SDK Dependency

Declare the library in your module-level build.gradle file. The SDK is now versioned and managed through Gradle.

dependencies {
    // ... other dependencies
    implementation 'pl.novelpay.sdk:client-client:1.3.2'
}

For Android 11 and above clients have to add:

<queries>
 <package android:name="com.marketpay.pos" />
</queries>

Otherwise IPC connectivity between apps will be broken

3. Core Usage: Sending Requests and Handling Responses

3.1 Nexo Retailer protocol specifics

Every communication session should start with a RetailerLoginRequest. This should be triggered in following cases:

  • Payment Application update
  • Payment Application restart
  • Client's (your) application restart
  • Connection between applications was lost
  • RetailerLogoutRequest was sent

New Login procedure should be triggered to instantiate the session. Otherwise requests from your application will be considered unauthorised and will be ignored by Payment Application.

3.1 Sending Requests (Client Interface)

The Client interface instance returned by ClientFactory::build() is your primary communication tool. All methods are suspended functions and should be called from a coroutine scope.

ActionMethodNotes
Start Sessionsuspend fun sendLoginRequest()Must be called first. Triggers a new session after any interruption (restarts, updates, loss of connection).
Paymentsuspend fun sendPaymentRequest(...)Used for Sales, Refunds, Pre-Authorizations, and Completions.
Cancellationsuspend fun sendReversalRequest(...)Used for transaction cancellation.
Status Checksuspend fun sendTransactionStatusRequest(...)Retrieves status of the last completed transaction.

Login Request

Every integration must start by establishing a secure session. The login request must be called at the start of your application and re-triggered if the connection is lost or the terminal app restarts.

// Establishing a secure communication session
fun performLogin() {
    viewModelScope.launch {
        try {
            client.sendLoginRequest()
            // Success is handled via your registered Interceptor
        } catch (e: Exception) {
            Logger.e("Login failed to initiate: ${e.message}")
        }
    }
}

Initiating a Payment

This is the core function for processing financial transactions. It requires a unique transaction ID for tracking and the specific amounts to be charged.

// Initiating a standard payment transaction
fun startPayment(amount: Double) {
    viewModelScope.launch {
        client.sendPaymentRequest(
            transactionID = "Txn_${UUID.randomUUID()}", // Unique sale ID
            paymentAmounts = PaymentAmounts(amount, "EUR"), 
            paymentType = PaymentType.Normal
        )
    }
// Responses will be processed asynchronously in the Interceptor (Section 3.2).

TransactionID should be maximum 35 long. If you use UUID, remove the "-" (hyphen) from the generated UUID.

Transaction Status Request

Use this to check the final outcome of a transaction if your app lost connection during processing. It retrieves the status of the last completed message.

// Checking the status of a specific transaction reference
fun checkStatus(reference: MessageReference) {
    viewModelScope.launch {
        client.sendTransactionStatusRequest(reference)
    }
}

Reversal Request

If a transaction was completed in error (e.g., wrong amount entered), the reversal request cancels that specific record using the original terminal transaction ID.

// Canceling a previously completed transaction
fun reverseTransaction(originalPoiId: String) {
    viewModelScope.launch {
        client.sendReversalRequest(
            saleReferenceId = "Sale_Ref_98765",
            poiTransactionId = originalPoiId, // The ID returned by the terminal in the original response
            transactionData = ReversalRequestMessageArguments.TransactionAmountsData(...)
        )
    }
}

Update

This function is dedicated to administrative terminal tasks, such as triggering software updates or downloading latest parameters available.

// Triggering administrative tasks like terminal updates
fun updateTerminal() {
    viewModelScope.launch {
        client.sendAdminRequest(RetailerAdminExtension.Update)
    }
}

Diagnosis

A simple "heartbeat" check to verify the link between your app and the payment terminal is still active without initiating a full transaction.

// Verifying the heartbeat connection to the terminal
fun testConnection() {
    viewModelScope.launch {
        client.sendTestConnectionRequest(
            DiagnosisRequestMessageArguments(messageId = "test_123")
        )
    }
}

Logout request

 

// Closing the current session gracefully
fun performLogout() {
    viewModelScope.launch {
        client.sendLogoutRequest()
    }
}

 

3.2 Receiving Responses (The Interceptor)

All responses to the requests sent via the Client interface are delivered to the Interceptor you defined in the setup.

Incoming parameter has generic type DomainMessage all of the messages in App-to-App communication are subtypes of this class. to specify handling strategy for each response typecheck is recommended. 

Each operation as (Payment, Reversal, Refund...) has it's own message type. It can be found in documentation package retailer-protocol. 

Map of requests and responses for main operations:

OperationRequest TypeResponse TypeExpected Response Subtypes
LoginRetailerLoginRequestRetailerLoginResponseSuccessRetailerLoginResponse, FailedRetailerLoginResponse
PaymentRetailerPaymentRequest

RetailerPaymentResponse

RetailerDisplayRequest

SuccessRetailerPaymentResponse, ErrorRetailerPaymentResponse
AbortRetailerAbortRequestRetailerAbortResponseErrorRetailerPaymentResponse
ReversalRetailerReversalRequestRetailerReversalResponseSuccessRetailerReversalResponse, ErrorRetailerReversalResponse
StatusTransactionStatus

RetailerTransactionStatusResponse

RetailerDisplayRequest

SuccessRetailerTransactionStatusResponse, ErrorRetailerTransactionStatusResponse, RetailerDisplayRequest

Note. Payment request are used for Refunds, Pre-Authorisations and Pre-Authorisation Completions as well. There is no separate subtypes for those operations. DisplayRequest will notify during processing which operation is being executed.

4. Handling State and Events

4.1 The EventObserver (State Changes)

Use the bindEventObserver to monitor the Payment Application's operational status and manage your application's user interface.

ObservableEvent.ServerEvent this event signals that a specific internal action or notification has occurred within the Payment Application.

The key event is ObservableEvent.TransactionStateChanged.

StateDescriptionForeground Recommendation
PaymentTransactionStatePayment Processing has started.Optional: Bring your application to the foreground (e.g., to show a custom UI during authorization).
IdleTransactionStatePayment Application is finished with the transaction.Recommended: Bring your application to the foreground to display the final result.
bindEventObserver { event: ObservableEvent
when (event) {
is ObservableEvent.ServerEvent Logger.d("New server event"
is ObservableEvent.TransactionStateChanged
when (event.state) {
is IdleTransactionState
bringToForeground(MainActivity :class
else
Ignor
}
}
}
}

4.2 BringToForeground

bringToForeground(MainActivity :class) - is function, responsible for "dragging" to foreground your application after state changes in Payment Application. MainActivity:class is placeholder for your currently running application's activity. If your application is Single-Activity you can don't worry about activity transitions so just provide your Main Activity class as in example.

  • PaymentTransactionSate -> If active/present: switch back to your application during payment authorization.
  • IdleTransactionState -> If active/present: swith back to your application at the end of payment.

For example: You'd like show advertisement in your application or custom transaction details during payment processing instead of Payment Application's spinner. All you have to do to achieve it: specify path for PaymentTransactionState.


illustration:

.bindEventObserver { event: ObservableEvent -> 
    when (event) {
        is ObservableEvent.TransactionStateChanged -> when (event.state) {
            // Bring your app to the foreground when payment finishes
            is IdleTransactionState -> bringToForeground(MainActivity::class.java) 
            // Optional: Bring your app to the foreground when payment starts
            is PaymentTransactionState -> bringToForeground(MainActivity::class.java)
            else -> { /* Ignore other states */ } 
        }
        is ObservableEvent.ServerEvent -> Logger.d("New server event") 
    } 
}

 

SYSTEM_ALERT_WINDOW - permission should be granted to your application to proper work of this function

 

Example to bring your app to foreground when authorization is in progress:

bindEventObserver { event: ObservableEvent
when (event) {
is ObservableEvent.ServerEvent Logger.d("New server event"
is ObservableEvent.TransactionStateChanged
when (event.state) {
is PaymentTransactionState
bringToForeground(MainActivity :class
else
Ignor
}
}
}
}

Example to bring your app to foreground when payment is completed:

bindEventObserver { event: ObservableEvent
when (event) {
is ObservableEvent.ServerEvent Logger.d("New server event"
is ObservableEvent.TransactionStateChanged
when (event.state) {
is IdleTransactionState
bringToForeground(MainActivity :class
is PaymentTransactionState
bringToForeground(MainActivity :class
else
Ignor
}
}
}
}

5. Appendix: ProGuard Rules

If your project uses obfuscation via R8/ProGuard, ensure the following rules are added to your proguard-rules.pro file.

 -keep class pl.novelpay.** { *; } 
 -dontwarn pl.novelpay.** 
 -keep class org.koin.** { *; } 
 -keep class androidx.lifecycle.** { *; } 
 -keep class com.jakewharton.threetenabp.** { *; } 
 -dontwarn com.google.gson.annotations.SerializedName  -dontwarn javax.xml.stream.Location 
 -dontwarn javax.xml.stream.XMLEventReader 
 -dontwarn javax.xml.stream.XMLInputFactory 
 -dontwarn javax.xml.stream.events.Attribute 
 -dontwarn javax.xml.stream.events.Characters 
 -dontwarn javax.xml.stream.events.StartElement 
 -dontwarn javax.xml.stream.events.XMLEvent 
 -dontwarn kotlinx.parcelize.Parcelize 
 -keepattributes
InnerClasses,Signature,RuntimeVisible*Annotations,EnclosingMethod  -dontwarn com.google.gson.TypeAdapter 
 -dontwarn com.google.gson.stream.JsonReader  -dontwarn com.google.gson.stream.JsonWriter  -keep class kotlin.Metadata { *; } 
 -keep class org.simpleframework.** { *; } 
 # Preserve all annotations. 
 -keepattributes Annotation 

6. Attachments

 

Related to