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
  • Multiple Connection Modes - Support for App-to-App, Cable, and LAN modes
  • 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.7") }

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 (App-to-App mode) val connectionConfig = ConnectionConfig() .setConnectionMode(ConnectionMode.APP_TO_APP) 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("10.00")) // Amount in smallest currency unit .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

App-to-App Mode

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

val connectionConfig = ConnectionConfig() .setConnectionMode(ConnectionMode.APP_TO_APP) TaplinkSDK.connect(connectionConfig, connectionListener)

Features:

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

Cable Mode

For traditional POS devices connected to payment terminals via USB or serial cable.

val connectionConfig = ConnectionConfig() .setConnectionMode(ConnectionMode.CABLE) .setCableProtocol(CableProtocol.AUTO) // Auto-detect cable type TaplinkSDK.connect(connectionConfig, connectionListener)

Supported Protocols:

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

LAN Mode

For POS devices connected to payment terminals via local network (wired/wireless).

// First connection: specify IP and port val connectionConfig = ConnectionConfig() .setConnectionMode(ConnectionMode.LAN) .setHost("192.168.1.100") .setPort(8443) TaplinkSDK.connect(connectionConfig, connectionListener) // Subsequent connections: use cached device info val connectionConfig = ConnectionConfig() .setConnectionMode(ConnectionMode.LAN) TaplinkSDK.connect(connectionConfig, connectionListener)

Features:

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

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("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)

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("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("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.

Authorization (Pre-Auth)

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

val amount = AuthAmountInfo() .setAuthAmount(BigDecimal("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("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 } })

Batch Close

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

val request = BatchCloseRequest.builder() .setTransactionRequestId("TXN_${System.currentTimeMillis()}") .setDescription("Batch Close") .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 } })

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

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

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: set tipConfig.onScreenTip to true to enable the feature. Other fields in tipConfig (tipMode, tipWithTax, suggestions) will use the SUNBAY platform’s default configuration if not specified. When amount.tipAmount and tipConfig are both present, tipAmount takes precedence.

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

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 // Device information TaplinkSDK.getConnectedDeviceId(): String? TaplinkSDK.getConnectionMode(): String? TaplinkSDK.getTaproVersion(): String? // Get transaction client TaplinkSDK.getClient(): TaplinkClient // 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.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)

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.

Last updated on