Skip to Content
SDKsTaplink SDK (Local Integration)Android (Kotlin)

Taplink SDK for Android

GitHub Repository: sunbay-taplink-sdk-android 

Taplink SDK is a payment integration SDK provided by SUNBAY for Android POS applications. It enables developers to quickly integrate payment capabilities with support for multiple connection modes and comprehensive transaction APIs.

Features

  • Quick Integration - Complete basic integration in just 3 steps
  • Integration Modes - On-Device, Cross-Device (LAN / Cable / Auto) and Sub-Screen
  • Comprehensive Transaction Types - Sale, Refund, Void, Auth, Query, and more
  • Robust Error Handling - Structured error codes with handling suggestions
  • Modern Architecture - Built with Kotlin and Coroutines
  • High Performance - Optimized for fast transaction processing

Quick Start

Installation

Add the dependency to your app module’s build.gradle.kts:

dependencies { implementation("com.sunmi:sunbay-taplink-sdk-android:1.0.9") }

All required permissions are already declared in the SDK module’s manifest and will be automatically merged into your app.

Basic Integration (3 Steps)

Step 1: Initialize SDK

Initialize the SDK in your Application class:

class MyApplication : Application() { override fun onCreate() { super.onCreate() val config = TaplinkConfig() .setAppId("your_app_id") .setSecretKey("your_secret_key") TaplinkSDK.init(this, config) } }

Step 2: Connect to Payment Terminal

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Connect to TaPro. Pick the integration mode that matches your hardware: // On-Device - business app and TaPro run on the same terminal // Cross-Device - TaPro is a separate PIN Pad reached over LAN or a cable val connectionConfig = ConnectionConfig.createOnDeviceMode() TaplinkSDK.connect(connectionConfig, object : ConnectionListener { override fun onConnected(deviceId: String, taproVersion: String) { // Connection successful Toast.makeText(this@MainActivity, "Connected to Tapro $taproVersion", Toast.LENGTH_SHORT).show() } override fun onDisconnected(reason: String) { // Connection failed Toast.makeText(this@MainActivity, "Connection failed: $reason", Toast.LENGTH_SHORT).show() } override fun onError(error: ConnectionError) { // Connection error Toast.makeText(this@MainActivity, error.message, Toast.LENGTH_SHORT).show() } }) } }

Step 3: Process Payment

