Handle errors and timeouts
This guide shows how to handle scanning that fails or times out, and how to tell when individual fields couldn't be read.
Handle scan failures and timeouts
When you use the camera UI, the SDK exposes a single entry point for a session that finishes without a usable result. A finished-but-empty result generally means the user should retry.
- Web (JS)
- Android (Kotlin)
- iOS (Swift)
- Flutter (Dart)
- React Native (TS)
Register an error callback. It receives one of a small set of error states, and these errors generally mean the user should retry:
"timeout": scanning ran longer than the allowed duration without completing"result_retrieval_failed": the result could not be retrieved"unknown": an unexpected error
blinkCard.addOnErrorCallback((error) => {
if (error === "timeout") {
// Prompt the user to try again in better conditions
}
void blinkCard.destroy();
});
BlinkCardCameraScanningScreen invokes onScanningCanceled when the user cancels or dismisses scanning, including after a timeout dialog.
A successful scan instead calls onScanningSuccess with a BlinkCardScanningResult.
BlinkCardCameraScanningScreen(
blinkCardSdk = blinkCardSdk,
onScanningSuccess = { result ->
// Use the scanning result
},
onScanningCanceled = {
// No usable result — prompt the user to try again
},
)
BlinkCardUXModel publishes the outcome through its @Published result: BlinkCardResultState? property.
Observe $result: when scanning ends without a usable capture, the model emits a BlinkCardResultState whose scanningResult is nil.
uxModel.$result
.compactMap { $0 }
.sink { state in
guard state.scanningResult != nil else {
// No usable result — prompt the user to try again
return
}
// Use state.scanningResult
}
.store(in: &cancellables)
performScan throws a PlatformException when scanning fails or the user cancels or dismisses scanning, including after a timeout dialog.
A successful scan instead returns a BlinkCardScanningResult.
try {
final result = await blinkCardPlugin.performScan(
blinkCardSdkSettings: sdkSettings,
blinkCardSessionSettings: sessionSettings,
);
// Use the scanning result
} on PlatformException catch (e) {
// No usable result — prompt the user to try again
}
The promise returned by performScan is rejected when scanning fails or the user cancels or dismisses scanning, including after a timeout dialog.
A successful scan instead resolves with a BlinkCardScanningResult.
try {
const result = await performScan(sdkSettings, sessionSettings, new ScanningUxSettings());
// Use the scanning result
} catch (error) {
// No usable result — prompt the user to try again
}
Adjust the timeout
- Web (JS)
- Android (Kotlin)
- iOS (Swift)
- Flutter (Dart)
- React Native (TS)
The scanning timeout defaults to 10 seconds. Change it, or disable it, through the UX manager exposed on the component:
// Extend to 20 seconds
blinkCard.blinkCardUxManager.setTimeoutDuration(20000);
// Disable the timeout entirely
blinkCard.blinkCardUxManager.setTimeoutDuration(null);
The step timeout defaults to 15 seconds.
Set it through BlinkCardUxSettings, in milliseconds.
A value of 0 disables the timeout.
BlinkCardCameraScanningScreen(
blinkCardSdk = blinkCardSdk,
uxSettings = BlinkCardUxSettings(stepTimeoutDurationMs = 20000),
onScanningSuccess = { /* ... */ },
onScanningCanceled = { /* ... */ },
)
The step timeout defaults to 15 seconds.
Set stepTimeoutDuration (a TimeInterval, in seconds) on BlinkCardSessionSettings.
let sessionSettings = BlinkCardSessionSettings(stepTimeoutDuration: 20.0)
The step timeout defaults to 15 seconds.
Set stepTimeTimeoutInterval on BlinkCardSessionSettings, in milliseconds.
final sessionSettings = BlinkCardSessionSettings(stepTimeTimeoutInterval: 20000);
The step timeout defaults to 15 seconds.
Set stepTimeTimeoutInterval on BlinkCardSessionSettings, in milliseconds.
const sessionSettings = new BlinkCardSessionSettings({ stepTimeTimeoutInterval: 20000 });
Detect missing or unreadable fields
When scanning completes, check the per-field extraction status to tell apart a field that was never present from one that was seen but couldn't be read.
In a photo flow, the process result carries a resultCompleteness object with one status per field.
- Web (JS)
- Android (Kotlin)
- iOS (Swift)
- Flutter (Dart)
- React Native (TS)
const processResult = session.process(imageData);
const completeness = processResult.resultCompleteness;
if (completeness.cvvExtractionStatus === "not-extracted") {
// The CVV was visible but couldn't be read (blur, glare, occlusion) — ask for a clearer capture
}
if (completeness.cvvExtractionStatus === "not-present") {
// The CVV hasn't appeared yet — it may be on a side not scanned so far
}
Each field status is one of:
"extracted": the field was read successfully"not-extracted": the field is present but couldn't be read—a blocking error that needs user action"not-present": the field hasn't been seen on any scanned side yet"not-requested": extraction was disabled for this field"not-available": no status is available
val processResult = session.process(inputImage)
val completeness = processResult.resultCompleteness
when (completeness.cvvExtractionStatus) {
FieldExtractionStatus.NotExtracted -> {
// The CVV was visible but couldn't be read (blur, glare, occlusion) — ask for a clearer capture
}
FieldExtractionStatus.NotPresent -> {
// The CVV hasn't appeared yet — it may be on a side not scanned so far
}
else -> { /* ... */ }
}
Each field status is a FieldExtractionStatus value:
Extracted: the field was read successfullyNotExtracted: the field is present but couldn't be read—a blocking error that needs user actionNotPresent: the field hasn't been seen on any scanned side yetNotRequested: extraction was disabled for this fieldNotAvailable: no status is available
let frameResult = try await session.process(inputImage: inputImage)
let completeness = frameResult.processResult?.resultCompleteness
switch completeness?.cvvExtractionStatus {
case .notExtracted:
// The CVV was visible but couldn't be read (blur, glare, occlusion) — ask for a clearer capture
case .notPresent:
// The CVV hasn't appeared yet — it may be on a side not scanned so far
default:
break
}
Each field status is a FieldExtractionStatus value:
extracted: the field was read successfullynotExtracted: the field is present but couldn't be read—a blocking error that needs user actionnotPresent: the field hasn't been seen on any scanned side yetnotRequested: extraction was disabled for this fieldnotAvailable: no status is available
The Flutter plugin doesn't expose per-field extraction status.
Check whether the field is null in the result instead:
if (result.cardAccounts[0].cvv == null) {
// The CVV wasn't extracted — it may be on a side that wasn't scanned
}
The React Native module doesn't expose per-field extraction status. Check whether the field is empty in the result instead:
if (!result.cardAccounts[0].cvv) {
// The CVV wasn't extracted — it may be on a side that wasn't scanned
}
Handle blur and poor lighting
By default, BlinkCard rejects blurred frames so it only processes sharp images, which already guards against most low-quality captures.
If a scan keeps timing out because the input is consistently blurry, the most useful response is to guide the user: improve lighting, hold the card flat, and avoid glare. The built-in camera UI already shows this kind of real-time feedback.