Reducing Friction in Fintech User Onboarding
Strategies and technical approaches for streamlining fintech user onboarding, from identity verification and KYC to progressive profiling, while balancing regulatory compliance with conversion optimization.
In most mobile apps, onboarding is a design challenge: how quickly can you get the user to the "aha moment" where they understand the product's value? In fintech, onboarding is a design challenge compounded by a regulatory one. Before a user can open an account, view a balance, or move money, you are legally required to verify their identity, screen them against sanctions lists, assess their risk profile, and obtain their consent to a stack of regulatory disclosures. These requirements are non-negotiable — you cannot remove them, simplify them beyond a certain point, or defer them indefinitely.
What you can do is execute them with precision, minimizing every unnecessary second of friction while maintaining full compliance. At Klivvr, we have iterated through six major versions of our onboarding flow. Each iteration reduced abandonment by measurable percentages, and the cumulative effect has been transformative: our onboarding completion rate has increased from 38% in version one to 72% in the current version. This article shares the strategies, technical implementations, and lessons behind that improvement.
Measuring the Onboarding Funnel
Before optimizing onboarding, you need to measure it with precision. A single metric — "onboarding completion rate" — is insufficient. You need step-level conversion data to identify exactly where users drop off.
Our onboarding funnel has seven steps, each tracked as a distinct analytics event:
- App download and launch (100% — this is the baseline)
- Phone number entry (87% proceed)
- OTP verification (82% proceed)
- Personal information (74% proceed)
- Identity document capture (65% proceed)
- Selfie / liveness verification (60% proceed)
- Account created (72% of those who started step 2, accounting for re-engagement)
Each transition has a measured drop-off rate, a hypothesized cause, and a tested intervention. For example, the drop from step 4 to step 5 (personal information to document capture) was our largest single point of attrition. User research revealed two primary causes: users did not have their ID document at hand, and users were uncomfortable photographing their government ID within an app.
Addressing these two concerns drove much of our subsequent optimization work.
Progressive Profiling: Ask Only What You Need, When You Need It
Traditional fintech onboarding front-loads every requirement: collect all personal information, verify identity, and complete KYC before the user sees a single screen of the product. This approach maximizes regulatory completeness but also maximizes drop-off. The user is asked to invest significant effort before receiving any value.
Progressive profiling restructures this contract. Instead of asking for everything upfront, you ask for the minimum required at each stage and defer additional collection until it is genuinely needed.
Our tiered approach works as follows:
Tier 0 — Exploration (no data required). Before creating an account, users can browse the app's feature descriptions, view example screens, and explore the interface. This costs nothing and builds familiarity.
Tier 1 — Basic account (phone number + OTP + name). With just a verified phone number and a name, we create a limited account. The user can access the app's dashboard, see how the product works with sample data, and explore the interface. They cannot hold or move money.
Tier 2 — Receive money (+ date of birth + basic KYC). To receive funds, the user provides their date of birth and basic personal details. We run an automated KYC check against sanctions and PEP (Politically Exposed Person) databases. If the check passes, the account can receive transfers up to a low threshold.
Tier 3 — Full functionality (+ ID verification + selfie). To send money, increase limits, or access premium features, the user completes full identity verification: document capture, liveness check, and address verification. By this point, the user has already experienced the product and is more invested in completing the process.
This approach requires close collaboration with your compliance team. Not every jurisdiction allows progressive onboarding — some require full KYC before any account functionality. We work market by market, mapping regulatory requirements to the minimum viable onboarding at each tier.
// OnboardingTierManager.kt
enum class AccountTier {
EXPLORATION, // No account yet
BASIC, // Phone verified, limited access
RECEIVE_ONLY, // Basic KYC passed, can receive funds
FULL // Full KYC, all features enabled
}
class OnboardingTierManager(
private val userRepository: UserRepository,
private val kycService: KYCService,
private val regulatoryConfig: RegulatoryConfig
) {
fun getRequiredFieldsForTier(
targetTier: AccountTier,
currentTier: AccountTier,
country: String
): List<OnboardingField> {
val marketConfig = regulatoryConfig.getConfig(country)
// Some markets require full KYC for any account
if (marketConfig.requiresFullKYCForBasicAccount) {
return getFullKYCFields()
}
return when (targetTier) {
AccountTier.BASIC -> listOf(
OnboardingField.PHONE_NUMBER,
OnboardingField.FULL_NAME,
OnboardingField.CONSENT_TERMS
)
AccountTier.RECEIVE_ONLY -> listOf(
OnboardingField.DATE_OF_BIRTH,
OnboardingField.NATIONALITY,
OnboardingField.RESIDENTIAL_ADDRESS
).filter { it !in getCompletedFields(currentTier) }
AccountTier.FULL -> listOf(
OnboardingField.IDENTITY_DOCUMENT,
OnboardingField.SELFIE_LIVENESS,
OnboardingField.SOURCE_OF_FUNDS
).filter { it !in getCompletedFields(currentTier) }
else -> emptyList()
}
}
}Identity Document Capture
The identity document step is typically the highest-friction point in fintech onboarding. Users must photograph a physical document using their phone's camera, and the result must be clear enough for automated extraction and manual review.
Our approach to reducing friction at this step:
Real-time quality feedback. Instead of letting users submit a blurry photo and then rejecting it after a 30-second server round trip, we provide real-time feedback during capture: "Move closer," "Reduce glare," "Hold steady." This uses on-device image analysis running on each camera frame.
// DocumentCaptureViewController.swift
class DocumentCaptureViewController: UIViewController {
private let qualityAnalyzer = DocumentQualityAnalyzer()
func processVideoFrame(_ frame: CVPixelBuffer) {
let quality = qualityAnalyzer.analyze(frame)
DispatchQueue.main.async {
self.updateGuidance(quality)
}
if quality.meetsMinimumThreshold {
captureHighResolutionImage()
}
}
private func updateGuidance(_ quality: DocumentQuality) {
if quality.blurScore < 0.6 {
guidanceLabel.text = "Hold your phone steady"
guidanceLabel.textColor = .systemOrange
} else if quality.glareScore < 0.5 {
guidanceLabel.text = "Tilt your document to reduce glare"
guidanceLabel.textColor = .systemOrange
} else if quality.edgeDetectionScore < 0.7 {
guidanceLabel.text = "Position the document within the frame"
guidanceLabel.textColor = .systemOrange
} else {
guidanceLabel.text = "Looking good — hold still"
guidanceLabel.textColor = .systemGreen
}
}
}Auto-capture. Rather than requiring the user to tap a shutter button, we automatically capture the image when the quality analyzer determines the frame is acceptable. This removes a step and ensures the captured image is always of sufficient quality.
NFC document reading. For markets where e-passports and national ID cards support NFC, we offer NFC reading as an alternative to photo capture. NFC reading extracts the document data directly from the chip, including the holder's photo, eliminating issues with image quality, glare, and OCR errors. It is both faster and more reliable.
// NFCDocumentReader.swift
import CoreNFC
class NFCDocumentReader: NSObject, NFCTagReaderSessionDelegate {
private var session: NFCTagReaderSession?
func startReading(
documentNumber: String,
dateOfBirth: String,
dateOfExpiry: String
) {
let mrzKey = buildMRZKey(
documentNumber: documentNumber,
dateOfBirth: dateOfBirth,
dateOfExpiry: dateOfExpiry
)
session = NFCTagReaderSession(
pollingOption: .iso14443,
delegate: self,
queue: nil
)
session?.alertMessage = "Hold your ID card to the top of your phone"
session?.begin()
}
func tagReaderSession(
_ session: NFCTagReaderSession,
didDetect tags: [NFCTag]
) {
guard case .iso7816(let tag) = tags.first else { return }
session.connect(to: tags.first!) { error in
guard error == nil else { return }
// Read document data via BAC/PACE protocols
self.readDocumentData(from: tag)
}
}
}Save and resume. If the user does not have their document at hand (the number one reason for drop-off at this step), we save their progress and send a reminder notification two hours later: "Complete your account setup — have your ID ready." The user picks up exactly where they left off, with all previously entered information pre-filled.
// OnboardingStateManager.kt
class OnboardingStateManager(
private val database: KlivvrDatabase,
private val notificationScheduler: NotificationScheduler
) {
fun saveProgress(userId: String, currentStep: OnboardingStep, formData: Map<String, String>) {
database.onboardingQueries.saveState(
user_id = userId,
current_step = currentStep.name,
form_data = Json.encodeToString(formData),
saved_at = Clock.System.now().toEpochMilliseconds()
)
// Schedule a reminder if the user leaves at a high-drop-off step
if (currentStep in listOf(OnboardingStep.DOCUMENT_CAPTURE, OnboardingStep.SELFIE)) {
notificationScheduler.scheduleOnboardingReminder(
userId = userId,
delay = Duration.ofHours(2),
message = "Your account is almost ready. Have your ID handy to finish setting up."
)
}
}
fun restoreProgress(userId: String): OnboardingState? {
val saved = database.onboardingQueries.getState(userId).executeAsOneOrNull()
?: return null
return OnboardingState(
currentStep = OnboardingStep.valueOf(saved.current_step),
formData = Json.decodeFromString(saved.form_data),
savedAt = Instant.fromEpochMilliseconds(saved.saved_at)
)
}
}OTP Verification Optimization
OTP (one-time password) verification is the first authentication gate in our onboarding. It is also a surprisingly common abandonment point: users switch to their SMS app, read the code, switch back, and sometimes lose the app state or fumble the code entry.
We minimize this friction with three techniques:
Auto-fill support. On iOS, setting textContentType = .oneTimeCode on the OTP input field enables the system to auto-fill from SMS. On Android, the SMS Retriever API can read the OTP without requesting full SMS permissions:
// OTP auto-read on Android using SMS Retriever API
class OTPReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val status = extras?.get(SmsRetriever.EXTRA_STATUS) as? Status
if (status?.statusCode == CommonStatusCodes.SUCCESS) {
val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE)
val otp = extractOTP(message)
onOTPReceived(otp)
}
}
}
private fun extractOTP(message: String?): String {
// Extract 6-digit code from SMS
val pattern = Regex("\\b(\\d{6})\\b")
return pattern.find(message ?: "")?.value ?: ""
}
}Automatic advancement. When all six digits have been entered (either manually or via auto-fill), the form automatically submits without requiring the user to tap a "Verify" button. This saves one tap and one decision.
Retry with countdown. If the OTP does not arrive, a "Resend code" button appears after 30 seconds with a visible countdown. After two failed SMS deliveries, we offer a voice call alternative, which has higher delivery reliability in some markets.
The Psychology of Progress
Onboarding is a psychological experience as much as a technical one. Users need to feel that they are making progress and that the remaining effort is manageable.
Progress indicator. A simple progress bar at the top of every onboarding screen shows how far the user has come. We experimented with numbered steps ("Step 3 of 5") versus a percentage bar and found that the percentage bar performed better — it emphasizes completion over remaining effort.
Micro-celebrations. When a step is completed (especially high-effort steps like document capture), a brief success animation and a congratulatory message ("Great, your ID looks perfect") provide emotional reinforcement. This is not frivolous — it measurably reduces drop-off at the subsequent step.
Estimated time remaining. At the beginning of onboarding, we display "This usually takes about 3 minutes." Setting a time expectation reduces anxiety and abandonment. The key is accuracy: if you say 3 minutes and it takes 10, trust is damaged. We measure the actual median completion time weekly and update the estimate accordingly.
Re-engagement for Abandoned Onboarding
Despite all optimization, some users will abandon onboarding. Our re-engagement strategy is multi-channel and time-sensitive:
- 2 hours after abandonment: Push notification reminder (if notifications are enabled).
- 24 hours: Email with a direct deep link back to the exact step where they left off.
- 72 hours: SMS with a simplified message and deep link.
- 7 days: Final outreach emphasizing the product's value proposition, not just "complete your sign-up."
Each touchpoint is tracked and attributed. We measure the re-engagement rate per channel and per onboarding step to optimize the messaging and timing.
// ReEngagementScheduler.kt
fun scheduleReEngagement(userId: String, abandonedStep: OnboardingStep) {
val deepLink = "klivvr://onboarding/resume?step=${abandonedStep.name}"
scheduler.schedule(
ReEngagementEvent(
userId = userId,
channel = Channel.PUSH,
delay = Duration.ofHours(2),
message = "Your Klivvr account is almost ready. Tap to continue.",
deepLink = deepLink
)
)
scheduler.schedule(
ReEngagementEvent(
userId = userId,
channel = Channel.EMAIL,
delay = Duration.ofHours(24),
templateId = "onboarding_reminder",
templateData = mapOf(
"step_name" to abandonedStep.displayName,
"deep_link" to deepLink,
"estimated_time" to "2 minutes"
)
)
)
scheduler.schedule(
ReEngagementEvent(
userId = userId,
channel = Channel.SMS,
delay = Duration.ofHours(72),
message = "Finish setting up your Klivvr account and start sending money for free: $deepLink"
)
)
}Conclusion
Fintech onboarding is the product's first impression, and it is the point where regulatory requirements and user experience collide most directly. You cannot eliminate KYC, identity verification, or regulatory disclosures — but you can execute them with the minimum possible friction. Progressive profiling, real-time quality feedback, auto-fill, save-and-resume, and strategic re-engagement are not clever growth hacks; they are fundamental product design decisions that respect both the user's time and the regulator's requirements. Every percentage point of improvement in onboarding completion translates directly to revenue, and more importantly, to users who gain access to financial services they need. That is the real measure of success.
Related Articles
Layered Security Architecture for Mobile Banking
An in-depth look at the multi-layered security architecture that protects mobile banking apps, from device integrity checks and encrypted storage to runtime protection and network security.
App Store Optimization for Fintech Products
A comprehensive guide to App Store Optimization (ASO) for fintech mobile apps, covering keyword strategy, creative optimization, ratings management, and the unique challenges of marketing a financial product in competitive app stores.
Building Offline-First Banking Experiences
How to architect a mobile banking app that remains functional without network connectivity, covering local data persistence, sync strategies, conflict resolution, and the UX patterns that make offline banking feel seamless.