private fun processPayment() { // Get TaplinkClient instance val client = TaplinkSDK.getClient() // Create sale request val amount = AmountInfo() .setOrderAmount(BigDecimal("1000")) // 1000 cents = USD 10.00 .setPricingCurrency("USD") val request = SaleRequest.builder() .setReferenceOrderId("ORDER_${System.currentTimeMillis()}") .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setAmount(amount) .setPaymentMethod(PaymentMethodInfo(PaymentCategory.CARD)) .setDescription("Product Purchase") .build() // Execute sale transaction client.sale(request, object : PaymentCallback { override fun onSuccess(result: PaymentResult) { // onSuccess = terminal returned a final response. Inspect the result to // determine the actual outcome. when { result.isSuccess() -> { // Transaction approved by issuer Toast.makeText(this@MainActivity, "Payment approved: ${result.transactionId}", Toast.LENGTH_SHORT).show() } result.isFailed() -> { // Transaction declined, cancelled, or failed // code: SDK standard error code (e.g. "307") // message: Detailed error from Tapro (e.g. "K004: Insufficient funds (051)") Toast.makeText(this@MainActivity, "Payment failed: ${result.message}", Toast.LENGTH_SHORT).show() } result.isProcessing() -> { // Gateway still deciding — poll with client.query() pollForFinalStatus(result.transactionRequestId!!) } } } override fun onFailure(error: PaymentError) { // Technical/communication error — no response received from terminal. // This is NOT a card decline. Toast.makeText(this@MainActivity, "Error: ${error.message}", Toast.LENGTH_SHORT).show() } override fun onProgress(event: PaymentEvent) { // Update progress UI updateProgressUI(event.message) } }) }

That’s it! You’ve completed the basic integration in just 3 steps.

Connection Modes

Reorganized in v1.0.9. A connection is now described by integration mode — where your business app and TaPro physically live relative to each other — and, for a separate terminal, by the channel used to reach it. The transport-oriented API from v1.0.8 (APP_TO_APP / LAN / CABLE) is fully supported and unchanged; see Choosing a mode for the mapping.

Integration Modes

ModeHardware layoutChannel
ON_DEVICEBusiness app and TaPro run on the same terminalLocal (App-to-App)
CROSS_DEVICETaPro is a separate terminal acting as a PIN PadCrossDeviceStrategy.LAN / CABLE / AUTO
SUB_SCREENSeparate terminal used as a customer-facing displayUSB VSP (fixed)
// Same terminal val onDevice = ConnectionConfig.createOnDeviceMode() // Separate terminal over LAN val lan = ConnectionConfig.createCrossDeviceMode( CrossDeviceStrategy.LAN, host = "192.168.1.100", port = 8443 ) // Separate terminal over a cable (cable try-order: VSP -> RS232 -> AOA) val cable = ConnectionConfig.createCrossDeviceMode(CrossDeviceStrategy.CABLE) // Separate terminal, SDK picks the channel and re-routes if the active one drops val auto = ConnectionConfig.createCrossDeviceMode( CrossDeviceStrategy.AUTO, host = "192.168.1.100", port = 8443 ) // Customer-facing display over USB val subScreen = ConnectionConfig.createSubScreenMode()

CrossDeviceStrategy has no CLOUD value. Cloud-dispatched orders are delivered to TaPro directly by the cloud backend — the SDK never establishes or drives a Cloud connection. A Cross-Device / Cloud badge shown on the TaPro screen is a TaPro-side display concern only.

On-Device Mode

For Android all-in-one terminals where the POS app and TaPro run on the same device.

val connectionConfig = ConnectionConfig.createOnDeviceMode() TaplinkSDK.connect(connectionConfig, connectionListener)

Features:

  • Millisecond-level latency
  • Automatic detection
  • No additional configuration required

ConnectionMode.APP_TO_APP / ConnectionConfig.createAppMode() remain valid and behave identically — ON_DEVICE normalizes onto App-to-App internally. createAppMode(AppToAppMode.HEADLESS) and the other App-to-App options are unaffected.

Cross-Device over Cable

For a POS device connected to a separate payment terminal by USB or serial cable.

// Auto cable protocol (recommended): VSP -> RS232 -> AOA val connectionConfig = ConnectionConfig.createCrossDeviceMode(CrossDeviceStrategy.CABLE) // Or pin a specific protocol val vspOnly = ConnectionConfig.createCrossDeviceMode( CrossDeviceStrategy.CABLE, cableProtocol = CableProtocol.USB_VSP ) TaplinkSDK.connect(connectionConfig, connectionListener)

Supported Protocols:

  • USB-VSP (USB Virtual Serial Port)
  • RS232 (Standard serial communication)
  • USB AOA (Android Open Accessory 2.0)

CableProtocol.AUTO only chooses among these three cable protocols and never switches to LAN. Cross-transport failover requires CrossDeviceStrategy.AUTO (below). The classic ConnectionMode.CABLE / createCableMode(protocol) API is unchanged.

Cross-Device over LAN

For a POS device that reaches the payment terminal over the local network (wired or wireless).

private val connectionListener = object : ConnectionListener { override fun onConnected(deviceId: String, taproVersion: String) { } override fun onDisconnected(reason: String) { } override fun onError(error: ConnectionError) { } } private fun connectLan(manualHost: String? = null, manualPort: Int? = null) { // Cross-Device over LAN. The classic ConnectionMode.LAN form is equivalent. val config = ConnectionConfig.createCrossDeviceMode(CrossDeviceStrategy.LAN) // If user entered host/port, use user input first. if (!manualHost.isNullOrBlank()) { config.setHost(manualHost) } if (manualPort != null) { config.setPort(manualPort) } // Important: register global listener, then pass the same listener to connect(). // This is required for runtime service-address-change auto reconnect. TaplinkSDK.setConnectionListener(connectionListener) TaplinkSDK.connect(config, connectionListener) } // Example A: user manually specifies 8443 connectLan(manualHost = "192.168.1.100", manualPort = 8443) // Example B: no manual port, SDK uses cached/discovered LAN address connectLan()

Features:

  • TLS encryption
  • mDNS auto-discovery
  • Automatic IP/port update handling

Behavior Notes:

  • Manual host/port is applied first when provided by the user.
  • During runtime, when mDNS reports that the same terminal moved to a new host/port (for example 8443 -> 8444), the SDK can reconnect automatically.
  • For address-change auto reconnect, you must both call TaplinkSDK.setConnectionListener(listener) and pass a listener in TaplinkSDK.connect(config, listener).
  • CrossDeviceStrategy.LAN never falls back to a cable, even if a cable is physically attached. Use CrossDeviceStrategy.AUTO if you want that.

Available since v1.0.8. Automatic LAN address acquisition. Instead of asking the user to type an IP address and port, the SDK can obtain the LAN terminal address in two ways: mDNS auto-discovery or QR code scan.

These APIs return the resolved host/port through a DiscoveryListener; they do not open a connection themselves. Auto-fill your address input fields (and persist them if needed), then establish the connection with the standard LAN flow shown above. This keeps a single, predictable connection code path.

Option 1 — mDNS Discovery (discoverLanServices)

Runs a one-shot mDNS (Android NSD) discovery of _taplink._tcp services and returns all resolved services. Discovery times out automatically after 15 seconds.

TaplinkSDK.discoverLanServices(object : DiscoveryListener { override fun onDiscovered(services: List<DiscoveredService>) { val target = services.firstOrNull() ?: return // DiscoveredService(name, host, port) // Auto-fill your address fields, then connect via the standard LAN flow connectLan(manualHost = target.host, manualPort = target.port) } override fun onError(error: ConnectionError) { showError(error.message) } })

Option 2 — QR Code Scan (scanLanQrCode)

Opens the SDK’s built-in full-screen camera scanner and reads a lan://host/port QR code displayed by Tapro on the terminal. The scanned address is returned as a single-element list. The SDK requests the CAMERA permission and manages the scanner Activity for you.

TaplinkSDK.scanLanQrCode(object : DiscoveryListener { override fun onDiscovered(services: List<DiscoveredService>) { val target = services.firstOrNull() ?: return connectLan(manualHost = target.host, manualPort = target.port) } override fun onError(error: ConnectionError) { when (error.code) { "E504" -> { /* user cancelled the scan — no message needed */ } "E505" -> showError("Camera permission denied") "E506" -> showError("No usable camera available on this device") else -> showError(error.message) } } })

Distinguish a genuine user cancel (E504) from a real failure (permission denied, no camera). Do not collapse every error into a single “cancelled or invalid” message.

One-call variants (discover/scan AND connect)

If you prefer the SDK to connect immediately without backfilling your own fields, use the connect-in-one-step variants. They report the outcome through a ConnectionListener:

// mDNS discover, then connect to the first service that succeeds TaplinkSDK.autoDiscoverAndConnect(connectionListener) // Scan a lan:// QR code, then connect to it TaplinkSDK.scanAndConnect(connectionListener)

Call TaplinkSDK.disconnect() to cancel an in-progress discovery or scan session.

Required host-app dependencies for QR scan

The QR scanner is built on CameraX and ZXing. These are implementation dependencies inside the SDK and are not exposed transitively through the published AAR. To use scanLanQrCode / scanAndConnect, add them to your app module’s build.gradle.kts:

dependencies { // Required only if you use the QR scan APIs val cameraxVersion = "1.3.4" implementation("androidx.camera:camera-core:$cameraxVersion") implementation("androidx.camera:camera-camera2:$cameraxVersion") implementation("androidx.camera:camera-lifecycle:$cameraxVersion") implementation("androidx.camera:camera-view:$cameraxVersion") implementation("com.google.zxing:core:3.5.3") }

The CAMERA permission and scanner Activity are already declared in the SDK manifest. mDNS discovery (discoverLanServices / autoDiscoverAndConnect) needs no extra dependency — it uses Android’s built-in NSD. The scanner selects a back camera, then a front camera, then the first available camera; if none is usable, onError returns code E506.

Discovery / Scan error codes

CodeMeaningApplies to
E501No services found / discovery failedDiscovery
E502All discovered services failed to connectautoDiscoverAndConnect
E503Another discovery/scan operation is already in progressDiscovery & Scan
E504Scan cancelled by the userQR Scan
E505Camera permission deniedQR Scan
E506No usable camera available on this deviceQR Scan

Data models

data class DiscoveredService( val name: String, // mDNS instance name (placeholder "QR" for scan results) val host: String, // resolved host / IP, e.g. "192.168.1.100" val port: Int // service port, e.g. 8443 ) interface DiscoveryListener { fun onDiscovered(services: List<DiscoveredService>) fun onError(error: ConnectionError) }

Cross-Device with Auto Channel

New in v1.0.9. CrossDeviceStrategy.AUTO lets the SDK pick the channel for you and re-route to the other channel if the active one drops — a cable is unplugged, Wi-Fi is lost, or the terminal resets.

val config = ConnectionConfig.createCrossDeviceMode( CrossDeviceStrategy.AUTO, host = "192.168.1.100", // required: fallback target for the LAN channel port = 8443, autoPriority = listOf(CrossDeviceStrategy.LAN, CrossDeviceStrategy.CABLE) // default ) TaplinkSDK.setConnectionListener(connectionListener) TaplinkSDK.connect(config, connectionListener)

How the channel is chosen

  1. Candidates are tried in autoPriority order (default [LAN, CABLE]). Set the order explicitly with ConnectionConfig.setAutoPriority(...).
  2. Once connected, the channel does not change while the link is healthy.
  3. When the active link actually drops, the SDK rotates to the next candidate and reconnects — cable dropped → LAN, LAN dropped → cable.
  4. For the LAN channel the SDK rediscovers the same terminal by its serial over mDNS, so it reconnects to that terminal even if its IP address changed. If no discovered terminal matches, the configured host is used. It never connects an arbitrary terminal it happens to find on the network.

What Auto will never do

  • It will not tear down a healthy connection to move to a “better” channel.
  • It will not interrupt an in-flight transaction. If the link genuinely dies mid-transaction, you get the normal disconnect/failure path — apply the usual query()-then-decide rule before retrying.
  • It will not report a stale CONNECTED. During a re-route the status is CONNECTING; if no channel is reachable it settles on DISCONNECTED.

Always configure a LAN host (and port) on an AUTO config, even when a cable is your preferred channel. Without it the SDK has no verified LAN target to fall back to, and a cable-only session that has never connected over LAN will have nothing to re-route to.

Sub-Screen Mode

The Sub-Screen mode combines USB Virtual Serial Port (VSP) connection with automatic sub-screen activation in a single step. When you connect using this mode:

  1. The SDK establishes a VSP connection to the remote TaPro terminal
  2. Once connected, it automatically requests TaPro to open the USB customer-facing screen player
  3. onConnected is only delivered when BOTH the transport AND screen player are ready
  4. If the screen player fails to open, onError is delivered with code 351

Prerequisites

  • Remote device must have TaPro running with VSP/Cable service mode enabled
  • Devices must be connected via USB cable
  • Remote device must have a USB customer-facing display attached

Usage

// One-step connection: VSP + sub-screen activation val config = ConnectionConfig.createSubScreenMode() TaplinkSDK.connect(config, object : ConnectionListener { override fun onConnected(deviceId: String, taproVersion: String) { // Both VSP link AND sub-screen are ready // Full transaction API is available (sale, refund, void, etc.) } override fun onDisconnected(reason: String) { // Connection lost } override fun onError(error: ConnectionError) { when (error.code) { "351" -> { // VSP connected but sub-screen failed to open. // Check: Is a USB display connected to the remote device? // Check: Does TaPro support USB screen player (v1.0.5+)? } else -> { // VSP connection itself failed } } } })

Error Code 351 — Sub-Screen Open Failed

CodeMeaningSuggestion
351VSP connection succeeded but the USB screen player could not be opened on the remote TaPro deviceEnsure TaPro v1.0.5+ is running, and a USB customer-facing display is connected to the remote device

Choosing a mode

Your setupRecommended (v1.0.9)Equivalent classic API (still supported)
Business app and TaPro on one terminalcreateOnDeviceMode()createAppMode() / ConnectionMode.APP_TO_APP
Separate terminal on the same networkcreateCrossDeviceMode(CrossDeviceStrategy.LAN, host, port)createLanMode(host, port) / ConnectionMode.LAN
Separate terminal on a cablecreateCrossDeviceMode(CrossDeviceStrategy.CABLE)createCableMode() / ConnectionMode.CABLE
Separate terminal, either channel, survive a dropcreateCrossDeviceMode(CrossDeviceStrategy.AUTO, host, port)no equivalent
Customer-facing USB displaycreateSubScreenMode()unchanged

Upgrading from v1.0.8 requires no code changes. ConnectionMode.APP_TO_APP / LAN / CABLE, CableProtocol, createAppMode(), createLanMode(), createCableMode() and createDefault() all behave exactly as before. Adopt the new API when convenient — the two styles can coexist in one codebase.

Transaction Types

Sale Transaction

The most common payment transaction type. For card payments, specify paymentMethod as PaymentCategory.CARD.

val client = TaplinkSDK.getClient() val amount = AmountInfo() .setOrderAmount(BigDecimal("1000")) // 1000 cents = USD 10.00 .setPricingCurrency("USD") val request = SaleRequest.builder() .setReferenceOrderId("ORDER_${System.currentTimeMillis()}") .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setAmount(amount) .setPaymentMethod(PaymentMethodInfo(PaymentCategory.CARD)) .setDescription("Product Purchase") .build() client.sale(request, paymentCallback)

Signature configuration (Sale/Auth/non-referenced Refund). Use signatureConfig on a SaleRequest, an AuthRequest or a non-referenced RefundRequest to control signature handling per transaction. It drives two independent behaviors on Tapro: whether the on-screen e-signature page is shown, and whether a signature line is printed on the receipt (also reflected as printSignatureLine in receiptJson).

signatureConfig.useHostConfig selects one whole configuration source — the terminal configuration and the request configuration are never merged field by field:

FactoryuseHostConfigE-signature pageReceipt signature line
SignatureConfig.useHostConfig() (default)trueTerminal settings decideTerminal settings decide
SignatureConfig.onScreen()falseAlways shownPrinted only when a signature was captured
SignatureConfig.onReceipt()falseNever shownAlways printed for handwriting
SignatureConfig.onScreenAbove(threshold)falseShown only above thresholdPrinted only when a signature was captured
SignatureConfig.onReceiptAbove(threshold)falseNever shownPrinted only above threshold
SignatureConfig.none()falseNever shownNever printed

threshold is expressed in minor units and is compared against the final transaction amount. The comparison is strictly greater than — with threshold = 5000 ($50.00) a $50.00 transaction captures no signature, while $50.01 does. When useHostConfig is false, entryLocation is required — omitting it is rejected as an invalid parameter.

Compatibility with signatureEntryLocation. Providing an entryLocation is by itself enough to select the request configuration, even though useHostConfig defaults to true. This keeps SignatureConfig(entryLocation = ...) — and the legacy signatureEntryLocation field used by raw-JSON and Cloud integrations — behaving exactly as before instead of being silently ignored. When both signatureConfig and signatureEntryLocation are sent, signatureConfig wins.

val request = SaleRequest.builder() .setReferenceOrderId("ORDER_${System.currentTimeMillis()}") .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setAmount(AmountInfo().setOrderAmount(BigDecimal("1000")).setPricingCurrency("USD")) .setSignatureConfig(SignatureConfig.onReceipt()) // force receipt signature line .build() client.sale(request, paymentCallback) // Only require an on-screen signature above USD 50.00 val highValue = SaleRequest.builder() .setSignatureConfig(SignatureConfig.onScreenAbove(BigDecimal("5000"))) .build()

Non-referenced refunds. A non-referenced refund requires the cardholder to present a card, so it accepts the same signatureConfig:

val refund = RefundRequest.nonReferencedBuilder() .setTransactionRequestId(UUID.randomUUID().toString()) .setAmount(AmountInfo(orderAmount = BigDecimal("1000"), pricingCurrency = "USD")) .setReferenceOrderId("ORDER-000001") .setSignatureConfig(SignatureConfig.onReceipt()) .build()

A referenced refund (built with RefundRequest.referencedBuilder()) reuses the original transaction’s card data and never captures a signature — passing signatureConfig there throws IllegalArgumentException, and raw-JSON/Cloud requests are rejected with E302.

When signatureConfig is not set — or when SignatureConfig.useHostConfig() is used — Tapro falls back to the terminal’s signature settings (for approved, non-EBT transactions only):

  • Terminal capture method ON_SCREEN: show the e-signature page when always require signature is enabled, otherwise only when the CVM result is SIGNATURE.
  • Terminal capture method ON_RECEIPT: print the receipt signature line when always require signature is enabled, otherwise only when the CVM result is SIGNATURE.

EBT transactions never collect a signature regardless of this setting.

Refund Transaction

Supports full and partial refunds. For card refunds, specify paymentMethod as PaymentCategory.CARD.

Referenced Refund (with original transaction ID):

val amount = AmountInfo() .setOrderAmount(BigDecimal("500")) // 500 cents = USD 5.00 .setPricingCurrency("USD") val request = RefundRequest.referencedBuilder() .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setOriginalTransactionId("TXN20231119001") .setAmount(amount) .setPaymentMethod(PaymentMethodInfo(PaymentCategory.CARD)) .setDescription("Product Return") .build() client.refund(request, paymentCallback)

Non-Referenced Refund (requires card swipe):

val amount = AmountInfo() .setOrderAmount(BigDecimal("500")) // 500 cents = USD 5.00 .setPricingCurrency("USD") val request = RefundRequest.nonReferencedBuilder() .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setReferenceOrderId("REFUND_${System.currentTimeMillis()}") .setAmount(amount) .setPaymentMethod(PaymentMethodInfo(PaymentCategory.CARD)) .setDescription("Offline Refund") .build() client.refund(request, paymentCallback)

Void Transaction

Cancel a same-day transaction (faster than refund, no online authorization required).

val request = VoidRequest.builder() .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setOriginalTransactionId("TXN20231119001") .setDescription("Cancel Transaction") .build() client.void(request, paymentCallback)

Void is only available for same-day transactions. Use Refund for cross-day transactions.

Abort Transaction

Cancel an in-progress transaction before it reaches the payment gateway (for example, while the terminal is waiting for a card tap or PIN entry). Once the transaction has been authorized online, use Void (same-day) or Refund (cross-day) to reverse it instead.

Pass the transactionRequestId of the in-progress transaction as originalTransactionRequestId.

val request = AbortRequest.builder() .setOriginalTransactionRequestId("TXN_ORIGINAL_ID") // transactionRequestId of the in-progress transaction .setDescription("User cancelled") .build() client.abort(request, object : PaymentCallback { override fun onSuccess(result: PaymentResult) { // Abort accepted — clear the pending transaction state clearPendingTransaction() } override fun onFailure(error: PaymentError) { // Abort failed — the transaction may have already reached the gateway. // Use Void or Refund to reverse it if needed. } override fun onProgress(event: PaymentEvent) { } })

Abort only works during pre-authorization stages (card detection, PIN entry). It cannot cancel a transaction that has already been authorized online.

Authorization (Pre-Auth)

Freeze funds without actual deduction, commonly used for hotels and car rentals.

val amount = AuthAmountInfo() .setAuthAmount(BigDecimal("5000")) // 5000 cents = USD 50.00 .setPricingCurrency("USD") val request = AuthRequest.builder() .setReferenceOrderId("AUTH_${System.currentTimeMillis()}") .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setAmount(amount) .setDescription("Hotel Reservation") .build() client.auth(request, paymentCallback)

Post-Authorization

Complete authorization and perform actual deduction.

val amount = AmountInfo() .setOrderAmount(BigDecimal("4500")) // 4500 cents = USD 45.00 .setPricingCurrency("USD") val request = PostAuthRequest.builder() .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setOriginalTransactionId("TXN20231119002") .setAmount(amount) .setDescription("Complete Hotel Payment") .build() client.postAuth(request, paymentCallback)

Forced Authorization (Offline / Voice Auth)

Forced Authorization is used when the terminal cannot obtain an online authorization from the issuer (for example, network failure or damaged chip) but an authorization code is provided by the issuer (voice auth). To perform a forced authorization, obtain an auth code from the issuer and include it in a ForcedAuthRequest.

val request = ForcedAuthRequest.builder() .setReferenceOrderId("FORCED_ORDER_123") .setTransactionRequestId("TXN_FORCED_123") .setAmount(AmountInfo().setOrderAmount(BigDecimal("1000")).setPricingCurrency("USD")) .setAuthCode("AUTHCODE123") // required for forced auth .build() TaplinkSDK.getClient().forcedAuth(request, paymentCallback)

Query Transaction

Query transaction status, especially useful for timeout scenarios.

Background Processing: The query operation runs entirely in the background — Tapro does not come to the foreground or display any UI. Results are returned directly through the callback.

⚠️ Since Tapro does not display any loading screen during query, you must implement your own loading/waiting indicator in your application (e.g., a progress spinner or “Querying transaction status…” message) while waiting for the callback to return.

val query = QueryRequest() .setTransactionRequestId("TXN20231119001") client.query(query, object : PaymentCallback { override fun onSuccess(result: PaymentResult) { // Handle query result when { result.isSuccess() -> handleSuccess(result) result.isProcessing() -> continuePolling() result.isFailed() -> handleFailure(result) } } override fun onFailure(error: PaymentError) { // Handle query error } })

Terminal Info Query (GET_TERMINAL_INFO)

Available since v1.0.9. The connected TaPro device must also support GET_TERMINAL_INFO. If it does not, the SDK reports error code 352; upgrade TaPro before retrying.

Fetch the merchant and terminal information for the single, currently connected TaPro device. This is a read-only control action — it is not a cloud merchant lookup and not a terminal-list query: it never returns other devices, is not paginated, and the response has no terminals array or nextToken.

getTerminalInfo() does not create a transaction and does not require a transactionRequestId. It responds even while TaPro is processing a payment transaction, and its result is delivered through a dedicated TerminalInfoCallback — never PaymentCallback — so it can never be confused with payment-progress/success semantics.

TaplinkSDK.getTerminalInfo(object : TerminalInfoCallback { override fun onSuccess(info: TerminalInfo) { // Merchant fields val merchantId = info.merchantId val dbaName = info.dbaName val mids = info.midList // List<MerchantMid>: channelCode, channelName, mid // Terminal fields — the single device that answered this request val sn = info.terminal.sn val model = info.terminal.model val tids = info.terminal.tidList // List<TerminalTid>: channelCode, channelName, tid } override fun onFailure(error: PaymentError) { if (error.code == "352") { // TaPro does not support GET_TERMINAL_INFO — prompt the merchant to upgrade TaPro } // Otherwise a communication/signature-verification error } })

TerminalInfo fields:

FieldTypeDescription
merchantIdStringMerchant ID
dbaNameStringDoing-business-as name
mccString?Merchant category code
country / stateName / cityName / street / detailAddress / zipCodeString?Merchant address fields
midListList<MerchantMid>Merchant IDs by payment channel; empty list if none configured
terminalTerminalThe single TaPro device that answered this request

Terminal fields: sn, vendor (e.g. "SUNMI"), model (e.g. "P3", "P3K"), tidList: List<TerminalTid>.

getTerminalInfo() never returns secretKey, appId, network addresses, card data, or transaction details. Merchant status and createTime (merchant and terminal level) are intentionally not part of the response — TaPro has no real, backend-provisioned source for them today, so they are omitted rather than fabricated. Other optional fields that TaPro cannot resolve from real, backend-provisioned data are delivered as null (or an empty list for midList/tidList).

Batch Close

End-of-day settlement to close the current batch.

val request = BatchCloseRequest.builder() .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setDescription("Batch Close") .setPrintReceipt(PrintReceipt.BOTH) .build() client.batchClose(request, object : PaymentCallback { override fun onSuccess(result: PaymentResult) { if (result.isSuccess()) { val batchInfo = result.batchCloseInfo // Display batch summary showBatchSummary( batchNo = result.batchNo, totalCount = batchInfo?.totalCount ?: 0, totalAmount = batchInfo?.totalAmount ?: BigDecimal.ZERO ) } else { showError(result.message) } } override fun onFailure(error: PaymentError) { // Handle error } })

printReceipt is optional and defaults to AUTO.

ValueBatch Close behavior
AUTOUse the current Tapro batch report setting
BOTHPrint both total and transaction detail reports
TOTALPrint the total report only
DETAILPrint the transaction detail report only
NONEDo not print a batch report

MERCHANT and CUSTOMER describe transaction-receipt copies and carry no meaning for a batch report. They are not rejected — the SDK normalizes them to AUTO, so the terminal falls back to its own batch report setting.

Error Handling

The SDK separates transaction outcomes (always in onSuccess) from communication errors (always in onFailure).

Transaction Outcomes via onSuccess

All requests that Tapro receives and processes return through onSuccess, regardless of whether the transaction was approved, declined, or cancelled:

override fun onSuccess(result: PaymentResult) { when { result.isSuccess() -> handleApproved(result) // Approved — fulfill the order result.isFailed() -> { // Transaction failed — use code and message for detailed error analysis // result.code → SDK standard error code (e.g. "307", "310") // result.message → Detailed error description (e.g. "K004: Insufficient funds (051)") Log.e(TAG, "Transaction failed: code=${result.code}, message=${result.message}") showErrorDialog(result.message) } result.isProcessing() -> pollForFinalStatus(result) // Still processing — query later } }

Tip: When result.isFailed(), use result.code and result.message for detailed error analysis. The message field contains the Tapro internal exception code, error description, and original gateway response code (when applicable).

If you want to reuse an existing PaymentError-based UI for declined transactions, call result.toPaymentError() inside onSuccess.

Communication Errors via onFailure

onFailure fires only when the SDK cannot deliver the request or receive a response — connection lost, timeout, invalid configuration, etc.

override fun onFailure(error: PaymentError) { // Communication/technical error — no transaction result was received. // This is NOT a card decline — declines arrive via onSuccess with isFailed(). val code = error.code val message = error.message val suggestion = error.suggestion val canRetry = error.canRetryWithSameId when (error.detail.category) { ErrorCategory.INITIALIZATION -> { showDialog("Initialization Error", message, suggestion) } ErrorCategory.CONNECTION -> { showDialog("Connection Error", message, suggestion) } ErrorCategory.AUTHENTICATION -> { showDialog("Authentication Failed", message, suggestion) } ErrorCategory.TRANSACTION -> { // Request delivery error (timeout, etc.) if (canRetry) { retryWithSameRequest() } else { createNewTransaction() } } } }

Migrating from SDK v1.0.6

In v1.0.6 and earlier, declined transactions were delivered via onFailure(PaymentError). From v1.0.7 onwards, ALL terminal-confirmed outcomes (approved, declined, processing) arrive via onSuccess(PaymentResult).

Before (v1.0.6 and earlier)

override fun onSuccess(result: PaymentResult) { showApproved(result) // only called for approvals } override fun onFailure(error: PaymentError) { showError(error.message) // called for both declines AND comm errors }

After (v1.0.7)

// Check result in onSuccess override fun onSuccess(result: PaymentResult) { if (result.isSuccess()) showApproved(result) else if (result.isFailed()) showError(result.toPaymentError()) // reuse legacy error UI } override fun onFailure(error: PaymentError) { showError(error.message) }

Common Error Codes

The SDK uses a segmented error code design for quick problem identification.

Note: Error code 100 indicates success, not an error. Error codes 20x-39x are actual errors.

Error Code Ranges

Code RangeError TypeDescription
100SuccessOperation successful (not an error)
20xInitializationSDK initialization issues
21xConnection StateConnection state management and failures
23xApp-to-App ModeSame-device connection issues
24xLAN ModeNetwork connection issues
25xCable ModeUSB/Serial cable connection issues
30xTransactionTransaction processing errors
35xControl ActionRead-only control actions (e.g. openUsbScreenPlayer, getTerminalInfo) unsupported by the connected TaPro version

Quick Reference

Initialization Issues:

CodeIssueSolution
201SDK not initializedCall TaplinkSDK.init()
202SDK service errorRestart application
203Tapro initialization failedReconnect

Connection Issues:

CodeIssueSolution
211-213Connection state errorCheck connection state, call connect()
214, 221Connection failedCheck network/device/credentials
231-232App-to-App mode failedInstall Tapro app or restart device
241-242LAN mode failedCheck network and IP address
251-255Cable mode failedCheck cable connection and USB permissions

Transaction Issues:

CodeIssueSolutionRetry Rule
301-305Parameter/Send errorCheck parameters and network✅ Same ID OK
306Response timeoutQuery status first⚠️ Query then decide
307-311Transaction failedReview details, retry with new ID❌ Must use new ID
351Sub-Screen open failedEnsure a USB display is attached and TaPro is v1.0.5+N/A (connection-mode error)
352getTerminalInfo() unsupported by connected TaProUpgrade TaPro to a version that supports GET_TERMINAL_INFON/A (not retryable until upgraded)

Retry Rules:

  • Same ID OK: Safe to retry with the same transactionRequestId
  • ⚠️ Query then decide: Query transaction status before retrying
  • Must use new ID: Must use a new transactionRequestId to prevent duplicate charges

Important Concepts

Amount Units

All amount fields must use the smallest currency unit:

  • USD: Cents (1 Dollar = 100 Cents)
  • EUR: Cents (1 Euro = 100 Cents)
  • JPY: Yen (1 Yen = 1 Yen)
  • CNY: Fen (1 Yuan = 100 Fen)

Example:

// Correct: $12.34 = 1234 cents val amount = AmountInfo() .setOrderAmount(BigDecimal("1234")) // 1234 cents = $12.34 .setPricingCurrency("USD") // Wrong: Using base currency unit val wrongAmount = AmountInfo() .setOrderAmount(BigDecimal("12.34")) // Wrong! This will be interpreted as $0.1234 .setPricingCurrency("USD")

Tip & Surcharge Handling

  • When the tip amount is known upfront, keep orderAmount as the pre-tip subtotal and set amount.tipAmount separately (minor units). Tapro will use the breakdown to calculate the final transaction total and returns that total in the transaction result. If the tip is folded into orderAmount, tax may also be calculated on the tip portion.

  • To use on-screen tip, configure it via tipConfig. tipConfig.useHostConfig selects where the configuration comes from — the two sources are mutually exclusive and are never merged field by field:

    • useHostConfig = true — the terminal (SUNBAY platform) tip configuration is used and every other field in tipConfig is ignored, including onScreenTip. Setting onScreenTip = false alongside useHostConfig = true does not skip tipping — the terminal configuration still decides.
    • useHostConfig = false (default) — the request configuration is used as-is. Set tipConfig.onScreenTip to true to enable the on-screen prompt, and provide tipMode, tipWithTax and suggestions explicitly. Any field you leave out is treated as “not specified” and never falls back to the platform value — this applies to tipMode and tipWithTax just as much as to suggestions.

    onScreenTip selects where the tip is collected, and suggestions decides whether preset amounts are offered. The table below applies when useHostConfig = false:

    onScreenTipsuggestionsScreenReceipt tip area
    trueprovidedTip screen showing the given suggestionsNone
    truenullTip screen with custom entry onlyNone
    falseprovidedNo tip screenSuggested amounts, so the cardholder can write the tip by hand
    falsenullNo tip screenBlank tip line, so the cardholder can write the tip by hand

    The tip is only ever captured in one place. With onScreenTip = true the amount is settled on the tip screen, so the receipt carries no tip area at all. With onScreenTip = false the receipt takes over: it prints your suggestions when provided, or a blank tip line when not. Whenever the request configuration applies, it replaces the terminal’s receipt tip settings as a whole — the terminal’s own blank tip line is never added on top. tipWithTax still controls whether the suggested amounts are calculated on the taxed amount. The resulting tip area is also included in the receiptJson returned with the transaction result, so it is present whether the terminal prints the receipt or your app does. None of this applies when useHostConfig = true — the terminal then decides receipt printing through its own settings.

    In the typed Android SDK, tipMode and tipWithTax are non-null and carry defaults (ON_SALE and false), so a typed request always sends a value for them. Only raw-JSON and Cloud requests can leave them out.

    tipMode controls when the tip prompt appears: ON_SALE shows it before the card is read, so the tip travels with the sale; AFTER_SALE authorizes the sale first, then prompts for the tip and sends a follow-up tip adjustment. In both cases your callback fires once, after the tip is settled, carrying the final amount — you never observe an intermediate approved result that later changes.

    amount.tipAmount and tipConfig are mutually exclusive. Passing both fails validation with TipConfigConflict (E302 for raw JSON / Cloud requests) — pass tipAmount when your app already knows the tip, or tipConfig when the terminal should collect it.

  • If amount.surchargeAmount is provided and the customer pays with a Debit Card, Tapro will remove the surcharge amount before completing the transaction.

Signature Handling

  • Signature capture is configured per transaction via signatureConfig. See Signature configuration above for the full behaviour table. signatureConfig.useHostConfig selects one whole configuration source and the two are never merged:

    • SignatureConfig.useHostConfig() (default, and also the behaviour when signatureConfig is omitted) — the terminal signature configuration is used.
    • SignatureConfig.onScreen() / onReceipt() — capture the signature on screen or print a signature line on the receipt for every transaction.
    • SignatureConfig.onScreenAbove(threshold) / onReceiptAbove(threshold) — only require a signature when the final amount exceeds threshold (minor units).
    • SignatureConfig.none() — never require a signature.
  • When useHostConfig is false, entryLocation is required; omitting it is rejected as an invalid parameter.

Order ID vs Transaction Request ID

  • referenceOrderId: Merchant order number (one order can contain multiple transactions)
  • transactionRequestId: Transaction request ID (unique for each transaction)

Example:

val orderId = "ORDER001" // Sale transaction val saleRequest = SaleRequest.builder() .setReferenceOrderId(orderId) // Same .setTransactionRequestId("TXN001_SALE") // Different .setAmount(amount) .setPaymentMethod(PaymentMethodInfo(PaymentCategory.CARD)) .build() // Refund transaction (same order) val refundRequest = RefundRequest.referencedBuilder() .setTransactionRequestId("TXN001_REFUND") // Different .setOriginalTransactionId(originalTxnId) // Reference original transaction .setAmount(refundAmount) .setPaymentMethod(PaymentMethodInfo(PaymentCategory.CARD)) .build()

Best Practices

Connection State Monitoring

Monitor device connection status to ensure payment functionality is available.

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) TaplinkSDK.setConnectionListener(object : ConnectionListener { override fun onConnected(deviceId: String, taproVersion: String) { runOnUiThread { updateConnectionStatus("Connected to Tapro $taproVersion") enablePaymentButtons(true) } } override fun onDisconnected(reason: String) { runOnUiThread { updateConnectionStatus("Disconnected: $reason") enablePaymentButtons(false) } } override fun onError(error: ConnectionError) { runOnUiThread { updateConnectionStatus("Connection error: ${error.message}") } } }) } override fun onDestroy() { super.onDestroy() TaplinkSDK.removeConnectionListener() } }

Timeout Handling

Implement polling query mechanism for timeout scenarios.

private fun handleTimeout(transactionRequestId: String) { queryTransactionWithPolling(transactionRequestId) { result -> if (result.transactionStatus == "SUCCESS") { handleSuccess(result) } else { showRetryDialog() } } } private fun queryTransactionWithPolling( transactionRequestId: String, attempt: Int = 1, callback: (PaymentResponse) -> Unit ) { if (attempt > 12) { // Exceeded 12 attempts (60 seconds) showDialog("Transaction status unknown", "Please contact support. Transaction Request ID: $transactionRequestId") return } val query = QueryRequest().setTransactionRequestId(transactionRequestId) val client = TaplinkSDK.getClient() client.query(query, object : PaymentCallback { override fun onSuccess(result: PaymentResponse) { if (result.transactionStatus == "PROCESSING") { // Continue polling after 5 seconds Handler(Looper.getMainLooper()).postDelayed({ queryTransactionWithPolling(transactionRequestId, attempt + 1, callback) }, 5000) } else { callback(result) } } override fun onFailure(error: PaymentError) { showErrorMessage("Query failed: ${error.message}") } }) }

Progress Event Handling

Provide friendly user feedback to enhance user experience.

override fun onProgress(event: PaymentEvent) { runOnUiThread { when (event.status) { "PROCESSING" -> showProcessingAnimation("Processing...") "WAITING_CARD" -> showCardPrompt("Please insert, swipe, or tap card") "CARD_DETECTED" -> showCardPrompt("Card detected") "READING_CARD" -> showProcessingAnimation("Reading card information") "WAITING_PIN" -> showPinPrompt("Please enter PIN on payment terminal") "WAITING_SIGNATURE" -> showSignaturePrompt("Please sign on payment terminal") "WAITING_RESPONSE" -> showProcessingAnimation("Waiting for payment gateway response...") "PRINTING" -> showProcessingAnimation("Printing receipt...") "COMPLETED" -> hideAllPrompts() "CANCEL" -> showCancelMessage("Transaction cancelled") } } }

API Reference

TaplinkSDK

Main SDK class providing core functionality.

// Initialize SDK TaplinkSDK.init(context: Context, config: TaplinkConfig) // Connection management TaplinkSDK.connect(config: ConnectionConfig?, listener: ConnectionListener) TaplinkSDK.disconnect() TaplinkSDK.isConnected(): Boolean // LAN address acquisition (return host/port, do NOT connect) — since v1.0.8 TaplinkSDK.discoverLanServices(listener: DiscoveryListener) // mDNS discover-only TaplinkSDK.scanLanQrCode(listener: DiscoveryListener) // QR scan-only // LAN discover/scan AND connect in one step — since v1.0.8 TaplinkSDK.autoDiscoverAndConnect(listener: ConnectionListener) TaplinkSDK.scanAndConnect(listener: ConnectionListener) // Device information TaplinkSDK.getConnectedDeviceId(): String? TaplinkSDK.getConnectionMode(): String? TaplinkSDK.getTaproVersion(): String? // Get transaction client TaplinkSDK.getClient(): TaplinkClient // Terminal info query (read-only control action) — since v1.0.9 TaplinkSDK.getTerminalInfo(callback: TerminalInfoCallback) // SDK version TaplinkSDK.getVersion(): String

TaplinkClient

Transaction client class for executing payment operations.

val client = TaplinkSDK.getClient() // Transaction methods client.sale(request: SaleRequest, callback: PaymentCallback) client.refund(request: RefundRequest, callback: PaymentCallback) client.void(request: VoidRequest, callback: PaymentCallback) client.abort(request: AbortRequest, callback: PaymentCallback) client.auth(request: AuthRequest, callback: PaymentCallback) client.postAuth(request: PostAuthRequest, callback: PaymentCallback) client.incrementalAuth(request: IncrementalAuthRequest, callback: PaymentCallback) client.tipAdjust(request: TipAdjustRequest, callback: PaymentCallback) client.batchClose(request: BatchCloseRequest, callback: PaymentCallback) // Query method client.query(request: QueryRequest, callback: PaymentCallback) // Terminal info query — since v1.0.9 client.getTerminalInfo(callback: TerminalInfoCallback)

System Requirements

  • Android Version: Android 7.1 (API level 21) or higher
  • Language: Kotlin 1.8+ or Java 8+
  • Build Tool: Gradle 7.0+

Technical Stack

  • Language: Kotlin 1.7.10
  • Build Tool: Gradle with Kotlin DSL
  • Android Gradle Plugin: 8.13.1
  • Min SDK: Android 7.1 (API 25)
  • Target SDK: Android API 35
  • Java Version: Java 11

Note: This SDK is for local mode integration only. For server-side cloud mode integration, please use Nexus SDK.

Version History

v1.0.9 (Current)

  • New — Integration modes: ConnectionMode.ON_DEVICE (business app and TaPro on the same terminal) and ConnectionMode.CROSS_DEVICE (TaPro as a separate PIN Pad). Factories: ConnectionConfig.createOnDeviceMode() and createCrossDeviceMode(strategy, host, port, cableProtocol, autoPriority).
  • New — CrossDeviceStrategy { AUTO, LAN, CABLE }: selects the channel used by CROSS_DEVICE; set via ConnectionConfig.setCrossDeviceStrategy(...). CROSS_DEVICE defaults to AUTO. There is deliberately no CLOUD value.
  • New — Automatic channel failover (CrossDeviceStrategy.AUTO): the SDK re-routes between LAN and cable only after the active link actually drops, reconnecting the same terminal (matched by serial over mDNS, with the configured host as fallback). It never tears down a healthy link, never interrupts an in-flight transaction, and never reports a stale CONNECTED. Try order is configurable via setAutoPriority(...); default [LAN, CABLE].
  • New — SUB_SCREEN connection mode: ConnectionConfig.createSubScreenMode() combines the USB VSP link with mandatory customer-display activation — onConnected fires only when both are ready, otherwise onError with code 351.
  • New — Terminal info query: TaplinkSDK.getTerminalInfo(TerminalInfoCallback) / TaplinkClient.getTerminalInfo(TerminalInfoCallback) — read-only control action returning the merchant and terminal information of the currently connected TaPro (TerminalInfo, MerchantMid, Terminal, TerminalTid). Not a cloud merchant lookup and not a terminal-list query; answers even while TaPro is processing a transaction.
  • New — Error codes: 351 (Sub-Screen open failed) and 352 (connected TaPro does not support GET_TERMINAL_INFO — upgrade TaPro).
  • Improved — Faster failure on a dead cable link: a lightweight liveness probe runs before each transaction/query, so an unresponsive terminal returns 213 in about 3 seconds instead of hanging until the full transaction timeout. Treat 213 like any other connection error: reconnect and retry.
  • Improved — Cable plug-and-play: CableProtocol.AUTO now tries the physically inserted cable’s protocol first, waits for a freshly plugged VSP device to enumerate, and no longer misclassifies a VSP device as RS232.
  • Improved — Switching configurations: requesting a different configuration while a previous attempt is still CONNECTING now cancels the stuck attempt instead of queueing behind it.
  • Fixed — LAN connection no longer drops every few seconds when the terminal re-advertises its mDNS service, and no longer hangs forever after the terminal restarts (WebSocket keepalive, peer-restart detection and correct close-code propagation).
  • Fixed — mDNS discovery no longer floods with resolve failures on networks with several terminals.
  • Fixed — CrossDeviceStrategy.LAN never attempts a USB connection, even if the config also carries a cable protocol (previously surfaced as a USB permission prompt and error 212).
  • Fixed — openUsbScreenPlayer() no longer fails with T06, and cable / USB_VSP reconnect no longer times out when the cable was never unplugged.
  • No breaking changes. APP_TO_APP / LAN / CABLE, CableProtocol, createAppMode(), createLanMode(), createCableMode() and createDefault() all behave exactly as in v1.0.8.

v1.0.8

  • New — LAN service discovery: TaplinkSDK.discoverLanServices(DiscoveryListener) — mDNS discovery returns host/port without connecting.
  • New — LAN QR scan: TaplinkSDK.scanLanQrCode(DiscoveryListener) — built-in camera scanner reads lan://host/port QR code.
  • New — One-call variants: autoDiscoverAndConnect(ConnectionListener) and scanAndConnect(ConnectionListener) discover/scan and connect in a single step.
  • New — Signature configuration: signatureConfig on SaleRequest/AuthRequest/non-referenced RefundRequest — choose the terminal configuration (SignatureConfig.useHostConfig(), the default) or a per-transaction one via SignatureConfig.onScreen(), onReceipt(), onScreenAbove(threshold), onReceiptAbove(threshold), none(). The legacy signatureEntryLocation field remains supported for raw-JSON and Cloud integrations.
  • New — Tip configuration source: TipConfig.useHostConfig — when true, the terminal tip configuration is used and every other TipConfig field is ignored, onScreenTip included. Defaults to false. When false, any field left out is treated as “not specified” and no longer falls back to the terminal value — this now covers tipMode and tipWithTax as well as suggestions.
  • New — Request-driven receipt tip area: the request tip configuration replaces the terminal’s receipt tip settings as a whole, so the tip is captured in exactly one place. With onScreenTip = true the receipt carries no tip area at all. With onScreenTip = false the receipt takes over: it prints the suggested amounts when suggestions is provided, or a blank tip line when it is not — the tip flow is no longer skipped in either case.
  • Improved — Batch Close printReceipt: added the batch-report values TOTAL and DETAIL. The transaction-receipt values MERCHANT and CUSTOMER are no longer rejected — they are normalized to AUTO, so the terminal falls back to its own batch report configuration.
  • New — ConnectionConfig factories: createAppMode(), createCableMode(), createLanMode(), createDefault().
  • New — More PaymentEvent progress states: onProgress(event) can now report additional stages of the transaction, and unrecognized event codes are delivered as a generic fallback event rather than being dropped. Treat PaymentEvent as an open set — match the states you care about and always provide an else branch.
  • Breaking — PaymentEvent.WaitingOnlineResponse removed: use PaymentEvent.OnlineProcessing, which reports the same stage.
  • Breaking — Gson replaced by Jackson: the POM now declares jackson-databind / jackson-module-kotlin 2.17.2 instead of Gson. Both are internal to the SDK, so integrations using the typed models need no change. BasicRequest.bizData / BasicResponse.bizData changed from JsonObject to raw String.
  • New — AppToAppMode: ConnectionConfig.setAppToAppMode(mode) and createAppMode(mode). Java callers of the old no-argument createAppMode() must now pass AppToAppMode.CUSTOM.
  • New — TipSuggestions.names: optional display labels matched positionally to values; ignored when missing or of a different size.
  • New — PaymentResult fields: relatedTransactionStatus and transactionBatchStatus.
  • Added DiscoveredService(name, host, port) model and DiscoveryListener callback.
  • Added discovery/scan error codes E501E506.
  • QR scan requires host app to add CameraX (1.3.4) + ZXing (3.5.3) dependencies; mDNS needs no extra dependency.

v1.0.7

  • Breaking: Declined transactions delivered via onSuccess(result) with result.isFailed() == true. Removed onDeclined callback.
  • Added PaymentResult.toPaymentError() migration bridge.
  • Published POM now declares transitive runtime dependencies (Gson, Java-WebSocket, usb-serial-for-android).
  • Added TaplinkConfig.create(appId, secretKey) for merchant-less configuration.
  • Terminal cancellation/abort normalized as failed transaction results.

v1.0.6

  • tipConfig moved from AmountInfo to the transaction request (SaleRequest/PostAuthRequest).

v1.0.5

  • Added PaymentCallback.onDeclined(PaymentResult) for clean decline separation.
  • Added PaymentCallbackAdapter and ConnectionListenerAdapter.
  • Added TaplinkSDK.isInitialized() and TaplinkSDK.getConnectionStatus().

v1.0.4

  • Added TipConfig with on-screen tip collection and suggested tip amounts.

v1.0.3

  • Introduced type-safe TaplinkClient with dedicated request models.
  • Moved connection settings to ConnectionConfig.
  • Removed TaplinkException; errors handled through PaymentCallback.onFailure(PaymentError).

v1.0.2

  • Internal cable/transport stability improvements. No API changes.

v1.0.1

  • Consolidated USB AOA, USB VSP, and RS232 into ConnectionMode.CABLE with CableProtocol.
  • Added connection persistence and reconnection.

v1.0.0

  • Initial public release with App-to-App, LAN, and Cable connection modes.
Last updated on