Skip to main content

Terminal C2C SDK for Android (Kotlin)

The Terminal C2C SDK is a convenience wrapper around the Terminal C2C API. For providers other than BRI and NTT, you can call the Terminal C2C API directly without the SDK. BRI and NTT terminals require either the Gateway App or the C2C SDK as a pre-install.
The Terminal C2C SDK for Android enables you to integrate secure payment processing directly into your Android applications. This SDK provides a client-to-client communication interface that allows your POS system to interact with the Terminal Gateway over the local network.
TerminalC2C is a singleton object that provides both synchronous (suspend) and asynchronous methods for payment operations. It handles HTTP communication with the Terminal Gateway, request serialization, response parsing, and comprehensive error handling.

Version information

What’s new:
  • Added Cashup provider support for Indonesian terminals
  • Added SHC provider support for Malaysian terminals
  • Added multiple-provider support enabling apps to handle different terminal types simultaneously
  • Enhanced TerminalDevice configuration with provider-specific methods
  • Improved terminal selection for apps managing multiple EDC devices
The SDK follows semantic versioning. Breaking changes will bump the major version number.

Download

Download the Terminal Android SDK:

Installation

Follow these steps to add the Terminal C2C Android SDK to your project.
1

Extract the SDK

Extract the provided SDK zip file so that a repository directory is available in your project root.
Verify that the repository folder contains the necessary Maven artifacts for the SDK.
2

Configure Gradle settings

Add the repository to your Gradle configuration. Choose one of the following approaches:
3

Add dependencies

Add the required dependencies to your build.gradle.kts or build.gradle file:
dependencies {
  // Terminal C2C SDK
  implementation("co.xendit.terminal:c2c-android:<latest_version>")
  
  // Core Terminal SDK (required)
  implementation("co.xendit.terminal:core-android:<latest_version>")
  
  // Optional: Provider-specific dependencies for helper methods
  // Indonesia - BRI terminals
  implementation("co.xendit.terminal:id-bri-android:<latest_version>")
  
  // Indonesia - Cashup terminals  
  implementation("co.xendit.terminal:id-cashup-android:<latest_version>")
  
  // Thailand - NTT terminals
  implementation("co.xendit.terminal:th-ntt-android:<latest_version>")

  // Malaysia - SHC terminals
  implementation("co.xendit.terminal:my-shc-android:<latest_version>")
}
Provider-specific dependencies are optional and enable convenient helper methods for creating TerminalDevice instances. Include only the dependencies for providers you plan to use.
Sync your project to download the dependencies.

Getting Started

Before starting, you’ll need an In-Person Payment CLIENT_KEY from the Xendit In-Person Payment team. You will also need the terminal IP address and Terminal ID (TID).

Step 1: Initialize Terminal App

Initialize the Terminal App with your client key:
import co.xendit.terminal.core.TerminalApp

// In your Application class or Activity
TerminalApp.initialize(context, CLIENT_KEY, TerminalMode.LIVE)
// or TerminalMode.INTEGRATION for testing
Replace CLIENT_KEY with your actual client key from the Xendit dashboard.

Step 2: Add Terminal Providers

For Share Commerce (SHC) and Cashup terminals, make sure the Gateway app is installed on the device. If it is missing, install it from the Gateway download page or contact inpersonpayments@xendit.co for assistance.
// Add BRI provider for Indonesian terminals
TerminalC2C.addProvider(TerminalBRI)
Required dependency:
build.gradle.kts
implementation("co.xendit.terminal:id-bri-android:<latest_version>")
BRI provider supports timeout configuration for card and QR transactions, and provides enhanced transaction retry capabilities.
Provider dependencies are optional. You can use the generic TerminalDevice.create() method without adding provider-specific dependencies. Provider frameworks enable access to specialized helper methods like TerminalDevice.bri(), TerminalDevice.ntt(), and TerminalDevice.cashup().

Step 3: Configure Terminal Device

Set up your terminal device configuration using the Terminal ID (TID) and IP address:
Use the generic create method (available without provider dependencies):
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Payment
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.core.data.TerminalDevice
import co.xendit.terminal.id.bri.BRIPaymentMethod

// Configure the default terminal device
val terminalDevice = TerminalDevice.create(
  id = "TERMINAL_ID",
  ipAddress = "192.168.1.100",
  provider = "BRI" // or "CASHUP", "NTT"
)

