kreltix_liveness_sdk
Flutter SDK for Kreltix — identity verification with three methods to fit any flow.
| Method | What it does | Camera needed? |
|---|---|---|
startLivenessCheck() |
Challenge-based liveness detection — SDK handles camera, animated overlay, recording | Yes |
faceMatch() |
Compare ID card vs selfie — no camera, no session | No |
combinedCheck() |
Liveness + face match in one API call — you supply the image/video | No |
Features
- Drop-in liveness overlay — randomised blink/turn/smile challenges, no UI work required
- Direct face match — compare two images without touching the camera
- Combined check — single call confirms liveness AND face match simultaneously
- Confidence-scored results with per-challenge breakdown
- Progress callbacks at every stage of the liveness flow
- Typed error hierarchy — catch exactly the error you care about
- iOS and Android support (Flutter 3.10+, Dart 3.0+)
Installation
Add to your pubspec.yaml:
dependencies:
kreltix_liveness_sdk: ^0.1.0
Then run:
flutter pub get
Permissions
These entries are required. Without them the app will crash on first camera access.
Android
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
iOS
ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera is required for liveness verification.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for video recording.</string>
Both keys are needed — the SDK records short video clips during each challenge.
Quick Start
import 'package:kreltix_liveness_sdk/kreltix_liveness_sdk.dart';
// Initialise once — reuse the instance across your app.
final sdk = KreltixSDK.initialize('pk_live_your_public_key');
// Inside any widget method where you have a BuildContext:
final result = await sdk.startLivenessCheck(
context: context,
metadata: {'userId': 'user_123'},
);
if (result.isLive) {
print('Verified — ${result.confidence.toStringAsFixed(0)}% confidence');
} else {
print('Failed: ${result.recommendation}');
}
The SDK pushes a fullscreen MaterialPageRoute overlay onto your navigator stack, runs all challenges, shows the result screen for 2.5 seconds, then pops automatically — your await resumes with the LivenessResult.
Face Match
Compare an ID card against a selfie without any camera or session. Pass plain base64 strings.
final result = await sdk.faceMatch(
idCardImage: idCardBase64,
selfieImage: selfieBase64,
metadata: {'userId': 'user_123'},
);
if (result.verified) {
print('Match confirmed — distance: ${result.distance.toStringAsFixed(3)}');
} else {
print('No match: ${result.status}');
}
FaceMatchResult fields
| Field | Type | Description |
|---|---|---|
requestId |
String? |
Server-assigned request ID for audit logs |
match |
bool |
Raw face match result |
distance |
double |
Euclidean distance — lower is more similar (0.0–1.0) |
verified |
bool |
true when distance is within the confidence threshold |
status |
String |
"verified", "mismatch", or "error" |
error |
String? |
Error detail if the check could not complete |
Combined Check — Liveness + Face Match
Run liveness and face match in one call — no camera overlay. You supply the image or video.
Standard (passive liveness — single image)
final result = await sdk.combinedCheck(
idCardImage: idCardBase64,
image: selfieBase64, // passively-captured selfie
metadata: {'userId': 'user_123'},
);
if (result.verified) {
print('Live and face match confirmed');
}
Premium (active liveness — video + challenge)
final result = await sdk.combinedCheck(
idCardImage: idCardBase64,
tier: 'premium',
video: videoBase64,
challenge: 'blink', // 'blink' | 'turn_left' | 'turn_right' | 'smile'
);
print('isLive: ${result.isLive}');
print('faceMatch: ${result.faceMatch}');
print('verified: ${result.verified}');
CombinedCheckResult fields
| Field | Type | Description |
|---|---|---|
requestId |
String? |
Server-assigned request ID |
tier |
String |
"standard" or "premium" |
isLive |
bool |
Liveness passed |
livenessConfidence |
double |
0–100 liveness score |
challengeCompleted |
bool? |
Whether the active challenge was completed (premium only) |
challengeType |
String? |
Challenge that was used (premium only) |
faceChecked |
bool |
Whether face match ran (only runs if liveness passed) |
faceMatch |
bool? |
Face match result |
faceDistance |
double? |
Euclidean distance from face comparison |
verified |
bool |
true only when both liveness AND face match passed |
recommendation |
String? |
Human-readable outcome message |
error |
String? |
Error detail if either check failed |
How It Works
Your app SDK overlay Kreltix API
│ │ │
│─ initialize() ─────▶│ │
│─ startLivenessCheck() │
│ │─ POST /session ──────▶│
│ │◀─ challenges ─────────│
│ │ │
│ [overlay pushed onto navigator] │
│ │ │
│ │ challenge 1: blink │
│ │ [3-sec recording] │
│ │ challenge 2: smile │
│ │ [3-sec recording] │
│ │ │
│ │─ POST /verify ───────▶│
│ │◀─ LivenessResult ─────│
│ │ │
│ [result screen shown 2.5s, overlay popped]
│◀─ LivenessResult ───│
All Options
startLivenessCheck accepts named parameters beyond the required context:
final result = await sdk.startLivenessCheck(
context: context,
// Optional metadata — echoed back in the webhook payload
metadata: {'userId': 'user_123', 'source': 'flutter-app'},
// Fires when the backend session is created (contains challenge list)
onSessionCreated: (session) {
print('Challenges: ${session.challenges.join(' → ')}');
print('Expires: ${session.expiresAt}');
},
// Fires before each challenge recording starts
onChallengeStarted: (challenge) {
print('Next challenge: $challenge'); // e.g. "blink", "turn_left"
},
// Fires at each stage of the flow
onProgress: (event) {
print('[${event.stage.name}] ${event.message}');
},
// Fires if an error occurs (the exception is also rethrown)
onError: (err) {
print('SDK error: $err');
},
);
SDK Options
Pass SDKOptions as the second argument to initialize() to customise networking behaviour:
final sdk = KreltixSDK.initialize(
'pk_live_your_key',
SDKOptions(
baseUrl: 'https://api.kreltix.com', // default
timeout: Duration(seconds: 10), // default
maxRetries: 2, // default
),
);
| Field | Type | Default | Description |
|---|---|---|---|
baseUrl |
String |
https://api.kreltix.com |
API base URL |
timeout |
Duration |
Duration(seconds: 10) |
Per-request timeout |
maxRetries |
int |
2 |
Retry attempts on network failure |
LivenessResult
The object returned (and awaited) from startLivenessCheck():
| Field | Type | Description |
|---|---|---|
isLive |
bool |
true when the person passed all challenges |
confidence |
double |
0–100 liveness confidence score |
allChallengesPassed |
bool |
true if every individual challenge completed |
sessionId |
String |
Unique ID — use this to look up the result server-side |
challengeResults |
List<ChallengeStepResult> |
Per-challenge breakdown |
recommendation |
String? |
Human-readable outcome message |
error |
String? |
Error message if the check could not complete |
indicators |
Map<String, dynamic>? |
Raw analysis signals from the backend |
ChallengeStepResult
Each entry in challengeResults:
| Field | Type | Description |
|---|---|---|
challenge |
String |
Challenge name — "blink", "turn_left", etc. |
completed |
bool |
Whether this specific challenge passed |
indicators |
Map<String, dynamic>? |
Per-frame signals for this challenge |
Progress Stages
The onProgress callback fires with a ProgressEvent(stage, message) at each stage:
ProgressStage |
When |
|---|---|
sessionCreated |
Backend session created; challenge list received |
cameraReady |
Camera initialised and preview started |
recording |
Fired before each challenge recording begins |
uploading |
All videos recorded; upload starting |
processing |
Upload complete; backend analysing videos |
complete |
Result received; overlay closing |
onProgress: (event) {
switch (event.stage) {
case ProgressStage.sessionCreated:
showSnackBar('Session ready');
case ProgressStage.recording:
showSnackBar(event.message); // "Challenge: blink"
case ProgressStage.complete:
showSnackBar('Done');
default:
break;
}
},
Error Handling
All SDK errors extend KreltixError which implements Exception. Catch specific types for fine-grained recovery:
try {
final result = await sdk.startLivenessCheck(context: context);
} on KreltixCameraError catch (e) {
// Camera permission denied or hardware unavailable
showDialog(context: context, builder: (_) => AlertDialog(
title: const Text('Camera required'),
content: Text(e.message),
));
} on KreltixSessionExpiredError {
// Session timed out — safe to retry immediately
_startCheck(); // call your method again
} on KreltixInsufficientFundsError {
// Wallet balance too low
launchUrl(Uri.parse('https://kreltix.com/dashboard'));
} on KreltixAuthError catch (e) {
// Invalid or revoked pk_live_ key
debugPrint('Auth error: ${e.message}');
} on KreltixNetworkError catch (e) {
// Connectivity issue; SDK already retried maxRetries times
debugPrint('Network: ${e.message} (retryable: ${e.isRetryable})');
} on KreltixError catch (e) {
// Catch-all for any other SDK error
debugPrint('[${e.code}] ${e.message}');
}
Error Reference
| Class | Code | Retryable | Cause |
|---|---|---|---|
KreltixCameraError |
CAMERA_ERROR |
No | Camera permission denied or device issue |
KreltixSessionExpiredError |
SESSION_EXPIRED |
No | Session timed out — start a new check |
KreltixAuthError |
AUTH_ERROR |
No | Invalid or revoked public key |
KreltixInsufficientFundsError |
INSUFFICIENT_FUNDS |
No | Wallet balance too low |
KreltixNetworkError |
NETWORK_ERROR |
Yes | Connectivity issue; already auto-retried |
KreltixServerError |
SERVER_ERROR |
Yes | Temporary backend error |
KreltixValidationError |
VALIDATION_ERROR |
No | Invalid request parameters |
All errors expose .message, .code, and .isRetryable.
Full Widget Example
A complete StatefulWidget that tracks status and displays the result:
import 'package:flutter/material.dart';
import 'package:kreltix_liveness_sdk/kreltix_liveness_sdk.dart';
// Initialise once at app scope
final _sdk = KreltixSDK.initialize('pk_live_your_public_key');
class LivenessCheckButton extends StatefulWidget {
const LivenessCheckButton({super.key});
@override
State<LivenessCheckButton> createState() => _LivenessCheckButtonState();
}
class _LivenessCheckButtonState extends State<LivenessCheckButton> {
bool _loading = false;
String _status = '';
LivenessResult? _result;
Future<void> _run() async {
setState(() {
_loading = true;
_status = 'Initializing...';
_result = null;
});
try {
final result = await _sdk.startLivenessCheck(
context: context,
metadata: {'userId': 'user_123', 'source': 'flutter-app'},
onSessionCreated: (s) => setState(() =>
_status = 'Challenges: ${s.challenges.join(' → ')}'),
onChallengeStarted: (c) => setState(() =>
_status = 'Challenge: $c'),
onProgress: (e) => setState(() =>
_status = '[${e.stage.name}] ${e.message}'),
onError: (err) => setState(() =>
_status = 'Error: $err'),
);
setState(() {
_result = result;
_status = 'Complete.';
});
} on KreltixCameraError catch (e) {
setState(() => _status = 'Camera error: ${e.message}');
} on KreltixSessionExpiredError {
setState(() => _status = 'Session expired — try again.');
} on KreltixError catch (e) {
setState(() => _status = '[${e.code}] ${e.message}');
} finally {
setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
final result = _result;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: _loading ? null : _run,
child: Text(_loading ? 'Verifying...' : 'Verify Identity'),
),
if (_status.isNotEmpty) ...[
const SizedBox(height: 12),
Text(_status, style: const TextStyle(color: Colors.white70, fontSize: 13)),
],
if (result != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: result.isLive ? Colors.green.shade900 : Colors.red.shade900,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
result.isLive
? '✓ Verified — ${result.confidence.toStringAsFixed(0)}% confidence'
: '✗ Failed — ${result.recommendation ?? 'Check could not complete'}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 6),
Text('Session: ${result.sessionId}',
style: const TextStyle(fontSize: 11, color: Colors.white54)),
],
),
),
],
],
);
}
}
Webhooks
Set a webhook URL in the dashboard and Kreltix will POST a signed event payload each time a check completes. Use the sessionId from LivenessResult to correlate SDK results on your backend with incoming webhook events.
See the API Documentation tab in the dashboard for event shapes and signature verification.
Platform Requirements
| Platform | Minimum version |
|---|---|
| Flutter | 3.10.0 |
| Dart SDK | 3.0.0 |
| iOS | 12.0+ |
| Android | API 21+ (Android 5.0) |
Get an API Key
Sign up at kreltix.com and generate an SDK Key Pair from the API Keys page. You'll receive a pk_live_ public key to embed in your Flutter app.
New accounts include a 30-day free trial — all API calls are included at no charge, no card required.
Your secret sk_live_ key should only ever live on your backend server — never in the Flutter app.
Support
- Dashboard: kreltix.com
- API Docs: available in the Documentation tab after signing in
- pub.dev: pub.dev/packages/kreltix_liveness_sdk