← Back to Blog
Tutorial5 min read

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:

  • Capture a selfie with the camera package
  • Upload it to your backend
  • Backend checks liveness, then verifies the selfie against a reference photo (1:1) or searches your registered users (1:N)
  • This 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:

    yaml
    dependencies:
      camera: ^0.11.0
      http: ^1.2.0
    

    A minimal front-camera capture screen:

    dart
    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:

  • • Guide the user with an oval overlay so the face fills most of the frame
  • • Reject obviously dark frames before uploading
  • • JPEG at high resolution is fine — the API accepts JPEG/PNG up to 10 MB
  • Step 2: Upload to Your Backend

    dart
    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):

    javascript
    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

    dart
    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.

    Next Steps

  • • Read the authentication docs for header details
  • • Add face analytics if you need age or expression signals
  • • Review similarity thresholds before choosing your match cutoff
  • Get a free API key — the free tier covers 100 calls/month, plenty for prototyping the whole flow.

    Ready to get started?

    Try ARSA Face Recognition API free with 100 API calls/month.

    Start Free Trial