// Set as default device for TerminalC2C
TerminalC2C.setTerminalDevice(terminalDevice)
You can also specify a device per request instead of setting a default device.

Supported Payment Methods by Provider

When using co.xendit.terminal:id-bri-android dependency:
Payment MethodDescriptionCode Example
Insert CardChip card transactionsBRIPaymentMethod.insertCard
ContactlessTap-to-pay transactionsBRIPaymentMethod.contactless
QRISQR code paymentsBRIPaymentMethod.qris
BrizziE-money card paymentsBRIPaymentMethod.brizzi
BRI terminals support both physical card payments and digital payment methods popular in Indonesia.
Provider-specific payment enums require the corresponding provider dependency. They expose the exact string values expected by each terminal while keeping your code type-safe.
Replace the enum with the provider you integrate: CashupPaymentMethod, NTTPaymentMethod, or SHCPaymentMethod mirror the samples above.

API Usage

Create Payment

Process a payment transaction using the TerminalC2C singleton:
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Payment
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.id.bri.BRIPaymentMethod

// Create payment data
val payment = Payment(
  orderID = "ORDER_123",
  amount = 10000.0,
  paymentMethod = BRIPaymentMethod.contactless
)

// Execute payment (suspend function)
try {
  val result = TerminalC2C.createPayment(
    payment = payment,
    device = null,          // Optional: override the default device
    isSimulation = false    // Optional: pass true when exercising the simulator
  )
  println("Payment successful: ${result.status}")
} catch (error: TerminalException) {
  println("Payment failed: ${error.message}")
}
For non-coroutine environments, call TerminalC2C.createPaymentAsync which returns an ActionJob you can cancel if the UI is dismissed. The callback signature is (TerminalResult?, Throwable?).

Simulation Testing

Use simulation mode to validate your integration without connecting to a physical terminal.
import co.xendit.terminal.c2c.TerminalC2C

// Optional: toggle simulation globally for all subsequent requests
TerminalC2C.isSimulation = true

// Or pass the flag per payment request
val result = TerminalC2C.createPayment(
  payment = payment,
  isSimulation = true
)
Disable simulation (TerminalC2C.isSimulation = false and isSimulation = false) before running against real hardware. Use the special simulation amounts (400508 decline, 400509 unavailable, 400711 cancel) documented in the quickstart to trigger test scenarios.

Cancel Payment

Cancel an ongoing payment transaction:
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Cancel
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.id.bri.BRIPaymentMethod

// Create cancel data
val cancel = Cancel(
  paymentMethod = BRIPaymentMethod.contactless, // Optional
  terminalReference = "123456" // Reference from previous transaction
)

try {
  val result = TerminalC2C.cancelPayment(
    cancel = cancel,
    device = null,
    isSimulation = false
  )
  println("Cancellation successful: ${result.status}")
} catch (error: TerminalException) {
  println("Cancellation failed: ${error.message}")
}
Trigger receipt printing on the terminal:
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Receipt
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.id.bri.BRIPaymentMethod

// Create receipt data
val receipt = Receipt(
  paymentMethod = BRIPaymentMethod.contactless, // Optional
  terminalReference = "123456"
)

try {
  val result = TerminalC2C.printReceipt(
    receipt = receipt,
    device = null,
    isSimulation = false
  )
  println("Receipt print command sent: ${result.status}")
} catch (error: TerminalException) {
  println("Receipt print failed: ${error.message}")
}

Perform Settlement

Initiate a settlement (batch close) process:
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Settlement
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.id.bri.BRIPaymentMethod

// Create settlement data
val settlement = Settlement(
  paymentMethod = BRIPaymentMethod.contactless // Optional: pass null to settle all methods
)

try {
  val result = TerminalC2C.performSettlement(
    settlement = settlement,
    device = null,
    isSimulation = false
  )
  println("Settlement successful: ${result.status}")
} catch (error: TerminalException) {
  println("Settlement failed: ${error.message}")
}

Get Transaction History

Retrieve transaction history from the terminal:
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Histories
import co.xendit.terminal.core.data.Status
import co.xendit.terminal.core.data.TerminalResult
import co.xendit.terminal.core.data.response.CommandType

