Skip to Content
Integra pagos con terminales POS directamente desde tu app Android 🚀 - Conoce el SDK Android
SDKs
Logo de AndroidSDK Android

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

PermisoCuándoPor qué
ACCESS_FINE_LOCATIONSiempreCaptura de ubicación + escaneo BLE en Android 6-11
BLUETOOTH_SCANAndroid 12+Escaneo BLE
BLUETOOTH_CONNECTAndroid 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-Key en cada solicitud a la API.
  • Usa el entorno SANDBOX para pruebas y PRODUCTION para pagos reales.

Entornos

EntornoCuándo usarlo
CuboEnvironment.SANDBOXPruebas y desarrollo
CuboEnvironment.PRODUCTIONPagos 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ámetroRequeridoDefaultDescripción
contextContexto de la aplicación
authConfigTu API key
environmentNoPRODUCTIONSANDBOX para pruebas
enableLoggingNofalseLogs en Logcat con el tag CuboPosSDK
enableMsiNofalseHabilitar 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:

  1. Animación de escaneo con los dispositivos descubiertos
  2. El usuario toca un dispositivo para seleccionarlo
  3. Progreso de conexión: conectando → identificando → registrando → configurando
  4. 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

EstadoSignificado
DISCONNECTEDSin dispositivo conectado
SCANNINGBuscando dispositivos Bluetooth
CONNECTINGConectándose al dispositivo seleccionado
CONFIGURINGRegistrando con la API de Cubo y actualizando configuración EMV
CONNECTEDListo 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 listener

Pagos

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 = 15.00,100=15.00, `100` = 1.00.

Cancelar un pago

// Cancela una transacción en curso en cualquier momento CuboPosSDK.transaction().cancelTransaction() // Dispara onTransactionError(CuboTransactionError.TransactionCancelled)

Monedas soportadas

MonedaPaís
CuboCurrency.USDEl Salvador
CuboCurrency.GTQGuatemala
CuboCurrency.PABPanamá

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

EventoAcción
Permisos otorgadosCuboPosSDK.init(config)
Activity visiblesetConnectionListener(this) + scanAndConnect(this)
Activity ocultadisconnect() + removeConnectionListener()
Botón de pago presionadostartPaymentWithUI(this, request)
App cerrándoseremoveTransactionListener() + removeConnectionListener() + shutdown()
  • init() puede llamarse múltiples veces de forma segura (reemplaza la instancia anterior).
  • Después de shutdown(), debes llamar a init() nuevamente.
  • isInitialized() devuelve true entre init() y shutdown().

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

ProblemaSolución
IllegalStateException: INTERNET permissionAgrega <uses-permission android:name="android.permission.INTERNET"/> al manifest
IllegalStateException: ACCESS_FINE_LOCATIONSolicita el permiso en tiempo de ejecución antes de init()
IllegalStateException: CuboPosSDK not initializedLlama primero a init()
BluetoothDisabledPide al usuario que active el Bluetooth
BluetoothPermissionsDeniedSolicita BLUETOOTH_SCAN + BLUETOOTH_CONNECT (Android 12+)
ScanTimeout / no aparecen dispositivosAsegúrate de que el POS esté encendido y en rango
ConnectionFailedEl POS puede estar fuera de rango o emparejado con otro teléfono
PosNotRegisteredContacta al soporte de Cubo para registrar el POS
RegistrationFailedVerifica conexión a internet y reintenta
EmvUpdateFailedReintenta conexión. Si persiste, contacta a Cubo

Transacciones

ProblemaSolución
DeviceNotConnectedConecta el POS primero, espera a onDeviceReady()
TimeoutNo se presentó tarjeta en 60s — reintenta
CardReadFailedLimpia la tarjeta/lector, intenta con tap o inserción
CardNotSupportedPrueba con otra tarjeta
PinCancelledEl usuario canceló el PIN — reintenta si desea
ApiErrorVerifica conexión a internet y reintenta
DeclinedEl 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