SDK Android
SDK Android para conectar y procesar pagos con terminales POS de Cubo Pago. El SDK gestiona automáticamente el escaneo Bluetooth, la conexión, la configuración EMV, la lectura de tarjetas, el procesamiento de pagos y la captura de firma — todo con UI integrada.
Disponible para El Salvador (USD), Guatemala (GTQ) y Panamá (PAB).
Requisitos
- minSdk: 24 (Android 7.0)
- Lenguaje: Kotlin
- Arquitectura: Activity única recomendada (Jetpack Compose + Navigation)
Configuración
Dependencia
Agrega en el build.gradle.kts de tu app:
dependencies {
implementation("com.cubopago:possdk:<version>")
}Permisos
Tu app debe solicitarlos en tiempo de ejecución antes de llamar a CuboPosSDK.init():
| Permiso | Cuándo | Por qué |
|---|---|---|
ACCESS_FINE_LOCATION | Siempre | Captura de ubicación + escaneo BLE en Android 6-11 |
BLUETOOTH_SCAN | Android 12+ | Escaneo BLE |
BLUETOOTH_CONNECT | Android 12+ | Conexión con el dispositivo POS |
El SDK valida los permisos pero nunca los solicita. Esa es responsabilidad de tu app.
Ejemplo de solicitud de permisos:
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
if (results.all { it.value }) {
initializeSdk()
} else {
showError("Permisos requeridos no otorgados")
}
}
private fun requestPermissions() {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.add(Manifest.permission.BLUETOOTH_SCAN)
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
permissionLauncher.launch(permissions.toTypedArray())
}Autenticación
val authConfig = CuboAuthConfig.ApiKey("pk_live_your_api_key")- La clave se envía como header
X-API-Keyen cada solicitud a la API. - Usa el entorno
SANDBOXpara pruebas yPRODUCTIONpara pagos reales.
Entornos
| Entorno | Cuándo usarlo |
|---|---|
CuboEnvironment.SANDBOX | Pruebas y desarrollo |
CuboEnvironment.PRODUCTION | Pagos reales |
Inicialización
Llama a CuboPosSDK.init() una sola vez, después de que los permisos hayan sido otorgados. Generalmente en Application.onCreate() o Activity.onCreate().
CuboPosSDK.init(
CuboConfig(
context = applicationContext,
authConfig = CuboAuthConfig.ApiKey("pk_live_your_api_key"),
environment = CuboEnvironment.SANDBOX,
enableLogging = BuildConfig.DEBUG,
enableMsi = false
)
)| Parámetro | Requerido | Default | Descripción |
|---|---|---|---|
context | Sí | — | Contexto de la aplicación |
authConfig | Sí | — | Tu API key |
environment | No | PRODUCTION | SANDBOX para pruebas |
enableLogging | No | false | Logs en Logcat con el tag CuboPosSDK |
enableMsi | No | false | Habilitar pagos en cuotas |
¿Qué hace init() internamente?
Valida permisos
Verifica INTERNET y ACCESS_FINE_LOCATION (lanza IllegalStateException si faltan).
Captura ubicación
Obtiene la ubicación del dispositivo mediante Google Play Services y la guarda en SharedPreferences.
Inicializa componentes
Crea el proveedor de hardware POS, el cliente de API, el gestor de conexión y el gestor de transacciones.
Conexión
El SDK provee un bottom sheet integrado que gestiona el escaneo, la selección de dispositivo y el progreso de conexión automáticamente.
Tu App CuboPosSDK
| |
|-- init(config) ----------->|
|-- setConnectionListener -->|
|-- scanAndConnect() ------->| (SDK muestra bottom sheet)
| | (usuario selecciona dispositivo)
|<-- onConnectionStatus -----| (SCANNING → CONNECTING → CONFIGURING → CONNECTED)
|<-- onDeviceReady(info) ----| (POS listo para pagos)Conectar
CuboPosSDK.connection().setConnectionListener(this)
CuboPosSDK.connection().scanAndConnect(activity = this, timeoutSeconds = 30)El bottom sheet muestra:
- Animación de escaneo con los dispositivos descubiertos
- El usuario toca un dispositivo para seleccionarlo
- Progreso de conexión: conectando → identificando → registrando → configurando
- Estado de éxito o error
Implementación de CuboConnectionListener
interface CuboConnectionListener {
fun onConnectionStatusChanged(status: ConnectionStatus)
fun onDeviceFound(device: CuboDevice) // No necesario con scanAndConnect()
fun onDeviceReady(posInfo: CuboPosInfo)
fun onConnectionError(error: CuboConnectionError)
}Al usar scanAndConnect(), solo necesitas manejar onDeviceReady y onConnectionError. Los demás callbacks se siguen disparando si los querés usar para logs o analíticas.
Estados de ConnectionStatus
| Estado | Significado |
|---|---|
DISCONNECTED | Sin dispositivo conectado |
SCANNING | Buscando dispositivos Bluetooth |
CONNECTING | Conectándose al dispositivo seleccionado |
CONFIGURING | Registrando con la API de Cubo y actualizando configuración EMV |
CONNECTED | Listo para transacciones |
Manejo de errores de conexión
override fun onConnectionError(error: CuboConnectionError) {
when (error) {
is CuboConnectionError.BluetoothDisabled ->
showError("Activa el Bluetooth")
is CuboConnectionError.BluetoothPermissionsDenied ->
showError("Permisos de Bluetooth denegados")
is CuboConnectionError.LocationPermissionDenied ->
showError("Permiso de ubicación denegado")
is CuboConnectionError.ScanTimeout ->
showError("No se encontraron dispositivos")
is CuboConnectionError.DeviceNotFound ->
showError("Dispositivo no encontrado")
is CuboConnectionError.ConnectionFailed ->
showError("Falló la conexión, intenta de nuevo")
is CuboConnectionError.PosNotRegistered ->
showError("POS no registrado — contacta a Cubo")
is CuboConnectionError.RegistrationFailed ->
showError("Error de red al registrar POS")
is CuboConnectionError.EmvUpdateFailed ->
showError("Error actualizando configuración")
is CuboConnectionError.LocationUnavailable ->
showError("Ubicación no disponible")
is CuboConnectionError.Unknown ->
showError(error.message)
}
}Métodos adicionales de conexión
CuboPosSDK.connection().disconnect() // Desconectar del POS
CuboPosSDK.connection().isPosConnected() // Verificar si está conectado
CuboPosSDK.connection().getConnectedPosInfo() // Obtener info del POS conectado
CuboPosSDK.connection().getConnectionStatus() // Obtener estado actual
CuboPosSDK.connection().removeConnectionListener() // Remover listenerPagos
Una vez que el POS está conectado, procesa pagos con una sola llamada. El SDK muestra un bottom sheet que gestiona el progreso, selección de AID, selección de cuotas y visualización del resultado.
Tu App CuboPosSDK POS
| | |
|-- startPaymentWithUI() --->| (SDK muestra bottom sheet) |
| |-- configurar + leer tarjeta >|
| | | (usuario acerca/inserta tarjeta)
| | (EMV, PIN, selección AID) |
| |-- llamada API -- Cubo API -->|
| | (firma si es necesario) |
|<-- onTransactionComplete --| |
| (resultado + tickets) | |Iniciar un pago
CuboPosSDK.transaction().setTransactionListener(this)
CuboPosSDK.transaction().startPaymentWithUI(
activity = this,
request = CuboTransactionRequest(
amountInCents = 1500, // $15.00
currency = CuboCurrency.USD
)
)El monto siempre debe enviarse en centavos: 1500 = 1.00.
Cancelar un pago
// Cancela una transacción en curso en cualquier momento
CuboPosSDK.transaction().cancelTransaction()
// Dispara onTransactionError(CuboTransactionError.TransactionCancelled)Monedas soportadas
| Moneda | País |
|---|---|
CuboCurrency.USD | El Salvador |
CuboCurrency.GTQ | Guatemala |
CuboCurrency.PAB | Panamá |
Implementación de CuboTransactionListener
Al usar startPaymentWithUI(), el bottom sheet gestiona todo el feedback visual automáticamente. Los callbacks se siguen disparando — úsalos para logs, analíticas o lógica post-pago.
interface CuboTransactionListener {
fun onWaitingForCard()
fun onCardRead(readType: CuboReadType)
fun onProcessingTransaction()
fun onTransactionComplete(result: CuboTransactionResult)
fun onTransactionError(error: CuboTransactionError)
}Resultado de la transacción
data class CuboTransactionResult(
val transactionId: Int,
val paymentIntentToken: String,
val status: CuboTransactionStatus, // SUCCEEDED, DECLINED, ERROR, CANCELLED
val amount: String, // "15.00"
val currency: CuboCurrency,
val readType: CuboReadType, // NFC, CHIP o MAGSTRIPE
val cardLastDigits: String, // "1234"
val cardBrand: String, // "VISA", "MASTERCARD", etc.
val authorizationCode: String?,
val referenceId: String?,
val clientName: String?,
val companyTicket: CuboTicket?, // Copia del comercio
val clientTicket: CuboTicket?, // Copia del cliente
val signatureBitmap: Bitmap?, // Firma capturada (null si no se requiere)
val createdAt: String
)Manejo de errores de transacción
override fun onTransactionError(error: CuboTransactionError) {
when (error) {
is CuboTransactionError.DeviceNotConnected ->
showError("POS no conectado")
is CuboTransactionError.TransactionCancelled ->
showInfo("Transacción cancelada")
is CuboTransactionError.CardReadFailed ->
showError("Error leyendo tarjeta, intenta de nuevo")
is CuboTransactionError.CardNotSupported ->
showError("Tarjeta no soportada")
is CuboTransactionError.EmvError ->
showError("Error EMV, intenta de nuevo")
is CuboTransactionError.PinCancelled ->
showError("PIN cancelado")
is CuboTransactionError.ApiError ->
showError("Error de red, verifica tu conexión")
is CuboTransactionError.Timeout ->
showError("Tiempo agotado, intenta de nuevo")
is CuboTransactionError.Declined ->
showError("Transacción rechazada")
is CuboTransactionError.Unknown ->
showError(error.message)
}
}Pagos en cuotas (MSI)
Habilitar cuotas para tu comercio
Los pagos en cuotas solo están disponibles en Guatemala y El Salvador.
Los planes de cuotas se configuran fuera del SDK, desde el panel de Cubo Admin o la Cubo App del comercio. Ahí el comercio selecciona los plazos que desea ofrecer (por ejemplo, 3, 6, 12 meses). El SDK obtiene y muestra automáticamente solo los planes habilitados para ese comercio.
Habilitar en el SDK
Configura enableMsi = true en CuboConfig. El SDK gestiona la selección de cuotas automáticamente dentro del bottom sheet de pago.
CuboPosSDK.init(
CuboConfig(
context = applicationContext,
authConfig = CuboAuthConfig.ApiKey("pk_live_your_key"),
enableMsi = true
)
)
// El bottom sheet de pago mostrará las opciones de cuotas automáticamente
CuboPosSDK.transaction().startPaymentWithUI(this, request)Si enableMsi = false (por defecto), todos los pagos son únicos. No se muestra UI de cuotas.
Ejemplo completo
class PaymentActivity : AppCompatActivity(), CuboConnectionListener, CuboTransactionListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_payment)
requestPermissions()
}
// --- Permisos ---
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
if (results.all { it.value }) initializeSdk()
else showError("Permisos requeridos")
}
private fun requestPermissions() {
val permissions = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.add(Manifest.permission.BLUETOOTH_SCAN)
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
permissionLauncher.launch(permissions.toTypedArray())
}
// --- Inicialización + Conexión ---
private fun initializeSdk() {
CuboPosSDK.init(
CuboConfig(
context = applicationContext,
authConfig = CuboAuthConfig.ApiKey("pk_live_your_key_here"),
environment = CuboEnvironment.SANDBOX,
enableLogging = BuildConfig.DEBUG
)
)
CuboPosSDK.connection().setConnectionListener(this)
CuboPosSDK.connection().scanAndConnect(this, timeoutSeconds = 30)
}
// --- CuboConnectionListener ---
override fun onConnectionStatusChanged(status: ConnectionStatus) {}
override fun onDeviceFound(device: CuboDevice) {}
override fun onDeviceReady(posInfo: CuboPosInfo) {
runOnUiThread {
showStatus("POS listo")
CuboPosSDK.transaction().setTransactionListener(this)
enablePayButton()
}
}
override fun onConnectionError(error: CuboConnectionError) {
runOnUiThread { showError("Error: $error") }
}
// --- Pago ---
fun onPayButtonClicked(amountInCents: Long) {
if (!CuboPosSDK.connection().isPosConnected()) {
showError("POS no conectado")
return
}
CuboPosSDK.transaction().startPaymentWithUI(
activity = this,
request = CuboTransactionRequest(
amountInCents = amountInCents,
currency = CuboCurrency.USD
)
)
}
// --- CuboTransactionListener ---
override fun onWaitingForCard() {}
override fun onCardRead(readType: CuboReadType) {}
override fun onProcessingTransaction() {}
override fun onTransactionComplete(result: CuboTransactionResult) {
result.companyTicket?.let { printReceipt(it) }
}
override fun onTransactionError(error: CuboTransactionError) {
Log.w("MyApp", "Error: $error")
}
// --- Ciclo de vida ---
override fun onDestroy() {
super.onDestroy()
CuboPosSDK.transaction().removeTransactionListener()
CuboPosSDK.connection().removeConnectionListener()
CuboPosSDK.shutdown()
}
}Ciclo de vida
| Evento | Acción |
|---|---|
| Permisos otorgados | CuboPosSDK.init(config) |
| Activity visible | setConnectionListener(this) + scanAndConnect(this) |
| Activity oculta | disconnect() + removeConnectionListener() |
| Botón de pago presionado | startPaymentWithUI(this, request) |
| App cerrándose | removeTransactionListener() + removeConnectionListener() + shutdown() |
init()puede llamarse múltiples veces de forma segura (reemplaza la instancia anterior).- Después de
shutdown(), debes llamar ainit()nuevamente. isInitialized()devuelvetrueentreinit()yshutdown().
Buenas prácticas
Arquitectura de Activity única (Recomendado)
Recomendamos usar el patrón de Activity única con Jetpack Compose y Navigation Compose. En lugar de múltiples Activities, usa una sola AppCompatActivity con un NavHost que gestiona las pantallas como destinos composables.
Este enfoque:
- Evita problemas de recreación de Activity durante la conexión POS
- Simplifica la gestión del ciclo de vida del SDK (una Activity = un
init()/shutdown()) - Se alinea con la arquitectura moderna recomendada por Google
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestPermissions()
setContent {
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("payment") { PaymentScreen(navController) }
composable("receipt") { ReceiptScreen(navController) }
}
}
}
}Conectar en onResume, desconectar en onPause
Siempre vincula la conexión POS al ciclo de vida de la Activity:
override fun onResume() {
super.onResume()
CuboPosSDK.connection().setConnectionListener(this)
if (!CuboPosSDK.connection().isPosConnected()) {
CuboPosSDK.connection().scanAndConnect(this)
}
}
override fun onPause() {
super.onPause()
CuboPosSDK.connection().disconnect()
CuboPosSDK.connection().removeConnectionListener()
}Las conexiones Bluetooth consumen batería y pueden volverse inestables en segundo plano. Desconectar en onPause libera el hardware limpiamente y evita conexiones fantasma.
Inicializar una vez, conectar muchas veces
// ✅ Correcto: inicializar una vez
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
CuboPosSDK.init(config)
}
}
// ❌ Incorrecto: inicializar cada vez que te conectas
override fun onResume() {
CuboPosSDK.init(config) // Innecesario — ya fue inicializado
CuboPosSDK.connection().scanAndConnect(this)
}Siempre verifica la conexión antes de un pago
fun startPayment(amount: Long) {
if (!CuboPosSDK.connection().isPosConnected()) {
showError("POS no conectado")
return
}
CuboPosSDK.transaction().startPaymentWithUI(this, request)
}Shutdown limpio
Siempre llama a shutdown() cuando el SDK ya no sea necesario:
override fun onDestroy() {
super.onDestroy()
CuboPosSDK.transaction().removeTransactionListener()
CuboPosSDK.connection().removeConnectionListener()
CuboPosSDK.shutdown()
}Solución de problemas
Inicialización y conexión
| Problema | Solución |
|---|---|
IllegalStateException: INTERNET permission | Agrega <uses-permission android:name="android.permission.INTERNET"/> al manifest |
IllegalStateException: ACCESS_FINE_LOCATION | Solicita el permiso en tiempo de ejecución antes de init() |
IllegalStateException: CuboPosSDK not initialized | Llama primero a init() |
BluetoothDisabled | Pide al usuario que active el Bluetooth |
BluetoothPermissionsDenied | Solicita BLUETOOTH_SCAN + BLUETOOTH_CONNECT (Android 12+) |
ScanTimeout / no aparecen dispositivos | Asegúrate de que el POS esté encendido y en rango |
ConnectionFailed | El POS puede estar fuera de rango o emparejado con otro teléfono |
PosNotRegistered | Contacta al soporte de Cubo para registrar el POS |
RegistrationFailed | Verifica conexión a internet y reintenta |
EmvUpdateFailed | Reintenta conexión. Si persiste, contacta a Cubo |
Transacciones
| Problema | Solución |
|---|---|
DeviceNotConnected | Conecta el POS primero, espera a onDeviceReady() |
Timeout | No se presentó tarjeta en 60s — reintenta |
CardReadFailed | Limpia la tarjeta/lector, intenta con tap o inserción |
CardNotSupported | Prueba con otra tarjeta |
PinCancelled | El usuario canceló el PIN — reintenta si desea |
ApiError | Verifica conexión a internet y reintenta |
Declined | El emisor rechazó la tarjeta — prueba con otra |
Imports
import com.cubopago.possdk.CuboPosSDK
import com.cubopago.possdk.CuboConfig
import com.cubopago.possdk.auth.CuboAuthConfig
import com.cubopago.possdk.models.CuboEnvironment
import com.cubopago.possdk.connection.CuboConnectionListener
import com.cubopago.possdk.models.ConnectionStatus
import com.cubopago.possdk.models.CuboConnectionError
import com.cubopago.possdk.models.CuboDevice
import com.cubopago.possdk.models.CuboPosInfo
import com.cubopago.possdk.transaction.CuboTransactionListener
import com.cubopago.possdk.transaction.CuboTransactionRequest
import com.cubopago.possdk.transaction.CuboTransactionResult
import com.cubopago.possdk.transaction.CuboTransactionError
import com.cubopago.possdk.transaction.CuboCurrency
import com.cubopago.possdk.transaction.CuboReadType
import com.cubopago.possdk.models.CuboTicket