Quickstart: Your first payment with App2App

  • Updated

This guide takes you from an empty Android project to a successful card payment through the Market Pay payment application — login, payment, and reading the result. It assumes you know Android and Kotlin but are new to the SDK.

By the end you will have:

  1. Added the SDK from the Market Pay Maven repository.
  2. Declared the payment app so Android lets your app talk to it.
  3. Initialized the SDK once, at application startup.
  4. Logged in to open a session.
  5. Sent a payment and read the response.

The exhaustive list of types, fields, and enum values lives in the API reference (the Dokka documentation shipped as sdk-doc.zip in the Maven repository). This guide inlines only what you need for a first payment and names every type exactly, so you can look any of them up there.


Before you start

You need three things:

  • Maven repository credentials (a username and password) issued by Market Pay.
  • A device or emulator with the Market Pay payment application installed. App2App is on-device inter-process communication — both apps run on the same device. There is no network call between your app and the payment app.
  • An Android module targeting min SDK 22, target SDK 30.

If you do not yet have Maven credentials, request them from Market Pay before continuing — step 1 will not resolve the dependency without them.


1Add the SDK dependency

The SDK is distributed from the Market Pay Maven repository over HTTP on a non-standard port, so two settings that are unusual for a normal Maven repo are required here: allowInsecureProtocol = true, and credentials on the repo.

1a. Store your credentials outside the repo

Put the credentials in your global Gradle properties file (~/.gradle/gradle.properties), not in the project, so they are never committed:

MAVEN_LOGIN=your_login
MAVEN_PASSWORD=your_password

1b. Declare the repositories

In settings.gradle, inside dependencyResolutionManagement { repositories { … } }:

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

If the build fails on FAIL_ON_PROJECT_REPOS: comment out the repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) line in the same block. It conflicts with declaring repositories this way.

1c. Add the dependency

In your module-level build.gradle:

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

Sync. If the dependency resolves, your credentials and repositories are correct. A 401 here means the credentials are wrong or missing — see Troubleshooting.


2Declare the payment app (required on Android 11+)

Android 11 introduced package visibility: by default your app cannot see or bind to other apps on the device. App2App binds to the payment application, so you must declare it explicitly. Add this to your AndroidManifest.xml, as a direct child of the <manifest> element:

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

Without this, IPC binding silently fails on Android 11 and above — no error, no exception, just no connection. This is the single most common setup mistake.


3Initialize the SDK once

The SDK is a single long-lived instance tied to your application lifecycle. Build it once in your Application class, not per-screen. Initialization wires together four things: who your app is, the protocol configuration, an interceptor that receives every response, and an event observer that receives state changes.

val sdk: Client = ClientFactory.bindConfiguration(
    SDKConfiguration.defaultConfiguration(
        clientApplicationInfo = PackageInfo(
            applicationName = applicationInfo.name,
            applicationPackage = packageName
        )
        // paymentApplicationPackage defaults to the Market Pay payment app.
        // Override it only if you are told to for a non-standard environment.
    )
).bindContext(
    context = applicationContext
).bindProtocolConfiguration(
    RetailerProtocolFactory.createProtocol(
        RetailerConfiguration(
            saleID = "YOUR_SALE_ID",
            operatorId = "YOUR_OPERATOR_ID",
            softwareComponent = SoftwareComponent(
                providerCompanyIdentifier = "YourCompany",
                name = "YourApp",
                softwareVersion = "1.0.0"
            )
        )
    )
).bindInterceptor(
    interceptor = { message ->
        // Every response from the payment app arrives here, as a DomainMessage.
        // We handle specific responses in Step 5.
        Result.success(message)
    }
).bindEventObserver { event ->
    // State changes (payment started / finished) arrive here. See "Next steps".
}.build()

The configuration you pass to RetailerConfiguration identifies your point of sale to the payment application. saleID, operatorId, and softwareComponent are the essentials; the API reference lists the optional fields (poiID, operatorLanguage).


4Log in to open a session

Every session must begin with a login. Until you receive a successful login response, the payment application treats every other request as unauthorized and silently ignores it. This is the second most common mistake after the missing <queries> element.

