# Pix Biometria Guide (Android SDK - Simplified Integration) Upcoming Release 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. ## Prerequisites Before starting, ensure you have: 1. **Generated your Belvo Payments API Keys** 2. **Set up Webhooks** to receive payment and enrollment status updates 3. **Generated an SDK Access Token** (see below) 4. **Installed the Belvo Android SDK** ### SDK Access Token SDK Authentication 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: ```bash POST https://api.belvo.com/payments/api/widget-token/ Authorization: Basic Content-Type: application/json ``` ```json { "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` Token Best Practices Never hardcode tokens in your app. Always generate them server-side and implement secure storage and refresh logic. ### SDK Installation **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):** ```kotlin dependencies { implementation("com.belvo:biometric-pix-core:1.0.0") } ``` **Sync your project** and you're ready to go! ### App Configuration **Add permissions to `AndroidManifest.xml`:** ```xml ``` **Add App Links support for OAuth callbacks:** ```xml ``` **Share your package name and SHA-256 certificate fingerprint with Belvo:** ```bash # 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 and SDK Initialization Permission Timing Permissions **must** be requested before SDK initialization. Additionally, the permission request can only be called from an Activity context. **Step 1: Request Permissions First** ```kotlin 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() } } } ``` Resource Management 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. ## Enrollment Flow (6 Steps) The enrollment process registers a user's device with their institution for biometric payments. ```mermaid sequenceDiagram autonumber participant User participant YourApp participant BiometricPixSDK participant BelvoAPI participant Institution Note over YourApp,BiometricPixSDK: Call 1: getPaymentInstitutions() User->>YourApp: Initiates enrollment YourApp->>BiometricPixSDK: getPaymentInstitutions() BiometricPixSDK->>BelvoAPI: Fetch institutions BelvoAPI-->>BiometricPixSDK: Institution list BiometricPixSDK-->>YourApp: List<Institution> YourApp->>User: Display institution picker User-->>YourApp: Selects institution Note over YourApp,BiometricPixSDK: Call 2: createEnrollment() + openRedirectUrl() YourApp->>BiometricPixSDK: createEnrollment(cpf, institution, accountTenure, callbackUrl) BiometricPixSDK->>BiometricPixSDK: Collect risk signals internally BiometricPixSDK->>BelvoAPI: POST /enrollments/ BelvoAPI-->>BiometricPixSDK: Enrollment created (redirect_url) BiometricPixSDK-->>YourApp: Enrollment object YourApp->>BiometricPixSDK: openRedirectUrl(context, redirect_url) BiometricPixSDK->>Institution: Open institution app User->>Institution: Approve enrollment in institution app Institution-->>YourApp: OAuth callback (code, state, id_token) Note over YourApp,BiometricPixSDK: Call 3: completeEnrollmentAfterRedirection() YourApp->>BiometricPixSDK: completeEnrollmentAfterRedirection(callbackUrl) BiometricPixSDK->>BelvoAPI: POST /enrollments/complete-redirection/ BelvoAPI-->>BiometricPixSDK: Enrollment updated BiometricPixSDK-->>YourApp: Enrollment object Note over YourApp,BiometricPixSDK: Call 4: getFidoRegistrationOptions() YourApp->>BiometricPixSDK: getFidoRegistrationOptions(enrollmentId) BiometricPixSDK->>BelvoAPI: Poll for FIDO options (auto-retry) BelvoAPI-->>BiometricPixSDK: FIDO registration options BiometricPixSDK-->>YourApp: FidoRegistrationOptions Note over YourApp,BiometricPixSDK: Call 5: startRegistration() YourApp->>BiometricPixSDK: startRegistration(fidoOptions, callback) BiometricPixSDK->>User: Request biometric (fingerprint/face) User-->>BiometricPixSDK: Provides biometric BiometricPixSDK-->>YourApp: Credential via callback Note over YourApp,BiometricPixSDK: Call 6: confirmEnrollment() YourApp->>BiometricPixSDK: confirmEnrollment(enrollmentId, credential) BiometricPixSDK->>BelvoAPI: POST /enrollments/{id}/confirm/ BelvoAPI->>Institution: Register FIDO credential Institution-->>BelvoAPI: Registration confirmed BelvoAPI-->>BiometricPixSDK: Enrollment SUCCEEDED BiometricPixSDK-->>YourApp: Success (Boolean) YourApp->>User: Show success screen ``` ### Step 1: Get Payment Institutions Fetch the list of institutions that support biometric payments: ```kotlin 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>(emptyList()) val institutions: StateFlow> = _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:** ```kotlin @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) } } ``` ### Step 2: Create Enrollment and Open Institution App Create the enrollment and immediately open the institution app: ```kotlin 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}" } } } ``` Account Tenure Format 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. ### Step 5: Complete Enrollment After Redirection Handle the OAuth callback in your activity and complete the enrollment: ```kotlin // 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}" } } } ``` Alternative: Manual Parameters If you prefer to parse the callback URL yourself, you can pass parameters individually: ```kotlin val enrollment = sdk.completeEnrollmentAfterRedirection( state = stateParam, code = codeParam, idToken = idTokenParam ) ``` ### Step 4: Get FIDO Registration Options Retrieve the FIDO options needed for biometric registration. The SDK automatically polls for up to 5 minutes: ```kotlin 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." } } } ``` Automatic Polling 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. ### Step 5: Register Biometric and Confirm Prompt the user for biometric data and confirm the enrollment: ```kotlin 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" } } } } ``` Enrollment Complete! The device is now enrolled and ready for biometric payments. ## Payment Flow (4 Steps) Once enrolled, initiating payments requires four method calls: ```mermaid sequenceDiagram autonumber participant User participant YourApp participant BiometricPixSDK participant BelvoAPI participant Institution Note over YourApp,BiometricPixSDK: Call 1: listEnrollments() User->>YourApp: Initiates payment YourApp->>BiometricPixSDK: listEnrollments(deviceId) BiometricPixSDK->>BelvoAPI: GET /enrollments/?device_id=... BelvoAPI-->>BiometricPixSDK: List<Enrollment> BiometricPixSDK-->>YourApp: Enrollment list YourApp->>User: Display enrollment picker User-->>YourApp: Selects enrollment Note over YourApp,BiometricPixSDK: Call 2: createPaymentIntent() YourApp->>BiometricPixSDK: createPaymentIntent(payload) BiometricPixSDK->>BelvoAPI: POST /payment-intents/ BelvoAPI-->>BiometricPixSDK: PaymentIntent (with FIDO options) BiometricPixSDK-->>YourApp: PaymentIntent object Note over YourApp,BiometricPixSDK: Call 3: startSigning() + collectRiskSignals() YourApp->>BiometricPixSDK: startSigning(fidoOptions, callback) BiometricPixSDK->>User: Request biometric (fingerprint/face) User-->>BiometricPixSDK: Provides biometric BiometricPixSDK-->>YourApp: Assertion via callback YourApp->>BiometricPixSDK: collectRiskSignals(accountTenure) BiometricPixSDK-->>YourApp: RiskSignals Note over YourApp,BiometricPixSDK: Call 4: authorizePaymentIntent() YourApp->>BiometricPixSDK: authorizePaymentIntent(paymentIntentId, payload) BiometricPixSDK->>BelvoAPI: POST /payment-intents/{id}/authorize/ BelvoAPI->>Institution: Process payment Institution-->>BelvoAPI: Payment confirmed BelvoAPI-->>BiometricPixSDK: Payment SUCCEEDED BiometricPixSDK-->>YourApp: Authorization success (Boolean) YourApp->>User: Show payment confirmation ``` ### Step 1: List Enrollments Fetch all enrollments for the current device and let the user select one: ```kotlin class PaymentViewModel( private val sdk: BiometricPixSDK ) : ViewModel() { private val _enrollments = MutableStateFlow>(emptyList()) val enrollments: StateFlow> = _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:** ```kotlin @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 ) } } } ``` ### Step 2: Create Payment Intent Create a payment intent with all payment details: ```kotlin 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}" } } } ``` ### Step 3: Collect Biometric and Risk Signals Prompt for biometric authentication and collect risk signals: ```kotlin 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}" } } } ``` ### Step 4: Authorize Payment Authorize the payment with the collected data: ```kotlin 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" } } } ``` Payment Flow Complete! The payment is now authorized and processing. You need to monitor webhook events to track its final status. ## Error Handling All SDK methods that perform network operations can throw exceptions. Make sure to handle them appropriately: ```kotlin 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}") } ``` ## Webhooks While the SDK handles most of the workflow, you should still listen for webhook notifications to handle async updates: - **Enrollment status changes:** `ENROLLMENTS` webhook type - **Payment status changes:** `PAYMENT_INTENTS` webhook type For complete webhook documentation, see Payments Webhooks (Brazil). ## SDK Method Reference ### Initialization **`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 ### Enrollment Methods **`getPaymentInstitutions(): List`** - Fetches all institutions supporting biometric payments - Returns list of `Institution` objects with `id`, `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 number - `institution`: Institution ID from `getPaymentInstitutions()` - `accountTenure`: Customer creation date in "YYYY-MM-DD" format - `callbackUrl`: Deep link for OAuth callback (must be registered as App Link) - Returns `Enrollment` object with `id`, `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 the `redirect_url` from the enrollment response - `context`: Activity or Application context - `url`: 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 `FidoRegistrationOptions` when ready - Returns `null` if polling times out **`startRegistration(fidoOptions: String, callback: FidoRegistrationCallback)`** - Initiates biometric registration flow (fingerprint/face recognition) - `fidoOptions`: JSON string from `FidoRegistrationOptions.toJsonString()` - `callback`: Interface to receive success/error callbacks **`confirmEnrollment(enrollmentId: String, credential: PublicKeyCredential, response: AuthenticatorAttestationResponse): Boolean`** - Confirms enrollment with FIDO credential - Returns `true` on success, `false` on failure ### Payment Methods **`listEnrollments(deviceId: String): List`** - Fetches all enrollments for a device - Returns list of `Enrollment` objects with enriched institution data - Filter for `status == "SUCCEEDED"` to show only active enrollments **`createPaymentIntent(payload: CreatePaymentIntentPayload): PaymentIntent`** - Creates a payment intent - Returns `PaymentIntent` with `id` and `paymentMethodInformation.openFinanceBiometricPix.fidoOptions` **`startSigning(fidoOptions: String, fallbackCredential: String?, callback: FidoAuthenticationCallback)`** - Initiates biometric authentication for payment - `fallbackCredential`: Optional credential for retry scenarios - `callback`: 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 `RiskSignals` object for authorization payload **`authorizePaymentIntent(paymentIntentId: String, payload: AuthorizePaymentIntentPayload): Boolean`** - Authorizes payment with biometric assertion and risk signals - Returns `true` on success, `false` on failure