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.
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 iOS 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:
- iOS 15.0 or higher
- Swift 5.0 or higher
Installation via Swift Package Manager:
- In Xcode, select File → Add Packages...
- Enter:
https://github.com/belvo-finance-opensource/biometric-pix-ios-sdk - Select version requirements and click Add Package
- Choose BiometricPixSDK product and click Add Package
Add entitlements to your .entitlements file:
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:belvo.com</string>
</array>Add location usage description to Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses location for security and fraud prevention purposes.</string>Share your Team ID and Bundle Identifier with Belvo:
Format: TEAM_ID.BUNDLE_ID
Example: ABCDEFGHIJ.com.yourcompany.appnameInitialize the SDK once in your app, typically in your view model or service layer:
import BiometricPixSDK
class BiometricPixService {
private let sdk: BiometricPixSDK
init(accessToken: String) {
self.sdk = BiometricPixSDK(accessToken: accessToken)
}
deinit {
sdk.cleanup()
}
}Always call cleanup() when you're done with the SDK (e.g., in deinit or when logging out) to properly release resources.
The enrollment process registers a user's device with their institution for biometric payments.
Fetch the list of institutions that support biometric payments:
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:
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()
}
}
}Request location permissions required for risk assessment:
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()
}
}
}
}Create the enrollment with the following method call (the SDK handles risk signal collection internally):
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)")
}
}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).
Open the institution's app using the redirect_url:
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.
Handle the OAuth callback in your app and complete the enrollment:
// In your SceneDelegate or App delegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
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)")
}
}If you prefer to parse the callback URL yourself, you can pass parameters individually:
let enrollment = try 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:
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()
}
}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: 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)
}
}
}When using view models with callbacks in SwiftUI, declare them as @ObservedObject or @StateObject to prevent them from being destroyed during view re-renders:
struct EnrollmentView: View {
@StateObject private var viewModel: EnrollmentViewModel
}The device is now enrolled and ready for biometric payments.
Once enrolled, initiating payments requires four method calls (as you can see in the sequence diagram below):
Fetch all enrollments for the current device and let the user select one:
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:
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)
}
}
}Create a payment intent with all payment details:
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)")
}
}Prompt for biometric authentication and collect risk signals:
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)
}
}
}Authorize the payment with the collected data:
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")
}
}
}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:
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)")
}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(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
deinitor when user logs out
getPaymentInstitutions() throws -> [Institution]
- Fetches all institutions supporting biometric payments
- Returns array of
Institutionobjects withid,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 numberinstitution: Institution ID fromgetPaymentInstitutions()accountTenure: Customer creation date in "YYYY-MM-DD" formatcallbackUrl: Deep link for OAuth callback (must be registered in applinks)- Returns
Enrollmentobject withid,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
FidoRegistrationOptionswhen ready - Returns
nilif polling times out
startRegistration(fidoResponseString: String, callback: FidoRegistrationCallback) throws
- Initiates biometric registration flow (Face ID/Touch ID)
fidoResponseString: JSON string fromFidoRegistrationOptions.toJsonString()callback: Delegate to receive success/error callbacks
confirmEnrollment(enrollmentId: String, credential: PublicKeyCredential, response: AuthenticatorAttestationResponse) -> Bool
- Confirms enrollment with FIDO credential
- Returns
trueon success,falseon failure
listEnrollments(deviceId: String) throws -> [Enrollment]
- Fetches all enrollments for a device
- Returns array of
Enrollmentobjects with enriched institution data - Filter for
status == "SUCCEEDED"to show only active enrollments
createPaymentIntent(payload: CreatePaymentIntentPayload) throws -> PaymentIntent
- Creates a payment intent
- Returns
PaymentIntentwithidandpaymentMethodInformation.openFinanceBiometricPix.fidoOptions
startSigning(fidoResponseString: String, fallbackCredential: String?, callback: FidoAuthenticationCallback) throws
- Initiates biometric authentication for payment
fallbackCredential: Optional credential for retry scenarioscallback: 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
RiskSignalsobject for authorization payload
authorizePaymentIntent(paymentIntentId: String, payload: AuthorizePaymentIntentPayload) -> Bool
- Authorizes payment with biometric assertion and risk signals
- Returns
trueon success,falseon failure