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
| Mode | Hardware layout | Channel |
|---|---|---|
ON_DEVICE | Business app and TaPro run on the same terminal | Local (App-to-App) |
CROSS_DEVICE | TaPro is a separate terminal acting as a PIN Pad | CrossDeviceStrategy.LAN / CABLE / AUTO |
SUB_SCREEN | Separate terminal used as a customer-facing display | USB 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 inTaplinkSDK.connect(config, listener). CrossDeviceStrategy.LANnever falls back to a cable, even if a cable is physically attached. UseCrossDeviceStrategy.AUTOif 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
| Code | Meaning | Applies to |
|---|---|---|
E501 | No services found / discovery failed | Discovery |
E502 | All discovered services failed to connect | autoDiscoverAndConnect |
E503 | Another discovery/scan operation is already in progress | Discovery & Scan |
E504 | Scan cancelled by the user | QR Scan |
E505 | Camera permission denied | QR Scan |
E506 | No usable camera available on this device | QR 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
- Candidates are tried in
autoPriorityorder (default[LAN, CABLE]). Set the order explicitly withConnectionConfig.setAutoPriority(...). - Once connected, the channel does not change while the link is healthy.
- When the active link actually drops, the SDK rotates to the next candidate and reconnects — cable dropped → LAN, LAN dropped → cable.
- 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
hostis 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 isCONNECTING; if no channel is reachable it settles onDISCONNECTED.
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:
- The SDK establishes a VSP connection to the remote TaPro terminal
- Once connected, it automatically requests TaPro to open the USB customer-facing screen player
onConnectedis only delivered when BOTH the transport AND screen player are ready- If the screen player fails to open,
onErroris delivered with code351
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
| Code | Meaning | Suggestion |
|---|---|---|
| 351 | VSP connection succeeded but the USB screen player could not be opened on the remote TaPro device | Ensure TaPro v1.0.5+ is running, and a USB customer-facing display is connected to the remote device |
Choosing a mode
| Your setup | Recommended (v1.0.9) | Equivalent classic API (still supported) |
|---|---|---|
| Business app and TaPro on one terminal | createOnDeviceMode() | createAppMode() / ConnectionMode.APP_TO_APP |
| Separate terminal on the same network | createCrossDeviceMode(CrossDeviceStrategy.LAN, host, port) | createLanMode(host, port) / ConnectionMode.LAN |
| Separate terminal on a cable | createCrossDeviceMode(CrossDeviceStrategy.CABLE) | createCableMode() / ConnectionMode.CABLE |
| Separate terminal, either channel, survive a drop | createCrossDeviceMode(CrossDeviceStrategy.AUTO, host, port) | no equivalent |
| Customer-facing USB display | createSubScreenMode() | 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:
| Factory | useHostConfig | E-signature page | Receipt signature line |
|---|---|---|---|
SignatureConfig.useHostConfig() (default) | true | Terminal settings decide | Terminal settings decide |
SignatureConfig.onScreen() | false | Always shown | Printed only when a signature was captured |
SignatureConfig.onReceipt() | false | Never shown | Always printed for handwriting |
SignatureConfig.onScreenAbove(threshold) | false | Shown only above threshold | Printed only when a signature was captured |
SignatureConfig.onReceiptAbove(threshold) | false | Never shown | Printed only above threshold |
SignatureConfig.none() | false | Never shown | Never 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 isSIGNATURE. - Terminal capture method
ON_RECEIPT: print the receipt signature line when always require signature is enabled, otherwise only when the CVM result isSIGNATURE.
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:
| Field | Type | Description |
|---|---|---|
merchantId | String | Merchant ID |
dbaName | String | Doing-business-as name |
mcc | String? | Merchant category code |
country / stateName / cityName / street / detailAddress / zipCode | String? | Merchant address fields |
midList | List<MerchantMid> | Merchant IDs by payment channel; empty list if none configured |
terminal | Terminal | The 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.
| Value | Batch Close behavior |
|---|---|
AUTO | Use the current Tapro batch report setting |
BOTH | Print both total and transaction detail reports |
TOTAL | Print the total report only |
DETAIL | Print the transaction detail report only |
NONE | Do 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(), useresult.codeandresult.messagefor detailed error analysis. Themessagefield 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 Range | Error Type | Description |
|---|---|---|
| 100 | Success | Operation successful (not an error) |
| 20x | Initialization | SDK initialization issues |
| 21x | Connection State | Connection state management and failures |
| 23x | App-to-App Mode | Same-device connection issues |
| 24x | LAN Mode | Network connection issues |
| 25x | Cable Mode | USB/Serial cable connection issues |
| 30x | Transaction | Transaction processing errors |
| 35x | Control Action | Read-only control actions (e.g. openUsbScreenPlayer, getTerminalInfo) unsupported by the connected TaPro version |
Quick Reference
Initialization Issues:
| Code | Issue | Solution |
|---|---|---|
| 201 | SDK not initialized | Call TaplinkSDK.init() |
| 202 | SDK service error | Restart application |
| 203 | Tapro initialization failed | Reconnect |
Connection Issues:
| Code | Issue | Solution |
|---|---|---|
| 211-213 | Connection state error | Check connection state, call connect() |
| 214, 221 | Connection failed | Check network/device/credentials |
| 231-232 | App-to-App mode failed | Install Tapro app or restart device |
| 241-242 | LAN mode failed | Check network and IP address |
| 251-255 | Cable mode failed | Check cable connection and USB permissions |
Transaction Issues:
| Code | Issue | Solution | Retry Rule |
|---|---|---|---|
| 301-305 | Parameter/Send error | Check parameters and network | ✅ Same ID OK |
| 306 | Response timeout | Query status first | ⚠️ Query then decide |
| 307-311 | Transaction failed | Review details, retry with new ID | ❌ Must use new ID |
| 351 | Sub-Screen open failed | Ensure a USB display is attached and TaPro is v1.0.5+ | N/A (connection-mode error) |
| 352 | getTerminalInfo() unsupported by connected TaPro | Upgrade TaPro to a version that supports GET_TERMINAL_INFO | N/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
transactionRequestIdto 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
orderAmountas the pre-tip subtotal and setamount.tipAmountseparately (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 intoorderAmount, tax may also be calculated on the tip portion. -
To use on-screen tip, configure it via
tipConfig.tipConfig.useHostConfigselects 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 intipConfigis ignored, includingonScreenTip. SettingonScreenTip = falsealongsideuseHostConfig = truedoes not skip tipping — the terminal configuration still decides.useHostConfig = false(default) — the request configuration is used as-is. SettipConfig.onScreenTiptotrueto enable the on-screen prompt, and providetipMode,tipWithTaxandsuggestionsexplicitly. Any field you leave out is treated as “not specified” and never falls back to the platform value — this applies totipModeandtipWithTaxjust as much as tosuggestions.
onScreenTipselects where the tip is collected, andsuggestionsdecides whether preset amounts are offered. The table below applies whenuseHostConfig = false:onScreenTipsuggestionsScreen Receipt tip area trueprovided Tip screen showing the given suggestions None truenullTip screen with custom entry only None falseprovided No tip screen Suggested amounts, so the cardholder can write the tip by hand falsenullNo tip screen Blank tip line, so the cardholder can write the tip by hand The tip is only ever captured in one place. With
onScreenTip = truethe amount is settled on the tip screen, so the receipt carries no tip area at all. WithonScreenTip = falsethe receipt takes over: it prints yoursuggestionswhen 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.tipWithTaxstill controls whether the suggested amounts are calculated on the taxed amount. The resulting tip area is also included in thereceiptJsonreturned with the transaction result, so it is present whether the terminal prints the receipt or your app does. None of this applies whenuseHostConfig = true— the terminal then decides receipt printing through its own settings.In the typed Android SDK,
tipModeandtipWithTaxare non-null and carry defaults (ON_SALEandfalse), so a typed request always sends a value for them. Only raw-JSON and Cloud requests can leave them out.tipModecontrols when the tip prompt appears:ON_SALEshows it before the card is read, so the tip travels with the sale;AFTER_SALEauthorizes 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.tipAmountandtipConfigare mutually exclusive. Passing both fails validation withTipConfigConflict(E302for raw JSON / Cloud requests) — passtipAmountwhen your app already knows the tip, ortipConfigwhen the terminal should collect it. -
If
amount.surchargeAmountis 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.useHostConfigselects one whole configuration source and the two are never merged:SignatureConfig.useHostConfig()(default, and also the behaviour whensignatureConfigis 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 exceedsthreshold(minor units).SignatureConfig.none()— never require a signature.
-
When
useHostConfigisfalse,entryLocationis 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(): StringTaplinkClient
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) andConnectionMode.CROSS_DEVICE(TaPro as a separate PIN Pad). Factories:ConnectionConfig.createOnDeviceMode()andcreateCrossDeviceMode(strategy, host, port, cableProtocol, autoPriority). - New —
CrossDeviceStrategy { AUTO, LAN, CABLE }: selects the channel used byCROSS_DEVICE; set viaConnectionConfig.setCrossDeviceStrategy(...).CROSS_DEVICEdefaults toAUTO. There is deliberately noCLOUDvalue. - 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 configuredhostas fallback). It never tears down a healthy link, never interrupts an in-flight transaction, and never reports a staleCONNECTED. Try order is configurable viasetAutoPriority(...); default[LAN, CABLE]. - New —
SUB_SCREENconnection mode:ConnectionConfig.createSubScreenMode()combines the USB VSP link with mandatory customer-display activation —onConnectedfires only when both are ready, otherwiseonErrorwith code351. - 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) and352(connected TaPro does not supportGET_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
213in about 3 seconds instead of hanging until the full transaction timeout. Treat213like any other connection error: reconnect and retry. - Improved — Cable plug-and-play:
CableProtocol.AUTOnow 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
CONNECTINGnow 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.LANnever attempts a USB connection, even if the config also carries a cable protocol (previously surfaced as a USB permission prompt and error212). - Fixed —
openUsbScreenPlayer()no longer fails withT06, and cable /USB_VSPreconnect no longer times out when the cable was never unplugged. - No breaking changes.
APP_TO_APP/LAN/CABLE,CableProtocol,createAppMode(),createLanMode(),createCableMode()andcreateDefault()all behave exactly as in v1.0.8.
v1.0.8
- New — LAN service discovery:
TaplinkSDK.discoverLanServices(DiscoveryListener)— mDNS discovery returnshost/portwithout connecting. - New — LAN QR scan:
TaplinkSDK.scanLanQrCode(DiscoveryListener)— built-in camera scanner readslan://host/portQR code. - New — One-call variants:
autoDiscoverAndConnect(ConnectionListener)andscanAndConnect(ConnectionListener)discover/scan and connect in a single step. - New — Signature configuration:
signatureConfigonSaleRequest/AuthRequest/non-referencedRefundRequest— choose the terminal configuration (SignatureConfig.useHostConfig(), the default) or a per-transaction one viaSignatureConfig.onScreen(),onReceipt(),onScreenAbove(threshold),onReceiptAbove(threshold),none(). The legacysignatureEntryLocationfield remains supported for raw-JSON and Cloud integrations. - New — Tip configuration source:
TipConfig.useHostConfig— whentrue, the terminal tip configuration is used and every otherTipConfigfield is ignored,onScreenTipincluded. Defaults tofalse. Whenfalse, any field left out is treated as “not specified” and no longer falls back to the terminal value — this now coverstipModeandtipWithTaxas well assuggestions. - 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 = truethe receipt carries no tip area at all. WithonScreenTip = falsethe receipt takes over: it prints the suggested amounts whensuggestionsis 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 valuesTOTALandDETAIL. The transaction-receipt valuesMERCHANTandCUSTOMERare no longer rejected — they are normalized toAUTO, so the terminal falls back to its own batch report configuration. - New — ConnectionConfig factories:
createAppMode(),createCableMode(),createLanMode(),createDefault(). - New — More
PaymentEventprogress 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. TreatPaymentEventas an open set — match the states you care about and always provide anelsebranch. - Breaking —
PaymentEvent.WaitingOnlineResponseremoved: usePaymentEvent.OnlineProcessing, which reports the same stage. - Breaking — Gson replaced by Jackson: the POM now declares
jackson-databind/jackson-module-kotlin2.17.2 instead of Gson. Both are internal to the SDK, so integrations using the typed models need no change.BasicRequest.bizData/BasicResponse.bizDatachanged fromJsonObjectto rawString. - New —
AppToAppMode:ConnectionConfig.setAppToAppMode(mode)andcreateAppMode(mode). Java callers of the old no-argumentcreateAppMode()must now passAppToAppMode.CUSTOM. - New —
TipSuggestions.names: optional display labels matched positionally tovalues; ignored when missing or of a different size. - New —
PaymentResultfields:relatedTransactionStatusandtransactionBatchStatus. - Added
DiscoveredService(name, host, port)model andDiscoveryListenercallback. - Added discovery/scan error codes
E501–E506. - 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)withresult.isFailed() == true. RemovedonDeclinedcallback. - 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
tipConfigmoved fromAmountInfoto the transaction request (SaleRequest/PostAuthRequest).
v1.0.5
- Added
PaymentCallback.onDeclined(PaymentResult)for clean decline separation. - Added
PaymentCallbackAdapterandConnectionListenerAdapter. - Added
TaplinkSDK.isInitialized()andTaplinkSDK.getConnectionStatus().
v1.0.4
- Added
TipConfigwith on-screen tip collection and suggested tip amounts.
v1.0.3
- Introduced type-safe
TaplinkClientwith dedicated request models. - Moved connection settings to
ConnectionConfig. - Removed
TaplinkException; errors handled throughPaymentCallback.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.CABLEwithCableProtocol. - Added connection persistence and reconnection.
v1.0.0
- Initial public release with App-to-App, LAN, and Cable connection modes.