This documentation covers features from our upcoming release. While the core functionality and workflow described here will remain unchanged, you may notice some refinements before the final release.
With Belvo's Pix Biometria, collecting payments from users becomes seamless, removing the need for users to navigate to their financial institution to approve each individual payment request.
This guide demonstrates integration using the Belvo Android SDK convenience methods. The SDK handles backend communication, OAuth flows, and FIDO registration internally, providing a streamlined integration path.
The first step in enabling biometric payment collection is to enroll the user’s device with their institution. During enrollment, key data about the device and the user's public key credentials is securely registered with their institution, ensuring that future payments can be confirmed using biometric authentication alone.
Once enrollment is complete, you can start requesting payments directly from the user’s device.
Before starting, ensure you have:
- Generated your Belvo Payments API Keys
- Set up Webhooks to receive payment and enrollment status updates
- Generated an SDK Access Token (see below)
- Installed the Belvo Android SDK
The Biometric Pix SDK requires an access token to authenticate API requests. Generate this token from your backend server and pass it to the SDK during initialization.
Generate an SDK access token from your 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..."
}Never hardcode tokens in your app. Always generate them server-side and implement secure storage and refresh logic.
Minimum Requirements:
- Android API Level 24 (Android 7.0) or higher
- Kotlin 1.9.0 or higher
Add to your build.gradle.kts (Module level):
dependencies {
implementation("com.belvo:biometric-pix-core:1.0.0")
}Sync your project and you're ready to go!
Add permissions to 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" />Add App Links support for OAuth callbacks:
<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>Share your package name and SHA-256 certificate fingerprint with Belvo:
# Get your SHA-256 fingerprint
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# Format to share with Belvo:
# Package: com.yourcompany.appname
# SHA-256: AA:BB:CC:DD:EE:FF:...Permissions must be requested before SDK initialization. Additionally, the permission request can only be called from an Activity context.
Step 1: Request Permissions First
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)
// MUST request permissions BEFORE initializing SDK
requestPermissions()
}
private fun requestPermissions() {
BiometricPixSDK.requestPermission(this) { granted ->
if (granted) {
// Permissions granted, now initialize SDK
initializeSDK()
} else {
// Handle permission denial
showError("Location permission is required for enrollment")
}
}
}
private fun initializeSDK() {
// Initialize AFTER permissions are granted
sdk = BiometricPixSDK(
context = this,
accessToken = getAccessToken() // From your backend
)
// Now ready to proceed with enrollment
}
override fun onDestroy() {
super.onDestroy()
if (::sdk.isInitialized) {
sdk.cleanup()
}
}
}Always call cleanup() when you're done with the SDK (e.g., when the activity is destroyed or user logs out) to properly release resources and clean up background tasks.
The enrollment process registers a user's device with their institution for biometric payments.
Fetch the list of institutions that support biometric payments:
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) {
// Handle error (network, authentication, etc.)
_error.value = "Failed to load institutions: ${e.message}"
}
}
}
}Display institutions to user:
@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)
}
}Create the enrollment and immediately open the institution app:
fun startEnrollment() {
val institution = _selectedInstitution.value ?: return
viewModelScope.launch {
try {
// Create enrollment
val enrollment = sdk.createEnrollment(
cpf = userCPF, // User's CPF
institution = institution.id, // Selected institution ID
accountTenure = customerCreatedDate, // "YYYY-MM-DD" format
callbackUrl = "https://yourdomain.com/callback"
)
// Save enrollment ID and device ID for later
_enrollmentId.value = enrollment.id
_deviceId.value = enrollment.details.riskSignals.deviceId
// Immediately open institution app using the redirect URL
enrollment.details.redirectUrl?.let { url ->
sdk.openRedirectUrl(context, url)
} ?: run {
_error.value = "No redirect URL received from enrollment"
}
} catch (e: Exception) {
_error.value = "Enrollment creation failed: ${e.message}"
}
}
}The accountTenure parameter should be the date when the user was created as a Belvo Customer, in YYYY-MM-DD format. Extract this from the Customer's created_at timestamp (first 10 characters).
The SDK's openRedirectUrl() method handles opening the institution app automatically. The institution will redirect back to your callbackUrl with OAuth parameters.
Handle the OAuth callback in your activity and complete the enrollment:
// In your 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 parses parameters automatically
)
// Check if successful
when (enrollment.status) {
"PENDING" -> {
// Success - proceed to FIDO registration
getFidoOptions(enrollment.id)
}
"FAILED" -> {
// Handle failure
_error.value = "${enrollment.statusReasonCode}: ${enrollment.statusReasonMessage}"
}
}
} catch (e: Exception) {
_error.value = "Failed to complete enrollment: ${e.message}"
}
}
}If you prefer to parse the callback URL yourself, you can pass parameters individually:
val enrollment = sdk.completeEnrollmentAfterRedirection(
state = stateParam,
code = codeParam,
idToken = idTokenParam
)Retrieve the FIDO options needed for biometric registration. The SDK automatically polls for up to 5 minutes:
fun getFidoOptions(enrollmentId: String) {
viewModelScope.launch {
// SDK polls automatically every 1 second for up to 5 minutes
val fidoOptions = sdk.getFidoRegistrationOptions(enrollmentId)
if (fidoOptions != null) {
// FIDO options received, proceed to biometric registration
startBiometricRegistration(fidoOptions)
} else {
// Polling timed out (5 minutes passed)
_error.value = "Timeout waiting for FIDO options. Please try again."
}
}
}The getFidoRegistrationOptions() method handles all polling logic automatically. It checks every 1 second for up to 5 minutes, so you don't need to implement any retry logic.
Prompt the user for biometric data and confirm the enrollment:
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 handles payload creation automatically
confirmEnrollment(credential, response)
}
override fun onError(error: String) {
_error.value = "Biometric registration failed: $error"
}
}
)
} catch (e: Exception) {
_error.value = "Failed to start registration: ${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 = "Enrollment confirmation failed"
}
}
}
}The device is now enrolled and ready for biometric payments.
Once enrolled, initiating payments requires four method calls:
Fetch all enrollments for the current device and let the user select one:
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}"
}
}
}
}Display enrollments to user:
@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 = "Status: ${enrollment.status}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}Create a payment intent with all payment details:
fun createPayment(
amount: Double,
enrollmentId: String,
beneficiaryAccountId: String
) {
val payload = CreatePaymentIntentPayload(
amount = amount,
customer = Customer(identifier = userCPF), // User's CPF
description = "Payment for services",
statementDescription = "ACME Corp Purchase",
allowedPaymentMethodTypes = listOf("open_finance_biometric_pix"),
paymentMethodDetails = PaymentMethodDetails(
openFinanceBiometricPix = OpenFinanceBiometricPixPaymentMethodDetails(
beneficiaryBankAccount = beneficiaryAccountId,
enrollment = enrollmentId
)
),
confirm = true
)
viewModelScope.launch {
try {
val paymentIntent = sdk.createPaymentIntent(payload)
// Save payment intent ID
_paymentIntentId.value = paymentIntent.id
// Extract FIDO options for next step
paymentIntent.paymentMethodInformation?.openFinanceBiometricPix?.fidoOptions?.let { fidoOptions ->
promptForBiometric(fidoOptions)
}
} catch (e: Exception) {
_error.value = "Failed to create payment intent: ${e.message}"
}
}
}Prompt for biometric authentication and collect risk signals:
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, // Optional: provide if you have one
callback = object : FidoAuthenticationCallback {
override fun onSuccess(response: AssertionResponse) {
// Store assertion response
assertionResponse = response
// Collect risk signals
collectRiskSignals()
}
override fun onError(error: String) {
_error.value = "Biometric authentication failed: $error"
}
}
)
} catch (e: Exception) {
_error.value = "Failed to start signing: ${e.message}"
}
}
private fun collectRiskSignals() {
try {
riskSignals = sdk.collectRiskSignals(
accountTenure = customerCreatedDate // "YYYY-MM-DD"
)
// Once we have both assertion and risk signals, authorize payment
if (assertionResponse != null && riskSignals != null) {
authorizePayment()
}
} catch (e: Exception) {
_error.value = "Failed to collect risk signals: ${e.message}"
}
}
}Authorize the payment with the collected data:
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"
}
}
}The payment is now authorized and processing. You need to monitor webhook events to track its final status.
All SDK methods that perform network operations can throw exceptions. Make sure to handle them appropriately:
try {
val institutions = sdk.getPaymentInstitutions()
// Success
} catch (e: BiometricPixSDKException) {
when (e) {
is BiometricPixSDKException.NetworkError -> {
// Handle network issues
Log.e(TAG, "Network error: ${e.message}")
}
is BiometricPixSDKException.AuthenticationError -> {
// Handle invalid or expired token
Log.e(TAG, "Authentication failed - token may be expired")
}
is BiometricPixSDKException.InvalidParametersError -> {
// Handle invalid input
Log.e(TAG, "Invalid parameters: ${e.message}")
}
is BiometricPixSDKException.UnknownError -> {
// Handle unknown errors
Log.e(TAG, "Error: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Unexpected error: ${e.message}")
}While the SDK handles most of the workflow, you should still listen for webhook notifications to handle async updates:
- Enrollment status changes:
ENROLLMENTSwebhook type - Payment status changes:
PAYMENT_INTENTSwebhook type
For complete webhook documentation, see Payments Webhooks (Brazil).
BiometricPixSDK(context: Context, accessToken: String)
- Creates a new SDK instance with the provided context and access token
- Should be initialized once and reused throughout your app
- Access token obtained from
/payments/api/widget-token/endpoint
cleanup()
- Releases SDK resources and cleans up background tasks
- Call in
onCleared()or when user logs out
getPaymentInstitutions(): List<Institution>
- Fetches all institutions supporting biometric payments
- Returns list of
Institutionobjects withid,displayName,iconLogo, etc. - Throws on network or authentication errors
createEnrollment(cpf: String, institution: String, accountTenure: String, callbackUrl: String): Enrollment
- Creates enrollment and collects risk signals automatically
cpf: User's CPF numberinstitution: Institution ID fromgetPaymentInstitutions()accountTenure: Customer creation date in "YYYY-MM-DD" formatcallbackUrl: Deep link for OAuth callback (must be registered as App Link)- Returns
Enrollmentobject withid,redirect_url,device_id
openRedirectUrl(context: Context, url: String)
- Opens the institution app using the provided redirect URL
- Handles the redirect automatically, including deep linking
- Should be called immediately after
createEnrollment()with theredirect_urlfrom the enrollment response context: Activity or Application contexturl: The redirect URL from the enrollment object
completeEnrollmentAfterRedirection(callbackUrl: String): Enrollment
- Completes enrollment after institution OAuth callback
- Parses OAuth parameters automatically from full callback URL
- Alternative:
completeEnrollmentAfterRedirection(state: String, code: String, idToken: String)
getFidoRegistrationOptions(enrollmentId: String): FidoRegistrationOptions?
- Polls for FIDO options (automatic retry: 1 second interval, 5 minute timeout)
- Returns
FidoRegistrationOptionswhen ready - Returns
nullif polling times out
startRegistration(fidoOptions: String, callback: FidoRegistrationCallback)
- Initiates biometric registration flow (fingerprint/face recognition)
fidoOptions: JSON string fromFidoRegistrationOptions.toJsonString()callback: Interface to receive success/error callbacks
confirmEnrollment(enrollmentId: String, credential: PublicKeyCredential, response: AuthenticatorAttestationResponse): Boolean
- Confirms enrollment with FIDO credential
- Returns
trueon success,falseon failure
listEnrollments(deviceId: String): List<Enrollment>
- Fetches all enrollments for a device
- Returns list of
Enrollmentobjects with enriched institution data - Filter for
status == "SUCCEEDED"to show only active enrollments
createPaymentIntent(payload: CreatePaymentIntentPayload): PaymentIntent
- Creates a payment intent
- Returns
PaymentIntentwithidandpaymentMethodInformation.openFinanceBiometricPix.fidoOptions
startSigning(fidoOptions: String, fallbackCredential: String?, callback: FidoAuthenticationCallback)
- Initiates biometric authentication for payment
fallbackCredential: Optional credential for retry scenarioscallback: Interface to receive assertion response
collectRiskSignals(accountTenure: String): RiskSignals
- Collects device fingerprinting and security signals
accountTenure: Customer creation date in "YYYY-MM-DD" format- Returns
RiskSignalsobject for authorization payload
authorizePaymentIntent(paymentIntentId: String, payload: AuthorizePaymentIntentPayload): Boolean
- Authorizes payment with biometric assertion and risk signals
- Returns
trueon success,falseon failure