// Create histories data with optional filters
val histories = Histories(
    commands = listOf(CommandType.PAY, CommandType.CANCEL), // Optional: filter by command types
    statuses = listOf(Status.SUCCESS), // Optional: filter by statuses
    from = "2024-01-01T00:00:00Z", // Optional: ISO 8601 format start date
    to = "2024-01-31T23:59:59Z" // Optional: ISO 8601 format end date
)

try {
  val results: List<TerminalResult> = TerminalC2C.getHistory(histories)
  println("Retrieved ${results.size} records")

  results.forEach { transaction ->
    println("Transaction status: ${transaction.status}")
  }
} catch (error: TerminalException) {
  println("Failed to retrieve history: ${error.message}")
}

Async API Usage

For interoperability with Java or callback-based code, use the async methods:

Create Payment (Async)

import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Payment
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.id.bri.BRIPaymentMethod

val payment = Payment(
  orderID = "ORDER_123",
  amount = 10000.0,
  paymentMethod = BRIPaymentMethod.contactless
)

val job = TerminalC2C.createPaymentAsync(
  payment = payment,
  device = null,
  isSimulation = false
) { result, error ->
  when {
    error != null -> println("Payment failed: ${error.message}")
    result != null -> println("Payment successful: ${result.status}")
    else -> println("Payment finished with no result")
  }
}

// Cancel the job if the user navigates away
job.cancel()

Cancel Payment (Async)

import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Cancel
import co.xendit.terminal.id.bri.BRIPaymentMethod

val cancel = Cancel(
  paymentMethod = BRIPaymentMethod.contactless,
  terminalReference = "123456"
)

val cancelJob = TerminalC2C.cancelPaymentAsync(
  cancel = cancel,
  device = null,
  isSimulation = false
) { result, error ->
  when {
    error != null -> println("Cancellation failed: ${error.message}")
    result != null -> println("Cancellation successful: ${result.status}")
  }
}

cancelJob.cancel()
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Receipt
import co.xendit.terminal.id.bri.BRIPaymentMethod

val receipt = Receipt(
  paymentMethod = BRIPaymentMethod.contactless,
  terminalReference = "123456"
)

val receiptJob = TerminalC2C.printReceiptAsync(
  receipt = receipt,
  device = null,
  isSimulation = false
) { result, error ->
  when {
    error != null -> println("Receipt print failed: ${error.message}")
    result != null -> println("Receipt print command sent: ${result.status}")
  }
}

receiptJob.cancel()

Perform Settlement (Async)

import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Settlement
import co.xendit.terminal.id.bri.BRIPaymentMethod

val settlement = Settlement(
  paymentMethod = BRIPaymentMethod.contactless
)

val settlementJob = TerminalC2C.performSettlementAsync(
  settlement = settlement,
  device = null,
  isSimulation = false
) { result, error ->
  when {
    error != null -> println("Settlement failed: ${error.message}")
    result != null -> println("Settlement successful: ${result.status}")
  }
}

settlementJob.cancel()

Get History (Async)

import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.Histories
import co.xendit.terminal.core.data.Status
import co.xendit.terminal.core.data.TerminalResult
import co.xendit.terminal.core.data.response.CommandType

val histories = Histories(
  commands = listOf(CommandType.PAY, CommandType.CANCEL),
  statuses = listOf(Status.SUCCESS),
  from = "2024-01-01T00:00:00Z",
  to = "2024-01-31T23:59:59Z"
)

val historyJob = TerminalC2C.getHistoryAsync(
  histories = histories,
  device = null,
  isSimulation = false
) { results: List<TerminalResult>?, error ->
  when {
    error != null -> println("Failed to retrieve history: ${error.message}")
    results != null -> {
      println("Retrieved ${results.size} records")
      results.forEach { println("Transaction status: ${it.status}") }
    }
  }
}

historyJob.cancel()

Error Handling

TerminalException

The SDK throws TerminalException for terminal-specific errors:
import co.xendit.terminal.c2c.TerminalC2C
import co.xendit.terminal.c2c.data.TerminalException
import co.xendit.terminal.c2c.data.ApiError

