# Pix Biometria Guide (iOS SDK-Only 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 iOS 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 iOS 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:** - iOS 15.0 or higher - Swift 5.0 or higher **Installation via Swift Package Manager:** 1. In Xcode, select **File** → **Add Packages...** 2. Enter: `https://github.com/belvo-finance-opensource/biometric-pix-ios-sdk` 3. Select version requirements and click **Add Package** 4. Choose **BiometricPixSDK** product and click **Add Package** ### App Configuration **Add entitlements to your `.entitlements` file:** ```xml com.apple.developer.associated-domains webcredentials:belvo.com ``` **Add location usage description to `Info.plist`:** ```xml NSLocationWhenInUseUsageDescription This app uses location for security and fraud prevention purposes. ``` **Share your Team ID and Bundle Identifier with Belvo:** ``` Format: TEAM_ID.BUNDLE_ID Example: ABCDEFGHIJ.com.yourcompany.appname ``` ## SDK Initialization Initialize the SDK once in your app, typically in your view model or service layer: ```swift import BiometricPixSDK class BiometricPixService { private let sdk: BiometricPixSDK init(accessToken: String) { self.sdk = BiometricPixSDK(accessToken: accessToken) } deinit { sdk.cleanup() } } ``` Resource Management Always call `cleanup()` when you're done with the SDK (e.g., in `deinit` or when logging out) to properly release resources. ## Enrollment Flow (7 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: [Institution] YourApp->>User: Display institution picker User-->>YourApp: Selects institution Note over YourApp,BiometricPixSDK: Call 2: requestPermission() YourApp->>BiometricPixSDK: requestPermission() BiometricPixSDK->>User: Request location permission User-->>BiometricPixSDK: Grants permission BiometricPixSDK-->>YourApp: Permission granted Note over YourApp,BiometricPixSDK: Call 3: createEnrollment() 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 Note over YourApp,Institution: Redirect to Institution YourApp->>User: Redirect to institution (redirect_url) User->>Institution: Approve enrollment in institution app Institution-->>YourApp: OAuth callback (code, state, id_token) Note over YourApp,BiometricPixSDK: Call 4: completeEnrollmentAfterRedirection() YourApp->>BiometricPixSDK: completeEnrollmentAfterRedirection(callbackUrl) BiometricPixSDK->>BelvoAPI: POST /enrollments/complete-redirection/ BelvoAPI-->>BiometricPixSDK: Enrollment updated BiometricPixSDK-->>YourApp: Enrollment object Note over YourApp,BiometricPixSDK: Call 5: 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 6: startRegistration() YourApp->>BiometricPixSDK: startRegistration(fidoOptions, callback) BiometricPixSDK->>User: Request biometric (Face ID/Touch ID) User-->>BiometricPixSDK: Provides biometric BiometricPixSDK-->>YourApp: Credential via callback Note over YourApp,BiometricPixSDK: Call 7: 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: ```swift import BiometricPixSDK class EnrollmentViewModel: ObservableObject { private let sdk: BiometricPixSDK @Published var institutions: [Institution] = [] @Published var selectedInstitution: Institution? init(sdk: BiometricPixSDK) { self.sdk = sdk } func loadInstitutions() { do { institutions = try sdk.getPaymentInstitutions() } catch { // Handle error (network, authentication, etc.) print("Failed to load institutions: \(error)") } } } ``` **Display institutions to user:** ```swift struct InstitutionPickerView: View { @ObservedObject var viewModel: EnrollmentViewModel var body: some View { List(viewModel.institutions) { institution in Button(action: { viewModel.selectedInstitution = institution }) { HStack { AsyncImage(url: URL(string: institution.iconLogo)) .frame(width: 40, height: 40) Text(institution.displayName) } } } .onAppear { viewModel.loadInstitutions() } } } ``` ### Step 2: Request Permissions Request location permissions required for risk assessment: ```swift func requestPermissions() { sdk.requestPermission { granted in DispatchQueue.main.async { if let permissionGranted = granted as? Bool, permissionGranted { // Permissions granted, proceed to enrollment self.startEnrollment() } else { // Handle permission denial self.showPermissionDeniedAlert() } } } } ``` ### Step 3: Create Enrollment Create the enrollment with the following method call (the SDK handles risk signal collection internally): ```swift func startEnrollment() { guard let institution = selectedInstitution else { return } do { let enrollment = try sdk.createEnrollment( cpf: userCPF, // User's CPF institution: institution.id, // Selected institution ID accountTenure: customerCreatedDate, // "YYYY-MM-DD" format callbackUrl: "https://myapp.com/callback" ) // Save enrollment ID and device ID for later self.enrollmentId = enrollment.id self.deviceId = enrollment.details.riskSignals.deviceId // Redirect user to institution if let redirectUrl = enrollment.details.redirectUrl { self.openInstitutionApp(url: redirectUrl) } } catch { // Handle error print("Enrollment creation failed: \(error)") } } ``` 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). ### Step 4: Redirect to Institution Open the institution's app using the `redirect_url`: ```swift func openInstitutionApp(url: String) { guard let url = URL(string: url) else { return } if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } ``` The institution will redirect back to your `callbackUrl` with OAuth parameters. ### Step 5: Complete Enrollment After Redirection Handle the OAuth callback in your app and complete the enrollment: ```swift // In your SceneDelegate or App delegate func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let url = URLContexts.first?.url else { return } // Pass the full callback URL to the SDK handleEnrollmentCallback(url: url) } func handleEnrollmentCallback(url: URL) { do { let enrollment = try sdk.completeEnrollmentAfterRedirection( callbackUrl: url.absoluteString // SDK parses parameters automatically ) // Check if successful if enrollment.status == "PENDING" { // Success - proceed to FIDO registration self.getFidoOptions(enrollmentId: enrollment.id) } else if enrollment.status == "FAILED" { // Handle failure self.showEnrollmentError( code: enrollment.statusReasonCode, message: enrollment.statusReasonMessage ) } } catch { print("Failed to complete enrollment: \(error)") } } ``` Alternative: Manual Parameters If you prefer to parse the callback URL yourself, you can pass parameters individually: ```swift let enrollment = try sdk.completeEnrollmentAfterRedirection( state: stateParam, code: codeParam, idToken: idTokenParam ) ``` ### Step 6: Get FIDO Registration Options Retrieve the FIDO options needed for biometric registration. The SDK automatically polls for up to 5 minutes: ```swift func getFidoOptions(enrollmentId: String) { // SDK polls automatically every 1 second for up to 5 minutes if let fidoOptions = sdk.getFidoRegistrationOptions(enrollmentId: enrollmentId) { // FIDO options received, proceed to biometric registration self.startBiometricRegistration(fidoOptions: fidoOptions) } else { // Polling timed out (5 minutes passed) self.showTimeoutError() } } ``` 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 7: Register Biometric and Confirm Prompt the user for biometric data and confirm the enrollment: ```swift class EnrollmentViewModel: NSObject, ObservableObject { private let sdk: BiometricPixSDK private var enrollmentId: String? func startBiometricRegistration(fidoOptions: FidoRegistrationOptions) { do { try sdk.startRegistration( fidoResponseString: fidoOptions.toJsonString(), callback: self ) } catch { print("Failed to start registration: \(error)") } } } // Implement FIDO callback extension EnrollmentViewModel: FidoRegistrationCallback { func onSuccess(credential: PublicKeyCredential, response: AuthenticatorAttestationResponse) { // SDK handles payload creation automatically guard let enrollmentId = self.enrollmentId else { return } let success = sdk.confirmEnrollment( enrollmentId: enrollmentId, credential: credential, response: response ) if success { DispatchQueue.main.async { self.showEnrollmentSuccess() } } else { DispatchQueue.main.async { self.showEnrollmentError() } } } func onError(error: String) { DispatchQueue.main.async { self.showBiometricError(message: error) } } } ``` View Model Lifecycle When using view models with callbacks in SwiftUI, declare them as `@ObservedObject` or `@StateObject` to prevent them from being destroyed during view re-renders: ```swift struct EnrollmentView: View { @StateObject private var viewModel: EnrollmentViewModel } ``` Enrollment Complete! The device is now enrolled and ready for biometric payments. ## Payment Flow (4 Steps) Once enrolled, initiating payments requires four method calls (as you can see in the sequence diagram below): ```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: [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 (Face ID/Touch ID) 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: ```swift class PaymentViewModel: ObservableObject { private let sdk: BiometricPixSDK @Published var enrollments: [Enrollment] = [] @Published var selectedEnrollment: Enrollment? func loadEnrollments(deviceId: String) { do { enrollments = try sdk.listEnrollments(deviceId: deviceId) } catch { print("Failed to load enrollments: \(error)") } } } ``` **Display enrollments to user:** ```swift struct EnrollmentSelectionView: View { @ObservedObject var viewModel: PaymentViewModel var body: some View { List(viewModel.enrollments) { enrollment in Button(action: { viewModel.selectedEnrollment = enrollment }) { HStack { if let institution = enrollment.institution { AsyncImage(url: URL(string: institution.iconLogo)) .frame(width: 40, height: 40) VStack(alignment: .leading) { Text(institution.displayName) Text("Status: \(enrollment.status)") .font(.caption) .foregroundColor(.gray) } } } } } .onAppear { viewModel.loadEnrollments(deviceId: savedDeviceId) } } } ``` ### Step 2: Create Payment Intent Create a payment intent with all payment details: ```swift func createPayment(amount: Double, enrollmentId: String, beneficiaryAccountId: String) { let payload = CreatePaymentIntentPayload( amount: amount, customer: Customer(identifier: userCPF), // User's CPF description: "Payment for services", statementDescription: "ACME Corp Purchase", allowedPaymentMethodTypes: ["open_finance_biometric_pix"], paymentMethodDetails: PaymentMethodDetails( openFinanceBiometricPix: OpenFinanceBiometricPixPaymentMethodDetails( beneficiaryBankAccount: beneficiaryAccountId, enrollment: enrollmentId ) ), confirm: true ) do { let paymentIntent = try sdk.createPaymentIntent(payload: payload) // Save payment intent ID self.paymentIntentId = paymentIntent.id // Extract FIDO options for next step if let fidoOptions = paymentIntent.paymentMethodInformation?.openFinanceBiometricPix?.fidoOptions { self.promptForBiometric(fidoOptions: fidoOptions) } } catch { print("Failed to create payment intent: \(error)") } } ``` ### Step 3: Collect Biometric and Risk Signals Prompt for biometric authentication and collect risk signals: ```swift class PaymentViewModel: NSObject, ObservableObject { private let sdk: BiometricPixSDK private var paymentIntentId: String? private var riskSignals: RiskSignals? private var assertionResponse: AssertionResponse? func promptForBiometric(fidoOptions: FidoOptions) { // Convert FIDO options to JSON string let fidoJsonString = fidoOptions.toJsonString() do { try sdk.startSigning( fidoResponseString: fidoJsonString, fallbackCredential: nil, // Optional: provide if you have one callback: self ) } catch { print("Failed to start signing: \(error)") } } func collectRiskSignals() { do { self.riskSignals = try sdk.collectRiskSignals( accountTenure: customerCreatedDate // "YYYY-MM-DD" ) // Once we have both assertion and risk signals, authorize payment if assertionResponse != nil && riskSignals != nil { self.authorizePayment() } } catch { print("Failed to collect risk signals: \(error)") } } } // Implement FIDO authentication callback extension PaymentViewModel: FidoAuthenticationCallback { func onSuccess(response: AssertionResponse) { // Store assertion response self.assertionResponse = response // Collect risk signals self.collectRiskSignals() } func onError(error: String) { DispatchQueue.main.async { self.showPaymentError(message: error) } } } ``` ### Step 4: Authorize Payment Authorize the payment with the collected data: ```swift func authorizePayment() { guard let paymentIntentId = self.paymentIntentId, let riskSignals = self.riskSignals, let assertion = self.assertionResponse else { return } let payload = AuthorizePaymentIntentPayload( platform: "ios", riskSignals: riskSignals, assertion: assertion ) let success = sdk.authorizePaymentIntent( paymentIntentId: paymentIntentId, payload: payload ) DispatchQueue.main.async { if success { self.showPaymentSuccess() } else { self.showPaymentError(message: "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: ```swift do { let institutions = try sdk.getPaymentInstitutions() // Success } catch let error as BiometricPixSDKError { switch error { case .networkError(let message): // Handle network issues print("Network error: \(message)") case .authenticationError: // Handle invalid or expired token print("Authentication failed - token may be expired") case .invalidParameters(let message): // Handle invalid input print("Invalid parameters: \(message)") case .unknown(let message): // Handle unknown errors print("Error: \(message)") } } catch { print("Unexpected error: \(error)") } ``` ## 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(accessToken: String)`** - Creates a new SDK instance with the provided access token - Should be initialized once and reused throughout your app - Access token obtained from `/payments/api/widget-token/` endpoint **`cleanup()`** - Releases SDK resources - Call in `deinit` or when user logs out ### Enrollment Methods **`getPaymentInstitutions() throws -> [Institution]`** - Fetches all institutions supporting biometric payments - Returns array of `Institution` objects with `id`, `displayName`, `iconLogo`, etc. - Throws on network or authentication errors **`createEnrollment(cpf: String, institution: String, accountTenure: String, callbackUrl: String) throws -> 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 in applinks) - Returns `Enrollment` object with `id`, `redirect_url`, `device_id` **`completeEnrollmentAfterRedirection(callbackUrl: String) throws -> 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 `nil` if polling times out **`startRegistration(fidoResponseString: String, callback: FidoRegistrationCallback) throws`** - Initiates biometric registration flow (Face ID/Touch ID) - `fidoResponseString`: JSON string from `FidoRegistrationOptions.toJsonString()` - `callback`: Delegate to receive success/error callbacks **`confirmEnrollment(enrollmentId: String, credential: PublicKeyCredential, response: AuthenticatorAttestationResponse) -> Bool`** - Confirms enrollment with FIDO credential - Returns `true` on success, `false` on failure ### Payment Methods **`listEnrollments(deviceId: String) throws -> [Enrollment]`** - Fetches all enrollments for a device - Returns array of `Enrollment` objects with enriched institution data - Filter for `status == "SUCCEEDED"` to show only active enrollments **`createPaymentIntent(payload: CreatePaymentIntentPayload) throws -> PaymentIntent`** - Creates a payment intent - Returns `PaymentIntent` with `id` and `paymentMethodInformation.openFinanceBiometricPix.fidoOptions` **`startSigning(fidoResponseString: String, fallbackCredential: String?, callback: FidoAuthenticationCallback) throws`** - Initiates biometric authentication for payment - `fallbackCredential`: Optional credential for retry scenarios - `callback`: Delegate to receive assertion response **`collectRiskSignals(accountTenure: String) throws -> 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) -> Bool`** - Authorizes payment with biometric assertion and risk signals - Returns `true` on success, `false` on failure