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 SDK module to your project’s settings.gradle.kts:

include(":app") include(":lib_taplink_sdk") include(":lib_taplink_communication")

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

dependencies { implementation(project(":lib_taplink_sdk")) }

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") .setMerchantId("your_merchant_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: PaymentResponse) { // Payment successful Toast.makeText(this@MainActivity, "Payment successful: ${result.transactionId}", Toast.LENGTH_SHORT).show() } override fun onFailure(error: PaymentError) { // Payment failed Toast.makeText(this@MainActivity, "Payment failed: ${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)

Query Transaction

Query transaction status, especially useful for timeout scenarios.

val query = QueryRequest() .setTransactionRequestId("TXN20231119001") client.query(query, object : PaymentCallback { override fun onSuccess(result: PaymentResponse) { // Handle query result when (result.transactionStatus) { "SUCCESS" -> handleSuccess(result) "PROCESSING" -> continuePolling() "FAILED" -> 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: PaymentResponse) { val batchInfo = result.batchCloseInfo // Display batch summary showBatchSummary( batchNo = result.batchNo, totalCount = batchInfo?.totalCount ?: 0, totalAmount = batchInfo?.totalAmount ?: BigDecimal.ZERO ) } override fun onFailure(error: PaymentError) { // Handle error } })

Error Handling

The SDK provides structured error information with handling suggestions.

client.sale(request, object : PaymentCallback { override fun onFailure(error: PaymentError) { // Access error details val code = error.code val message = error.message val suggestion = error.suggestion val canRetry = error.canRetryWithSameId // Handle based on error category when (error.detail.category) { ErrorCategory.INITIALIZATION -> { // Initialization error: reinitialize SDK showDialog("Initialization Error", message, suggestion) } ErrorCategory.CONNECTION -> { // Connection error: reconnect showDialog("Connection Error", message, suggestion) } ErrorCategory.AUTHENTICATION -> { // Authentication error: check credentials showDialog("Authentication Failed", message, suggestion) } ErrorCategory.TRANSACTION -> { // Transaction error: handle based on retry rules if (canRetry) { retryWithSameRequest() } else { createNewTransaction() } } } } })

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

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