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