try {
    val result = TerminalC2C.createPayment(payment)
} catch (e: TerminalException) {
    val apiError = e.error
    when (apiError.code) {
        "FAILED_TO_CONNECT" -> {
            // Terminal Gateway service not reachable
            println("Cannot connect to Terminal Gateway")
        }
        "AUTHENTICATION_FAILED" -> {
            // Invalid client key or terminal credentials
            println("Authentication failed")
        }
        "TERMINAL_BUSY" -> {
            // Terminal is processing another transaction
            println("Terminal is busy")
        }
        else -> {
            println("Error: ${apiError.message}")
        }
    }
}

Error Structure

data class TerminalException(
  val httpStatus: Int,
  val error: ApiError
)

data class ApiError(
  val code: ErrorCode,
  val message: String
)

Troubleshooting

Common Issues and Solutions

Problem: SDK cannot reach the Terminal Gateway service.Solution:
  1. Verify Terminal Gateway service is running locally
  2. Check the terminal device IP address is correct
  3. Verify network connectivity between your app and gateway
// Verify terminal device configuration

// Using generic method
val device = TerminalDevice.create(
  id = "TERMINAL_ID", 
  ipAddress = "192.168.1.100", // Verify this IP
  provider = "BRI" // or "CASHUP", "NTT"
)

// Or using provider-specific helper (requires provider dependency)
// val device = TerminalDevice.bri("TERMINAL_ID", "192.168.1.100")
// val device = TerminalDevice.cashup("TERMINAL_ID", "192.168.1.100")
// val device = TerminalDevice.ntt("TERMINAL_ID", "192.168.1.100")

TerminalC2C.setTerminalDevice(device)
Problem: Error indicating no terminal device is configured.Solution: Set a default terminal device before making requests:
// Set default device
TerminalC2C.setTerminalDevice(terminalDevice)

// Then make requests
val result = TerminalC2C.createPayment(payment)
Problem: Requests fail with authentication errors.Solutions:
  1. Verify TerminalApp is initialized with correct client key
  2. Check terminal ID matches the configured device
  3. Ensure client key has Terminal C2C permissions
// Re-initialize with correct client key
TerminalApp.initialize(context, CLIENT_KEY, TerminalMode.LIVE)
Problem: Network-level errors when communicating with gateway.Solutions:
  1. Check network connectivity
  2. Verify firewall settings allow connections to gateway port
  3. Ensure Terminal Gateway service is accessible
  4. Check terminal device IP address is reachable
Use network debugging tools to verify connectivity between your app and the Terminal Gateway service.

Best Practices

Error Handling

Always implement comprehensive error handling:
suspend fun processPaymentSafely(orderID: String, amount: Double): Result<TerminalResult> {
    return try {
        val payment = Payment(
            orderID = orderID,
            amount = amount,
      paymentMethod = BRIPaymentMethod.contactless
        )
        val result = TerminalC2C.createPayment(payment)
        Result.success(result)
    } catch (e: TerminalException) {
        // Handle terminal-specific errors
        Result.failure(e)
    } catch (e: Exception) {
        // Handle other errors
        Result.failure(e)
    }
}

Device Management

Set up terminal device once during app initialization:
class PaymentManager {
    init {
        // Configure terminal device once using generic method
        val terminalDevice = TerminalDevice.create(
            id = "TERMINAL_001",
            ipAddress = "192.168.1.100",
            provider = "BRI" // or "CASHUP", "NTT"
        )
        TerminalC2C.setTerminalDevice(terminalDevice)
    }
    
    suspend fun createPayment(payment: Payment): TerminalResult {
        return TerminalC2C.createPayment(payment)
    }
}

Coroutine Scope Management

Use appropriate coroutine scopes for async operations:
class PaymentActivity : AppCompatActivity() {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    
    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()
    }
    
    fun processPayment() {
        val payment = Payment(
          orderID = "ORDER_123",
          amount = 10000.0,
          paymentMethod = BRIPaymentMethod.contactless
        )
        
        scope.launch {
            try {
                val result = TerminalC2C.createPayment(payment)
                // Update UI
            } catch (e: Exception) {
                // Handle error
            }
        }
    }
}

Finding Terminal Information

1

Find Terminal ID (TID)

Open the BRI FMS app on the terminal device and locate the Terminal ID in the device information section.
BRI terminal showing Terminal ID in FMS app
2

Find IP Address

Open the ECRLink app on the terminal and check the network settings for the IP address.
BRI terminal showing IP address in ECRLink app
Screenshots and UI layouts may vary by firmware or app version. Refer to the latest vendor documentation if the interface differs from these instructions.