apenia_verify 0.0.2
apenia_verify: ^0.0.2 copied to clipboard
Apenia Verify — KYC capture SDK. Native active-liveness selfie + document scan; matching runs server-side in Apenia Verify.
apenia_verify #
Flutter KYC capture SDK for Apenia Verify.
The handset captures — native active-liveness selfie (ML Kit on Android, Vision on iOS) and document scan (CameraX / AVFoundation). The matching engine lives server-side in Apenia Verify: face-match, liveness/PAD scoring and any ID-authority lookup all happen there. The device never decides pass/fail and never holds the secret key.
This mirrors the ergonomics of the smile_id package (drop-in product
screens, PartnerParams, create-session → upload → callback) but keeps the
verification intelligence on your own infrastructure.
Capture UX #
- Document — fixed enclosure, no enhancement. A contained (non-fullscreen)
camera with a document-shaped frame sized to the physical doc (ID-1 card
1.586, passport data page1.42). Built on plain CameraX / AVFoundation — not the OS document scanner — so there are no enhancement, filter or re-crop options. The saved image is cropped to exactly the frame. Front, then back for two-sided documents. - Confirm / retake. After each side: "Is the document clear and readable?" → Yes, use this / Retake photo.
- Auto or manual capture.
ApeniaConfig.documentCaptureModeselectsmanual(shutter, default) orauto(fires once the document is detected in frame). The shutter always works as a manual override in auto mode. - On-device detail extraction. After the MRZ-bearing side (ID back / passport page) the SDK OCRs it on-device — ML Kit on Android, Vision on iOS — and parses the MRZ (TD1 / TD3) into full name, ID number, date of birth, sex, nationality and expiry. Kenyan IDs yield both the serial number and the national ID number, in that order.
- Face — controlled face-scanner. A dimmed oval overlay guides the user; capture won't start until the face is centred, correctly sized, frontal and eyes-open (the ring turns green). Then the active-liveness challenges run.
- 100% live data — no static path. The selfie primary and every liveness frame are sampled only during the live session, and the capture fails unless the full challenge sequence completes with a face present throughout. If the face is lost mid-sequence, positioning restarts. There is no code path that returns a still or gallery image, so what's uploaded is always live capture.
Liveness honesty: the free on-device path is active liveness — it proves a live person is present and following prompts (blink / turn / smile). It is not ISO/IEC 30107-3 presentation-attack detection. Apenia Verify runs passive PAD scoring server-side on the uploaded frames. If you later need certified on-device passive PAD, swap the native capture module for a licensed vendor SDK (FaceTec / iProov / Regula) behind the same
ApeniaVerifyPlatforminterface — the Dart API and upload flow don't change.
Install #
dependencies:
apenia_verify:
path: ../apenia_verify # or a git/hosted ref
Android #
minSdk≥ 24. TheCAMERApermission and capture activities are declared in the plugin manifest and merged automatically.
iOS #
- Deployment target ≥ 13.0.
- Add to the host app's
Info.plist:<key>NSCameraUsageDescription</key> <string>Used to capture your selfie and ID document for verification.</string>
Usage #
await ApeniaVerify.initialize(ApeniaConfig(
baseUrl: 'https://verify.apenia.com',
sessionProvider: (req) async { // your backend creates the session with sk_…
final json = await myBackend.createSession(flow: req.flow, metadata: req.params.toJson());
return ApeniaSession.fromJson(json); // { session_id, upload_url, client_token }
},
));
// Drop-in product screen:
ApeniaBiometricKYC(
params: PartnerParams(userId: user.id, jobId: uuid(), jobType: 'biometric_kyc'),
idType: 'NATIONAL_ID',
idNumber: '12345678',
onSuccess: (VerifyVerdict v) => print(v.status),
onError: (e) => print(e),
);
Or call the facade directly without the bundled UI:
final verdict = await ApeniaVerify.instance.runDocumentVerification(
params: PartnerParams(userId: user.id, jobId: uuid()),
captureBack: true,
);
Architecture #
Dart app ─▶ ApeniaVerify (facade)
├─ native capture ──MethodChannel──▶ Android: LivenessActivity / DocumentCaptureActivity (ML Kit + CameraX)
│ iOS: LivenessViewController / DocumentCaptureViewController (Vision + AVFoundation)
└─ ApeniaClient ──HTTPS──▶ Apenia Verify API (create session → pre-signed PUT → poll/IPN)
└─ face-match + PAD scoring + ID-authority lookup
Capture returns local file paths over the channel (small payload); the
Dart ApeniaClient zips the frames + an info.json manifest and uploads.
Server contract (SDK capture flow) #
Full backend spec to hand to the API team:
doc/onboarding-sessions-api.md.
This is the SDK sibling of Apenia's hosted onboarding-links product. The
hosted flow (POST /v1/onboarding/links → hosted URL) runs capture on
Apenia's page; the SDK flow runs capture on-device and submits against an
onboarding session. One credential per layer: your backend creates the
session with the secret sk_…; the app authenticates its calls with only the
short-lived Bearer client token that create-session returned.
The public Apenia docs currently document only the hosted links flow. The session endpoints below are the natural SDK counterpart — confirm the exact paths with the Apenia backend; if they differ, only
_sessionsPathinapenia_client.dartchanges. The result schema is already the real one.
1. POST /v1/onboarding/sessions (backend-proxied; mints the session) #
// request
{ "flow": "biometric_kyc", "metadata": { "user_id": "…", "job_id": "…", "customer_ref": "…" } }
// response
{ "session_id": "onb_…", "upload_url": "https://s3…/presigned-put" }
2. PUT <upload_url> (Content-Type: application/zip) #
A zip containing info.json (the manifest) plus the JPEG frames:
selfie.jpg, liveness_0.jpg…, id_front.jpg, id_back.jpg.
3. GET /v1/onboarding/sessions/{session_id} (or the onboarding.completed IPN) #
{ "session_id": "onb_…",
"status": "completed | pending | processing | failed | review",
"subject_id": "sub_…",
"result": {
"face_match": { "score": 0.95, "match": true },
"liveness": { "score": 0.98, "live": true },
"voice": { "score": 0.90 },
"identity_confidence": 84
} }
Prefer the IPN to your backend as the source of truth; the SDK polls this
endpoint only to give the caller a synchronous VerifyVerdict.
Flows #
biometric_kyc, doc_verification (extendable: authentication,
enhanced_document_verification).
Security & compliance notes #
- No secret key on device.
sessionProviderreturns the session your backend created with the secretsk_…; the app only ever holds the short-livedclient_token. - Idempotent
job_id. A retried upload with the samejob_idmust not create a duplicate KYC record server-side. - Consent + audit. Capture explicit user consent before launching a
product screen and log it (Kenya DPA / GDPR). The
rawmap onVerifyVerdictis retained for your audit trail. - Data minimisation. Frames live in the OS cache/temp dir only until upload; delete them after a successful submit in production.
Status #
Early release (0.0.2). The Dart layer analyzes clean and is unit-tested, including the end-to-end upload contract. The capture flows have been exercised on real hardware: document capture, MRZ extraction and the liveness challenges on Android; the framed capture and liveness on iOS.
Known gaps before production:
- Liveness is active only — see the note above; passive PAD is scored server-side by Apenia Verify.
- iOS MRZ extraction is implemented and compiles but has had less on-device mileage than the Android path.
- Retry limits and capture timeouts are minimal; tune for your own UX.