viewModelScope.launch {
    clientSDK.sendLoginRequest()
}

sendLoginRequest is a suspend function — call it from a coroutine. The call returns quickly; the result arrives asynchronously in the interceptor you registered in Step 3, not as a return value. That is the core pattern of this SDK: you send a request, and the response comes back later through the interceptor (more on this in Understanding the message model).

You must log in again whenever the session is interrupted — specifically after:

  • the payment application is updated,
  • the payment application restarts,
  • your application restarts,
  • the connection between the two apps is lost,
  • you sent a logout request.

A practical rule: trigger login at your app's startup, and retry it if any request stops getting responses.


5Send a payment and read the result

Send the payment

A payment is sent with sendPaymentRequest, passing a RegularPaymentRequestMessageArguments. For a first payment you need exactly three fields:

  • saleTransactionIdyour reference for this sale. Must be alphanumeric and 35 characters or fewer. If you generate it from a UUID, strip the hyphens, or you will exceed the limit.
  • paymentAmounts — a PaymentAmounts carrying the currency (e.g. "EUR", which must match the terminal's currency) and the amount as a BigDecimal.
  • paymentType — a PaymentType enum value. Use PaymentType.NORMAL for a standard sale.
viewModelScope.launch {
    clientSDK.sendPaymentRequest(
        RetailerMessageArguments.PaymentRequestMessageArguments
            .RegularPaymentRequestMessageArguments(
                saleTransactionId = "Sale001",
                paymentAmounts = PaymentAmounts(
                    currency = "EUR",
                    amount = BigDecimal("10.00")
                ),
                paymentType = PaymentType.NORMAL
            )
    )
}

The same sendPaymentRequest call handles refunds, pre-authorizations, and completions too — you change paymentType, not the method. PaymentType has 13 values in total; see the API reference for the full set.

Read the response

The payment result arrives in your interceptor as a subtype of RetailerPaymentResponse. Type-check the incoming message to branch on outcome:

.bindInterceptor(
    interceptor = { message ->
        when (message) {
            is SuccessRetailerPaymentResponse -> {
                // Payment approved — read amounts, card data, POI transaction id.
            }
            is ErrorRetailerPaymentResponse -> {
                // Payment declined or failed — inspect the error detail.
            }
            is RetailerDisplayRequest -> {
                // Progress updates during processing (e.g. which operation is running).
            }
        }
        Result.success(message)
    }
)

SuccessRetailerPaymentResponse and ErrorRetailerPaymentResponse are the two outcomes you must handle. RetailerDisplayRequest messages arrive during processing to tell you what the terminal is doing — handle them if you want to show progress, ignore them otherwise.

That's a complete round trip: setup, session, payment, result.


Troubleshooting your first integration

Nothing happens when I send a request — no response, no error. Almost always one of two things. Either the <queries> element is missing from your manifest (Step 2), so on Android 11+ the binding never forms; or you have not had a successful login yet (Step 4), so the payment app is ignoring your requests as unauthorized. Check those two before anything else.

Login fails with a BUSY condition. The payment application is still starting up (initializing). This is expected transient behaviour, not a configuration error — retry the login after a short delay. If login keeps failing with a non-BUSY error, the login request itself has bad field data; the error response carries a description.

The dependency won't resolve / I get a 401. Your Maven credentials are missing or incorrect. Confirm MAVEN_LOGIN and MAVEN_PASSWORD are set in ~/.gradle/gradle.properties and that the values are the ones Market Pay issued. If the build instead complains about an insecure or HTTP repository, you are missing allowInsecureProtocol = true on the repo (Step 1b).


Next steps

You have a working payment. From here:

  • Understanding the message model — why responses come back through the interceptor, and the Success* / Error* sealed-type pattern. Read this before building real response handling.
  • Session & login lifecycle — handling the interruption cases above robustly.
  • State & foreground handling — using the event observer and bringToForeground to control the screen during a payment.
  • API reference — the complete, authoritative list of every type, field, and enum (sdk-doc.zip). Whenever this guide names a type, you can look up its full signature there.

Was this article helpful?

0 out of 0 found this helpful