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(), 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 |
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 |
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: settipConfig.onScreenTiptotrueto enable the feature. Other fields intipConfig(tipMode,tipWithTax,suggestions) will use the SUNBAY platform’s default configuration if not specified. Whenamount.tipAmountandtipConfigare both present,tipAmounttakes precedence. -
If
amount.surchargeAmountis 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(): 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.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.