Add Face Verification to a Flutter App
Build selfie-based identity verification in Flutter: capture with the camera plugin, call a face verification API securely, and handle liveness — with full Dart code.
Face Verification in Flutter, the API Way
Running face recognition models on-device in Flutter means wrestling with TFLite delegates, model quantization, and per-platform camera quirks — and you still won't have server-grade accuracy. The simpler architecture for most apps: capture the photo in Flutter, send it to your backend, and let your backend call a face recognition API.
In this tutorial we build a selfie verification flow:
camera packageThis is the same flow behind KYC onboarding, attendance apps, and face login. Building for React Native instead? See our React Native guide.
Architecture: Keep the API Key Off the Device
Never ship your face API key inside the Flutter app — anything in the APK/IPA can be extracted. The app talks only to your backend; your backend holds the key and talks to ARSA. This is the core rule from our API key security guide.
Flutter app → your backend (holds x-key-secret) → faceapi.arsa.technology
Step 1: Capture a Selfie in Flutter
Add the dependencies:
dependencies: camera: ^0.11.0 http: ^1.2.0
A minimal front-camera capture screen:
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
class SelfieScreen extends StatefulWidget {
const SelfieScreen({super.key});
@override
State<SelfieScreen> createState() => _SelfieScreenState();
}
class _SelfieScreenState extends State<SelfieScreen> {
CameraController? _controller;
@override
void initState() {
super.initState();
_initCamera();
}
Future<void> _initCamera() async {
final cameras = await availableCameras();
final front = cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.front,
orElse: () => cameras.first,
);
_controller = CameraController(front, ResolutionPreset.high, enableAudio: false);
await _controller!.initialize();
if (mounted) setState(() {});
}
Future<void> _capture() async {
final file = await _controller!.takePicture();
if (!mounted) return;
Navigator.pop(context, file.path);
}
@override
Widget build(BuildContext context) {
if (_controller == null || !_controller!.value.isInitialized) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
Expanded(child: CameraPreview(_controller!)),
Padding(
padding: const EdgeInsets.all(16),
child: FilledButton(onPressed: _capture, child: const Text("Capture Selfie")),
),
],
);
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
}
Capture quality tips that directly improve match scores:
Step 2: Upload to Your Backend
import 'dart:io';
import 'package:http/http.dart' as http;
Future<Map<String, dynamic>> verifySelfie(String imagePath, String userId) async {
final uri = Uri.parse("https://api.yourapp.com/verify-face");
final request = http.MultipartRequest("POST", uri)
..fields["user_id"] = userId
..files.add(await http.MultipartFile.fromPath("photo", imagePath));
final response = await http.Response.fromStream(await request.send());
if (response.statusCode != 200) {
throw Exception("Verification failed: ${response.body}");
}
return jsonDecode(response.body) as Map<String, dynamic>;
}
Step 3: The Backend Verification Call
Your backend forwards the image to ARSA. Two options depending on your use case:
1:1 verification — compare the selfie against a stored reference photo (e.g., from ID document capture). Uses POST /api/v1/face_recognition/validate_faces with image1 and image2 multipart fields.
1:N recognition — identify who the selfie belongs to among registered users. Register each user once with register_face and a x-face-uid header, then call recognize_face at verification time. Our 1:N recognition explainer covers when to use which.
Node.js backend example (see the full Node.js tutorial for setup):
app.post("/verify-face", upload.single("photo"), async (req, res) => {
const blob = new Blob([req.file.buffer], { type: "image/jpeg" });
// 1. Liveness gate
const lv = new FormData();
lv.append("face_image", blob, "selfie.jpg");
const liveness = await arsaFetch("/face_liveness", { body: lv });
if (liveness.faces?.some(f => f.liveness === "fake")) {
return res.status(403).json({ verified: false, reason: "spoof" });
}
// 2. Identify against registered users
const rec = new FormData();
rec.append("face_image", blob, "selfie.jpg");
const match = await arsaFetch("/face_recognition/recognize_face", { body: rec });
res.json({ verified: true, faces: match.faces });
});
Step 4: Handle the Result in Flutter
final result = await verifySelfie(path, currentUserId);
if (result["verified"] == true) {
// proceed to the authenticated flow
} else if (result["reason"] == "spoof") {
// show "please use your real face" message and retry
}
Design the failure UX deliberately: allow 2–3 retries with guidance ("remove glasses", "find better lighting") before falling back to another verification method.
Don't Skip Liveness
A Flutter camera capture is trivially spoofable — anyone can point the phone at a photo on another screen. Passive liveness blocks this with zero extra UX. For regulated flows like fintech onboarding, consider active liveness: the API issues head-movement instructions, your app records a short video, and the API verifies the movements — the approach we compare in active vs. passive liveness.