Esta documentación cubre características de nuestro próximo lanzamiento. Aunque la funcionalidad principal y el flujo de trabajo descrito aquí permanecerán sin cambios, puede que notes algunas mejoras antes del lanzamiento final.
Con Pix Biometria de Belvo, la recolección de pagos de los usuarios se vuelve sencilla, eliminando la necesidad de que los usuarios naveguen a su institución financiera para aprobar cada solicitud de pago individual.
Esta guía demuestra la integración utilizando los métodos de conveniencia del SDK de Android de Belvo. El SDK maneja la comunicación de backend, los flujos de OAuth y el registro FIDO internamente, proporcionando un camino de integración simplificado.
El primer paso para habilitar la recolección de pagos biométricos es registrar el dispositivo del usuario con su institución. Durante el registro, los datos clave sobre el dispositivo y las credenciales de clave pública del usuario se registran de manera segura con su institución, asegurando que los pagos futuros puedan ser confirmados usando solo la autenticación biométrica.
Una vez que el registro esté completo, puedes comenzar a solicitar pagos directamente desde el dispositivo del usuario.
Antes de comenzar, asegúrate de tener:
- Generadas tus claves de la API de Belvo Payments
- Configurados los webhooks para recibir actualizaciones de estado de pagos y registros
- Generado un SDK Access Token (ver abajo)
- Instalado el Belvo Android SDK
El SDK de Pix Biometria requiere un token de acceso para autenticar las solicitudes de la API. Genera este token desde tu servidor backend y pásalo al SDK durante la inicialización.
Genera un token de acceso del SDK desde tu backend:
POST https://api.belvo.com/payments/api/widget-token/
Authorization: Basic <base64_encoded_secret_id:secret_password>
Content-Type: application/json{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Nunca codifiques tokens directamente en tu aplicación. Siempre genéralos del lado del servidor e implementa un almacenamiento seguro y lógica de actualización.
Requisitos Mínimos:
- Android API Level 24 (Android 7.0) o superior
- Kotlin 1.9.0 o superior
Agrega a tu build.gradle.kts (nivel de Módulo):
dependencies {
implementation("com.belvo:biometric-pix-core:1.0.0")
}Sincroniza tu proyecto ¡y estarás listo para comenzar!
Agrega permisos a AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />Agrega soporte de App Links para callbacks de OAuth:
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="yourdomain.com"
android:pathPrefix="/callback" />
</intent-filter>
</activity>Comparte el nombre de tu paquete y la huella digital del certificado SHA-256 con Belvo:
# Obtén tu huella digital SHA-256
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# Formato para compartir con Belvo:
# Package: com.yourcompany.appname
# SHA-256: AA:BB:CC:DD:EE:FF:...Los permisos deben ser solicitados antes de la inicialización del SDK. Además, la solicitud de permisos solo puede ser llamada desde un contexto de Activity.
Paso 1: Solicitar Permisos Primero
import com.belvo.biometricpixsdk.BiometricPixSDK
import androidx.activity.ComponentActivity
class EnrollmentActivity : ComponentActivity() {
private lateinit var sdk: BiometricPixSDK
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// DEBE solicitar permisos ANTES de inicializar el SDK
requestPermissions()
}
private fun requestPermissions() {
BiometricPixSDK.requestPermission(this) { granted ->
if (granted) {
// Permisos concedidos, ahora inicializar el SDK
initializeSDK()
} else {
// Manejar la denegación de permisos
showError("Se requiere permiso de ubicación para el registro")
}
}
}
private fun initializeSDK() {
// Inicializar DESPUÉS de que se concedan los permisos
sdk = BiometricPixSDK(
context = this,
accessToken = getAccessToken() // Desde tu backend
)
// Ahora listo para proceder con el registro
}
override fun onDestroy() {
super.onDestroy()
if (::sdk.isInitialized) {
sdk.cleanup()
}
}
}Siempre llama a cleanup() cuando hayas terminado con el SDK (por ejemplo, cuando la actividad se destruye o el usuario cierra sesión) para liberar adecuadamente los recursos y limpiar las tareas en segundo plano.
El proceso de inscripción registra el dispositivo de un usuario con su institución para pagos biométricos.
Obtén la lista de instituciones que soportan pagos biométricos:
import com.belvo.biometricpixsdk.BiometricPixSDK
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class EnrollmentViewModel(
private val sdk: BiometricPixSDK
) : ViewModel() {
private val _institutions = MutableStateFlow<List<Institution>>(emptyList())
val institutions: StateFlow<List<Institution>> = _institutions
fun loadInstitutions() {
viewModelScope.launch {
try {
_institutions.value = sdk.getPaymentInstitutions()
} catch (e: Exception) {
// Manejar error (red, autenticación, etc.)
_error.value = "Error al cargar instituciones: ${e.message}"
}
}
}
}Mostrar instituciones al usuario:
@Composable
fun InstitutionPickerScreen(
viewModel: EnrollmentViewModel
) {
val institutions by viewModel.institutions.collectAsState()
LazyColumn {
items(institutions) { institution ->
InstitutionItem(
institution = institution,
onClick = { viewModel.selectInstitution(institution) }
)
}
}
LaunchedEffect(Unit) {
viewModel.loadInstitutions()
}
}
@Composable
fun InstitutionItem(
institution: Institution,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp)
) {
AsyncImage(
model = institution.iconLogo,
contentDescription = institution.displayName,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(text = institution.displayName)
}
}Crea la inscripción e inmediatamente abre la aplicación de la institución:
fun startEnrollment() {
val institution = _selectedInstitution.value ?: return
viewModelScope.launch {
try {
// Crear inscripción
val enrollment = sdk.createEnrollment(
cpf = userCPF, // CPF del usuario
institution = institution.id, // ID de la institución seleccionada
accountTenure = customerCreatedDate, // Formato "YYYY-MM-DD"
callbackUrl = "https://yourdomain.com/callback"
)
// Guardar ID de inscripción e ID de dispositivo para más tarde
_enrollmentId.value = enrollment.id
_deviceId.value = enrollment.details.riskSignals.deviceId
// Inmediatamente abrir la aplicación de la institución usando la URL de redirección
enrollment.details.redirectUrl?.let { url ->
sdk.openRedirectUrl(context, url)
} ?: run {
_error.value = "No se recibió URL de redirección de la inscripción"
}
} catch (e: Exception) {
_error.value = "La creación de la inscripción falló: ${e.message}"
}
}
}El parámetro accountTenure debe ser la fecha en que el usuario fue creado como Cliente de Belvo, en formato YYYY-MM-DD. Extrae esto del timestamp created_at del Cliente (primeros 10 caracteres).
El método openRedirectUrl() del SDK maneja la apertura de la aplicación de la institución automáticamente. La institución redirigirá de vuelta a tu callbackUrl con parámetros OAuth.
Maneja el callback de OAuth en tu actividad y completa el registro:
// En tu MainActivity
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.data?.let { uri ->
handleEnrollmentCallback(uri)
}
}
fun handleEnrollmentCallback(uri: Uri) {
viewModelScope.launch {
try {
val enrollment = sdk.completeEnrollmentAfterRedirection(
callbackUrl = uri.toString() // SDK analiza los parámetros automáticamente
)
// Verifica si fue exitoso
when (enrollment.status) {
"PENDING" -> {
// Éxito - procede al registro FIDO
getFidoOptions(enrollment.id)
}
"FAILED" -> {
// Maneja el fallo
_error.value = "${enrollment.statusReasonCode}: ${enrollment.statusReasonMessage}"
}
}
} catch (e: Exception) {
_error.value = "Error al completar el registro: ${e.message}"
}
}
}Si prefieres analizar la URL de callback tú mismo, puedes pasar los parámetros individualmente:
val enrollment = sdk.completeEnrollmentAfterRedirection(
state = stateParam,
code = codeParam,
idToken = idTokenParam
)Recupera las opciones FIDO necesarias para el registro biométrico. El SDK realiza automáticamente la consulta durante hasta 5 minutos:
fun getFidoOptions(enrollmentId: String) {
viewModelScope.launch {
// El SDK consulta automáticamente cada 1 segundo durante hasta 5 minutos
val fidoOptions = sdk.getFidoRegistrationOptions(enrollmentId)
if (fidoOptions != null) {
// Opciones FIDO recibidas, proceder al registro biométrico
startBiometricRegistration(fidoOptions)
} else {
// El tiempo de espera de la consulta se agotó (pasaron 5 minutos)
_error.value = "Tiempo de espera agotado para las opciones FIDO. Por favor, inténtalo de nuevo."
}
}
}El método getFidoRegistrationOptions() maneja toda la lógica de consulta automáticamente. Verifica cada 1 segundo durante hasta 5 minutos, por lo que no necesitas implementar ninguna lógica de reintento.
Solicite al usuario los datos biométricos y confirme la inscripción:
class EnrollmentViewModel(
private val sdk: BiometricPixSDK
) : ViewModel() {
private var enrollmentId: String? = null
fun startBiometricRegistration(fidoOptions: FidoRegistrationOptions) {
try {
sdk.startRegistration(
fidoOptions = fidoOptions.toJsonString(),
callback = object : FidoRegistrationCallback {
override fun onSuccess(credential: PublicKeyCredential, response: AuthenticatorAttestationResponse) {
// SDK maneja la creación de payload automáticamente
confirmEnrollment(credential, response)
}
override fun onError(error: String) {
_error.value = "El registro biométrico falló: $error"
}
}
)
} catch (e: Exception) {
_error.value = "Error al iniciar el registro: ${e.message}"
}
}
private fun confirmEnrollment(
credential: PublicKeyCredential,
response: AuthenticatorAttestationResponse
) {
val enrollmentId = this.enrollmentId ?: return
viewModelScope.launch {
val success = sdk.confirmEnrollment(
enrollmentId = enrollmentId,
credential = credential,
response = response
)
if (success) {
_state.value = EnrollmentState.Success
} else {
_error.value = "La confirmación de inscripción falló"
}
}
}
}El dispositivo ahora está inscrito y listo para pagos biométricos.
Una vez inscrito, iniciar pagos requiere cuatro llamadas a métodos:
Obtén todas las inscripciones para el dispositivo actual y permite que el usuario seleccione una:
class PaymentViewModel(
private val sdk: BiometricPixSDK
) : ViewModel() {
private val _enrollments = MutableStateFlow<List<Enrollment>>(emptyList())
val enrollments: StateFlow<List<Enrollment>> = _enrollments
fun loadEnrollments(deviceId: String) {
viewModelScope.launch {
try {
_enrollments.value = sdk.listEnrollments(deviceId)
} catch (e: Exception) {
_error.value = "Failed to load enrollments: ${e.message}"
}
}
}
}Mostrar inscripciones al usuario:
@Composable
fun EnrollmentSelectionScreen(
viewModel: PaymentViewModel
) {
val enrollments by viewModel.enrollments.collectAsState()
LazyColumn {
items(enrollments) { enrollment ->
enrollment.institution?.let { institution ->
EnrollmentItem(
enrollment = enrollment,
institution = institution,
onClick = { viewModel.selectEnrollment(enrollment) }
)
}
}
}
LaunchedEffect(Unit) {
viewModel.loadEnrollments(savedDeviceId)
}
}
@Composable
fun EnrollmentItem(
enrollment: Enrollment,
institution: Institution,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp)
) {
AsyncImage(
model = institution.iconLogo,
contentDescription = institution.displayName,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(text = institution.displayName)
Text(
text = "Estado: ${enrollment.status}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}Crea una intención de pago con todos los detalles del pago:
fun createPayment(
amount: Double,
enrollmentId: String,
beneficiaryAccountId: String
) {
val payload = CreatePaymentIntentPayload(
amount = amount,
customer = Customer(identifier = userCPF), // CPF del usuario
description = "Pago por servicios",
statementDescription = "Compra ACME Corp",
allowedPaymentMethodTypes = listOf("open_finance_biometric_pix"),
paymentMethodDetails = PaymentMethodDetails(
openFinanceBiometricPix = OpenFinanceBiometricPixPaymentMethodDetails(
beneficiaryBankAccount = beneficiaryAccountId,
enrollment = enrollmentId
)
),
confirm = true
)
viewModelScope.launch {
try {
val paymentIntent = sdk.createPaymentIntent(payload)
// Guardar ID de la intención de pago
_paymentIntentId.value = paymentIntent.id
// Extraer opciones FIDO para el siguiente paso
paymentIntent.paymentMethodInformation?.openFinanceBiometricPix?.fidoOptions?.let { fidoOptions ->
promptForBiometric(fidoOptions)
}
} catch (e: Exception) {
_error.value = "Error al crear la intención de pago: ${e.message}"
}
}
}Solicitar autenticación biométrica y recopilar señales de riesgo:
class PaymentViewModel(
private val sdk: BiometricPixSDK
) : ViewModel() {
private var paymentIntentId: String? = null
private var riskSignals: RiskSignals? = null
private var assertionResponse: AssertionResponse? = null
fun promptForBiometric(fidoOptions: FidoOptions) {
try {
sdk.startSigning(
fidoOptions = fidoOptions.toJsonString(),
fallbackCredential = null, // Opcional: proporcionar si tienes uno
callback = object : FidoAuthenticationCallback {
override fun onSuccess(response: AssertionResponse) {
// Almacenar respuesta de aserción
assertionResponse = response
// Recopilar señales de riesgo
collectRiskSignals()
}
override fun onError(error: String) {
_error.value = "La autenticación biométrica falló: $error"
}
}
)
} catch (e: Exception) {
_error.value = "Error al iniciar la firma: ${e.message}"
}
}
private fun collectRiskSignals() {
try {
riskSignals = sdk.collectRiskSignals(
accountTenure = customerCreatedDate // "YYYY-MM-DD"
)
// Una vez que tenemos tanto la aserción como las señales de riesgo, autorizar el pago
if (assertionResponse != null && riskSignals != null) {
authorizePayment()
}
} catch (e: Exception) {
_error.value = "Error al recopilar señales de riesgo: ${e.message}"
}
}
}Autoriza el pago con los datos recopilados:
private fun authorizePayment() {
val paymentIntentId = this.paymentIntentId ?: return
val riskSignals = this.riskSignals ?: return
val assertion = this.assertionResponse ?: return
val payload = AuthorizePaymentIntentPayload(
platform = "android",
riskSignals = riskSignals,
assertion = assertion
)
viewModelScope.launch {
val success = sdk.authorizePaymentIntent(
paymentIntentId = paymentIntentId,
payload = payload
)
if (success) {
_state.value = PaymentState.Success
} else {
_error.value = "Payment authorization failed"
}
}
}El pago ahora está autorizado y en proceso. Necesitas monitorear eventos de webhook para rastrear su estado final.
Todos los métodos del SDK que realizan operaciones de red pueden lanzar excepciones. Asegúrate de manejarlas adecuadamente:
try {
val institutions = sdk.getPaymentInstitutions()
// Éxito
} catch (e: BiometricPixSDKException) {
when (e) {
is BiometricPixSDKException.NetworkError -> {
// Manejar problemas de red
Log.e(TAG, "Error de red: ${e.message}")
}
is BiometricPixSDKException.AuthenticationError -> {
// Manejar token inválido o expirado
Log.e(TAG, "Autenticación fallida - el token puede estar expirado")
}
is BiometricPixSDKException.InvalidParametersError -> {
// Manejar entrada inválida
Log.e(TAG, "Parámetros inválidos: ${e.message}")
}
is BiometricPixSDKException.UnknownError -> {
// Manejar errores desconocidos
Log.e(TAG, "Error: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error inesperado: ${e.message}")
}Aunque el SDK maneja la mayor parte del flujo de trabajo, aún debes escuchar las notificaciones de webhook para manejar actualizaciones asíncronas:
- Cambios en el estado de inscripción: tipo de webhook
ENROLLMENTS - Cambios en el estado de pago: tipo de webhook
PAYMENT_INTENTS
Para obtener la documentación completa de los webhooks, consulta Webhooks de Pagos (Brasil).
BiometricPixSDK(context: Context, accessToken: String)
- Crea una nueva instancia del SDK con el contexto y el access token proporcionados
- Debe inicializarse una vez y reutilizarse en toda tu aplicación
- Access token obtenido del endpoint
/payments/api/widget-token/
cleanup()
- Libera los recursos del SDK y limpia las tareas en segundo plano
- Llamar en
onCleared()o cuando el usuario cierra sesión
getPaymentInstitutions(): List<Institution>
- Obtiene todas las instituciones que admiten pagos biométricos
- Devuelve una lista de objetos
Institutionconid,displayName,iconLogo, etc. - Lanza una excepción en caso de errores de red o autenticación
createEnrollment(cpf: String, institution: String, accountTenure: String, callbackUrl: String): Enrollment
- Crea la inscripción y recopila señales de riesgo automáticamente
cpf: Número de CPF del usuarioinstitution: ID de la institución degetPaymentInstitutions()accountTenure: Fecha de creación del cliente en formato "YYYY-MM-DD"callbackUrl: Enlace profundo para la devolución de llamada OAuth (debe estar registrado como App Link)- Devuelve un objeto
Enrollmentconid,redirect_url,device_id
openRedirectUrl(context: Context, url: String)
- Abre la aplicación de la institución usando la URL de redirección proporcionada
- Maneja la redirección automáticamente, incluyendo el enlace profundo
- Debe ser llamado inmediatamente después de
createEnrollment()con elredirect_urlde la respuesta de inscripción context: Contexto de la Actividad o Aplicaciónurl: La URL de redirección del objeto de inscripción
completeEnrollmentAfterRedirection(callbackUrl: String): Enrollment
- Completa la inscripción después de la devolución de llamada OAuth de la institución
- Analiza automáticamente los parámetros OAuth de la URL completa de devolución de llamada
- Alternativa:
completeEnrollmentAfterRedirection(state: String, code: String, idToken: String)
getFidoRegistrationOptions(enrollmentId: String): FidoRegistrationOptions?
- Consulta las opciones de registro FIDO (reintento automático: intervalo de 1 segundo, tiempo de espera de 5 minutos)
- Devuelve
FidoRegistrationOptionscuando está listo - Devuelve
nullsi el tiempo de espera de la consulta se agota
startRegistration(fidoOptions: String, callback: FidoRegistrationCallback)
- Inicia el flujo de registro biométrico (reconocimiento de huella digital/rostro)
fidoOptions: Cadena JSON deFidoRegistrationOptions.toJsonString()callback: Interfaz para recibir devoluciones de llamada de éxito/error
confirmEnrollment(enrollmentId: String, credential: PublicKeyCredential, response: AuthenticatorAttestationResponse): Boolean
- Confirma la inscripción con la credencial FIDO
- Devuelve
trueen caso de éxito,falseen caso de fallo
listEnrollments(deviceId: String): List<Enrollment>
- Obtiene todas las inscripciones para un dispositivo
- Devuelve una lista de objetos
Enrollmentcon datos enriquecidos de la institución - Filtrar por
status == "SUCCEEDED"para mostrar solo inscripciones activas
createPaymentIntent(payload: CreatePaymentIntentPayload): PaymentIntent
- Crea una intención de pago
- Devuelve
PaymentIntentconidypaymentMethodInformation.openFinanceBiometricPix.fidoOptions
startSigning(fidoOptions: String, fallbackCredential: String?, callback: FidoAuthenticationCallback)
- Inicia la autenticación biométrica para el pago
fallbackCredential: Credencial opcional para escenarios de reintentocallback: Interfaz para recibir la respuesta de la afirmación
collectRiskSignals(accountTenure: String): RiskSignals
- Recoge huellas digitales del dispositivo y señales de seguridad
accountTenure: Fecha de creación del cliente en formato "YYYY-MM-DD"- Devuelve un objeto
RiskSignalspara la carga útil de autorización
authorizePaymentIntent(paymentIntentId: String, payload: AuthorizePaymentIntentPayload): Boolean
- Autoriza el pago con afirmación biométrica y señales de riesgo
- Devuelve
trueen caso de éxito,falseen caso de fallo