Instalar el SDK en Kotlin
Es una biblioteca Kotlin/JVM sin dependencias de Android, así que se consume igual desde una app Android (minSdk 21) que desde un servicio JVM. Todas las llamadas son suspend y corren en Dispatchers.IO: puedes invocarlas desde el hilo principal sin bloquearlo.
| Coordenadas | com.spidipagos.spidi:spidi:0.2.3 |
| Repositorio | https://us-central1-maven.pkg.dev/linen-bliss-303822/spidi-maven |
| Registro | Google Cloud Artifact Registry (Maven privado) |
Lo que necesitas antes de empezar
- Acceso de lectura al repositorio. SPIDI otorga el rol Artifact Registry Reader sobre
spidi-maven. Necesitan el correo de Google de cada persona del equipo y la cuenta de servicio de tu integración continua. - Un
session_idde prueba, que crea tu backend contra la API. Pídela por el monto mínimo. - Datos de un pagador real: cédula, teléfono y banco venezolanos, asociados entre sí. El banco valida que ese teléfono pertenezca a esa cédula.
Paso 1 · Activa tu acceso
gcloud auth login
Exporta el token antes de compilar:
export ARTIFACT_REGISTRY_TOKEN="$(gcloud auth print-access-token)"
En PowerShell:
$env:ARTIFACT_REGISTRY_TOKEN = (gcloud auth print-access-token)
401 UnauthorizedEl token dura alrededor de una hora. Vuelve a exportarlo: es lo primero que hay que descartar.
Paso 2 · Declara la dependencia
repositories {
maven {
url = uri("https://us-central1-maven.pkg.dev/linen-bliss-303822/spidi-maven")
credentials {
username = System.getenv("ARTIFACT_REGISTRY_USERNAME") ?: "oauth2accesstoken"
password = System.getenv("ARTIFACT_REGISTRY_TOKEN")
?: error("Define ARTIFACT_REGISTRY_TOKEN con gcloud auth print-access-token")
}
}
mavenCentral()
}
dependencies {
implementation("com.spidipagos.spidi:spidi:0.2.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
kotlin { jvmToolchain(17) }
En un proyecto con dependencyResolutionManagement:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven {
url = uri("https://us-central1-maven.pkg.dev/linen-bliss-303822/spidi-maven")
credentials {
username = System.getenv("ARTIFACT_REGISTRY_USERNAME") ?: "oauth2accesstoken"
password = System.getenv("ARTIFACT_REGISTRY_TOKEN") ?: ""
}
}
}
}
En Android, además:
android {
defaultConfig { minSdk = 21 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("com.spidipagos.spidi:spidi:0.2.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
}
<uses-permission android:name="android.permission.INTERNET" />
Comprueba que resuelve:
./gradlew :app:dependencies --configuration runtimeClasspath
Paso 3 · Tu primera llamada
import com.spidipagos.spidi.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val client = SpidiClient() // Environment.PRODUCTION por defecto
val response = client.getSession(sessionId)
if (client.isPaidSession(response)) {
println("Esta sesión ya se pagó")
return@runBlocking
}
val amounts = Amounts.from(response.data)
println("Total: Bs. ${amounts.bsFormatted} (USD ${amounts.usdFormatted})")
val pos = client.getPosCommerce(response.data.posCommerceAlias)
val banks = client.getBanks()
println("POS: ${pos.data.id} · ${banks.size} bancos disponibles")
}
Paso 4 · El flujo del débito inmediato
4.1 — Prepara los datos del pagador
val identification = buildIdentification(IdentificationType.CEDULA, "12345678") // "V12345678"
val instrument = buildInstrument("0412", "1234567") // "04121234567"
val bank = client.getBanks().first { it.code == "0102" }
4.2 — Crea la transacción
val tx = client.createTransaction(
CreateTransactionInput(
identification = identification,
amount = formatBsAmount(session.bsAmount), // "952,50" — con coma
bankCode = bank.code, // "0102" — el código nacional
instrument = instrument, // el teléfono, no un nombre de método
posCommerceId = pos.data.id,
),
)
4.3 — Persiste antes de confirmar
val transaction = SpidiTransaction(
transactionId = tx.transactionId,
usdAmount = session.usdAmount,
bsAmount = session.bsAmount,
bcvRate = session.bcvRate,
payerPaymentId = identification,
payerPaymentPhone = instrument,
bankId = bank.id, // el UUID, no el código nacional
spidiSessionId = session.id,
spidiCommerceAlias = session.spidiCommerceAlias,
posCommerceAlias = session.posCommerceAlias,
mainCurrency = session.mainCurrency,
spidiId = tx.spidiId,
productServiceDescription = session.description.orEmpty(),
)
client.saveSession(
SaveSessionInput(
id = sessionId,
usdAmount = transaction.usdAmount,
bsAmount = transaction.bsAmount,
bcvRate = transaction.bcvRate,
spidiCommerceAlias = transaction.spidiCommerceAlias,
posCommerceAlias = transaction.posCommerceAlias,
mainCurrency = transaction.mainCurrency,
transactionId = transaction.transactionId,
payerPaymentId = transaction.payerPaymentId,
payerPaymentPhone = transaction.payerPaymentPhone,
bankId = transaction.bankId,
spidiId = transaction.spidiId,
posHomeUrl = transaction.posHomeUrl,
productServiceDescription = transaction.productServiceDescription,
),
)
4.4 — Confirma el débito. Aquí sale el dinero, y esta llamada no se reintenta:
try {
val result = client.confirmPayment(tx.transactionId, key, transaction)
result.rejection?.let { rejection ->
error("El banco rechazó el pago · ${rejection.codeId}: ${rejection.userDescription}")
}
println("Pago confirmado · comprobante ${result.id}")
} catch (e: SpidiException) {
// Nunca reintentes: la API no admite claves de idempotencia y un segundo
// intento puede debitar dos veces. Pregunta el estado.
val status = client.getTransactionStatus(sessionId)
error("Confirmación ambigua: ${e.message} · estado actual: ${status.status}")
}
catch (e: SpidiException) {
when (e.code) {
ErrorCode.SESSION_EXPIRED,
ErrorCode.SESSION_NOT_FOUND -> solicitarSesionNueva()
ErrorCode.INVALID_IDENTIFICATION -> resaltarCampoCedula()
ErrorCode.BANK_UNAVAILABLE -> sugerirOtroBanco()
ErrorCode.TIMEOUT, ErrorCode.NETWORK -> client.getTransactionStatus(sessionId)
else -> mostrar(e.message)
}
}
Validación y saneado
val errors = Validation.payer(
Payer(identificationNumber = "12345678", phoneNumber = "1234567", bank = bank),
)
if (errors.isNotEmpty()) {
mostrarError(errors[PayerField.BANK])
}
Sanitize.identification(IdentificationType.CEDULA, entrada) // dígitos, máximo 9
Sanitize.phoneNumber(entrada) // dígitos, máximo 7
Sanitize.paymentKey(entrada) // dígitos, máximo 8
Cripto vía Binance Pay
val order = client.createCryptoOrder(sessionId, session.bsAmount)
// order.paymentUrl · order.paymentDeeplink · order.paymentQr.img (PNG listo para pintar)
// Acredita solo cuando el procesador reporte el pago
val status = client.getCryptoOrderStatus(order.crixtoOrderId)
if (status !in listOf("PAID", "PAID_PENDING")) return
val result = client.confirmCryptoPayment(
order.crixtoOrderId,
CryptoConfirmationInput(
transactionId = "600${order.crixtoOrderId.takeWhile(Char::isDigit)}",
usdAmount = session.usdAmount,
bsAmount = session.bsAmount,
bcvRate = session.bcvRate,
payerPaymentId = "NA", // en cripto no hay cédula ni teléfono
payerPaymentPhone = "NA",
bankId = CRYPTO_BANK_ID, // banco fijo con el que acreditamos Binance
spidiSessionId = session.id,
spidiCommerceAlias = session.spidiCommerceAlias,
posCommerceAlias = session.posCommerceAlias,
mainCurrency = session.mainCurrency,
spidiId = order.transactionSpidiId,
),
)
En un ViewModel
class CheckoutViewModel(private val client: SpidiClient = SpidiClient()) : ViewModel() {
private val _state = MutableStateFlow<CheckoutState>(CheckoutState.Loading)
val state = _state.asStateFlow()
fun load(sessionId: String) = viewModelScope.launch {
_state.value = try {
val session = client.getSession(sessionId)
CheckoutState.Ready(session.data, Amounts.from(session.data), client.getBanks())
} catch (e: SpidiException) {
CheckoutState.Failed(e.message, e.code)
}
}
}
Las reglas que no cambian entre plataformas
Los formatos de monto, los dos identificadores del banco, la validación y —sobre todo— la regla de que la confirmación del débito no se reintenta nunca son idénticos en las cuatro. Viven en una sola página para que no se lean a medias:
→ El contrato y los errores · Antes de tocar dinero real
Del documento de integración que entrega SPIDI, de agosto de 2026, copiado literal. No lo hemos ejecutado: el paquete es privado y no tenemos acceso. A diferencia del resto de este portal, aquí no hay una prueba que lo respalde.