Saltar al contenido principal

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.

Coordenadascom.spidipagos.spidi:spidi:0.2.3
Repositoriohttps://us-central1-maven.pkg.dev/linen-bliss-303822/spidi-maven
RegistroGoogle 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_id de 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)
Si Gradle responde 401 Unauthorized

El 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,
),
)

Cripto con Binance Pay

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

De dónde sale este código

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.