apenia_verify
Apenia Verify — KYC capture SDK. Native active-liveness selfie + document scan; matching runs server-side in Apenia Verify.
0.0.19
247 downloads/mo
#1158 most downloaded on pub.dev
What this package is like to depend on
Last release 1 months ago
22 Jul 2026
Too new to tell
only 1 release windows
Nearly every release is documented
notes for 19 of 19 stable releases
Nothing withdrawn
no release was ever pulled
1 months old
19 releases · first in 2026
19 releases in the last 12 months
see the full history below
Release timeline
19 releases · Jul 2026 to Jul 2026Releases
latest 19-
0.0.1922 Jul 2026Release notes
Open source →- Passport enclosure widened to 2.4. Tuned against a real open passport on device — 2.4 fits the two-page spread comfortably while the MRZ stays large enough to read (confirmed reading correctly at this width).
-
0.0.1822 Jul 2026Release notes
Open source →- Wider passport enclosure. The passport capture frame was sized to a single ID-3 data page (1.586... 1.42), but a passport is presented open flat, so the data page sits inside a two-page spread. The frame is now wider (1.9) to fit the opened booklet without clipping the binding, and the guidance reads "hold it open flat".
-
0.0.1722 Jul 2026Release notes
Open source →- Refreshed the Apenia brand lockup. The plain
apenia.wordmark is now a proper logo: a rounded brand-gradient badge with a verify check next to a clean "Apenia" — an identity product, stated in the mark. Shown in every screen's app bar.
- Refreshed the Apenia brand lockup. The plain
-
0.0.1622 Jul 2026Release notes
Open source →- Verification wait is now a legible stepper. The single indefinite spinner
became a two-step progress list — Uploading your capture → Matching your
face to your ID — each marked done / in-progress / upcoming, driven by the
real
SubmitPhase. A minute-long server wait reads as steady progress rather than a hang, with a "keep this screen open" reassurance.
- Verification wait is now a legible stepper. The single indefinite spinner
became a two-step progress list — Uploading your capture → Matching your
face to your ID — each marked done / in-progress / upcoming, driven by the
real
-
0.0.1522 Jul 2026Release notes
Open source →- "Document captured" confirmation. After the ID scan, a small elegant
modal card — floating over a dimmed scrim, with a success mark and a
Continue button — confirms the milestone before moving to the selfie,
instead of silently swapping screens. New reusable
ApeniaModalCard, in Apenia red.
- "Document captured" confirmation. After the ID scan, a small elegant
modal card — floating over a dimmed scrim, with a success mark and a
Continue button — confirms the milestone before moving to the selfie,
instead of silently swapping screens. New reusable
-
0.0.1421 Jul 2026Release notes
Open source →- Polished the document-choice list. National ID / Passport are now proper
elevated list rows in a
ListView— a soft shadow, a document-shaped icon chip (rounded, not a circle), a clearer chevron affordance, and a brand-tinted press ripple. The list scrolls cleanly instead of being pinned by a spacer.
- Polished the document-choice list. National ID / Passport are now proper
elevated list rows in a
-
0.0.1321 Jul 2026Release notes
Open source →Result screen: privacy-first, and a clear action per outcome.
- No personal data or scores on the result screen. It previously showed the face-match / liveness percentages, the on-device check scorecard, and the extracted document fields (name, ID number, date of birth). Those are for your backend — via the verdict and the IPN — not for a screen an end user (or whoever is next to them) can read. The screen now shows only the decision, a short message, and the next action.
- Three distinct outcomes.
- Verified — filled success mark, "You're verified", Done.
- Under review — amber, reassuring copy, Done.
- Couldn't verify / didn't go through (rejected or failed) — a Try again button that restarts capture, plus a quiet Done.
onCompletefires on every terminal screen — success, review, declined, failed, and the still-processing screen — so the host app always receives the verdict when the user finishes, whichever outcome they landed on.
-
0.0.1221 Jul 2026Release notes
Open source →Fixes polling against the current Apenia API — upgrade. Verdicts were never being read, so the flow polled until timeout on every job.
- Reads the
schema_version: "1.0"response envelope. Production nests the verdict underverdict(decision,code,reason,confidence) and no longer sends top-levelstatus/code/decision. The SDK was reading the old top-level fields, found nothing, and treated every poll as non-terminal — so a completed job looked identical to a pending one and the flow spun until the poll deadline. Both shapes now parse (the flat one is kept for the mock and older hosted paths). - Fixes in-progress codes being mistaken for failures.
4010(waiting for upload) and4020(scoring) share the4xxxfamily with real failures; the old family bucket mapped them tofailed, which would stop polling and show a failure on a job that was merely still running. Now4010 → pending,4020 → processing(keep polling),4030 → expired,9010 → rejected, matching the server's own overrides. - Verdict text now reads
verdict.reason(v1.0) as well as the oldmessage, and identity confidence readsverdict.confidence.
Branch on
VerifyVerdict.isApproved/.decisionas before — the mapping is unchanged, only where it's read from. - Reads the
-
0.0.1121 Jul 2026Release notes
Open source →UX around submission, and illustration-led onboarding screens.
- Submission phases are now distinct.
submitArtifactsreports its stage through anonPhasecallback (creatingSession→uploading→verifying), andApeniaOnboardingshows a matching screen for each. A multi-minute server wait no longer sits under a single "uploading" spinner. - Fixed: a still-processing job could render as a rejection. If polling hit the deadline while the server was still scoring, the SDK returned a non-terminal verdict and the result screen showed it as declined. A non-terminal outcome now routes to a dedicated "Verification in progress" screen — the real decision arrives via the IPN — and is never shown as a decline.
- Observable polling. Each poll attempt is reported to
onDiagnostic(poll N -> processing (code …)), and a poll timeout is logged distinctly. - Illustration-led instruction screens. The intro, document and selfie screens now lead with original Apenia-red hero illustrations (a verify shield, an ID card, a face-in-oval that previews the real capture oval) instead of a flat icon; the approved result uses a filled success mark. All drawn in Flutter — no bundled assets — and the capture-only flow is unchanged (still no gallery-upload path).
- Submission phases are now distinct.
-
0.0.1020 Jul 2026Release notes
Open source →Selfie capture quality. 0.0.9 uploaded whichever frame happened to be current when the alignment gate tripped — which in practice meant a blink, a downward glance, or the darkest frame of the session. The face is the only region a matcher scores, so this was the weakest link in the whole flow.
- Best-frame selection. Every aligned frame is now scored and the best one
is uploaded as both the selfie and the
face_chip, rather than the newest one. Weighting: 50% eye-openness, 20% frontality, 30% face exposure. If the best frame so far has closed eyes the capture waits briefly and prompts "Look at the camera", but never blocks indefinitely — a hard gate would strand anyone whose eyes read as narrow to the detector. - Waits for auto-exposure and white balance to converge. The gate could pass within a second of the camera opening, before AE/AWB had settled, so the darkest and most colour-cast frame of the session became the matching image. This matters disproportionately for darker skin, where an underexposed face loses most of its usable tonal detail.
- Exposure is measured on the face, not the frame. A backlit subject leaves the face in shadow while the frame average looks perfectly fine.
- Wider face crop. Both detectors return a box tight on the face that excludes hair, so the previous symmetric margin clipped the forehead. Now asymmetric — generous above, moderate at the sides and chin.
- Fixed: the orientation fix in 0.0.9 was upscaling every frame 3× on iOS.
UIGraphicsImageRendererdefaults to the screen scale, so normalising orientation re-rendered at 3× the pixel dimensions: pure interpolation, no added detail, ~4× the bytes, and a visibly soft face that matches worse than the original. A selfie was landing at 1.5 MB instead of ~380 KB.
Android and iOS are now line-for-line equivalent here — same selection, same weights, same crop margins. The 0.0.9 rotation bug existed because the two platforms had drifted apart; closing that gap was deliberate.
Note on measuring this
If your Apenia deployment reports
provider: face baseline, these improvements are not measurable from the score — an identical capture scored 30 to 61 across runs on that provider. Judge the change by looking at the uploaded images. Seedoc/platform-findings.mdfor the server-side issues found while integrating against production. - Best-frame selection. Every aligned frame is now scored and the best one
is uploaded as both the selfie and the
-
0.0.920 Jul 2026Release notes
Open source →Fixes a severe capture bug in 0.0.8 — upgrade. Selfies were saved with the wrong orientation on both platforms, which fails server-side face match and liveness on a perfectly good capture.
- Fixed: rotated selfies. On iOS the frame was built as
.leftMirroredand saved without normalising —jpegDatawrites raw pixels plus an EXIF tag, and server decoders routinely ignore EXIF (PIL withoutexif_transpose, OpenCV). On AndroidImageProxy.toBitmap()discardsrotationDegreesentirely, so the frame was saved in sensor orientation with no EXIF hint at all. Both now rotate the pixels before writing. A sideways face reads as a failed liveness check ("possible photo or screen replay"), so this could decline every genuine user.
New payload — all additive
Existing
client_analysiskeys are unchanged; parsers built against 0.0.8 keep working.-
face_chip.jpg— a tight crop of the face from the primary frame (40% margin, quality 0.95), uploaded as an extra artifact. The full selfie spends most of its pixels on background; this gives the matcher far more detail on the only region it scores, for a couple of hundred KB. Flagged in the payload asliveness.face_chip. -
liveness.frames[]— face geometry per saved frame: yaw/pitch/roll, bounds, and eye/smile probabilities. Lets the server verify the prompted motion (eye-openness collapsing on a blink,smilingrising on a smile) instead of trustingchallenges_passed. Bounds are normalised to top-left fractions on both platforms, so Vision's bottom-left origin and ML Kit's pixel rect don't leak into the contract. -
account_match— the fake-account signal: the identity the customer registered with versus the identity the document asserts. SupplyApeniaOnboarding.expectedName/expectedIdNumberto enable it; the key is absent entirely otherwise. Names compare as token sets with case, accents and punctuation normalised, so ordering and a missing middle name don't read as a mismatch —subset("Moses Gathecha" vs "MOSES GATHECHA WAKANYI") is the normal case. Apostrophes are stripped rather than spaced, so O'Brien and N'Dour don't falsely mismatch.disjointis grounds for review, never automatic rejection — marriage, transliteration and registration typos all produce honest mismatches. And it proves only that the account matches the document: someone presenting a stolen genuine ID passes it perfectly. Face match is what stands there.
- Fixed: rotated selfies. On iOS the frame was built as
-
0.0.820 Jul 2026Release notes
Open source →First release exercised end-to-end against the live Apenia API. Every fix below came from real traffic, not from the docs — several are cases where the live API and the published docs disagree.
- Fixed: a valid response could crash the flow after a successful upload.
The live API returns
"code": "2000"as a string; the SDK read it withas num?, which throws on a type mismatch rather than yielding null. The upload succeeded, then parsing the result threw.code,identity_confidenceand the check scores now accept a number or a string. - Fixed: rejections could read as successes. A rejected verification comes
back with
"status": "completed"alongside"code": "2000"/"decision": "Rejected". Branching onstatuswould approve it. The SDK derives fromcodefirst (thendecision, thenstatus) — now verified against a real 2xxx response rather than inferred from the docs. - Fixed: scores were always blank. The docs document
{"score": 0.95, "match": true}; the live API sendsmatch_score/liveness_score/matchedon a 0–100 scale. Neither documented field exists in practice, so every score parsed as null. Both shapes are now read, and anything above 1 is normalised as a percentage. - Fixed:
ApeniaOnboardingsent an invalid flow. It usedflow: "onboarding", which the API rejects with HTTP 422. It now defaults tobiometric_kyc— the flow this screen actually performs — and is overridable via the newflowparameter.
Uploads
- Document images are ~85% smaller. The document frame is cropped out of a
full-resolution still, so a captured ID was landing at ~5 MB per side — a
measured archive was 11.1 MB, of which 10 MB was two ID photos, versus
~380 KB for a selfie. Images are now capped at 2000 px on the long edge
(1400 for
standard). The same capture now produces a 2.1 MB archive that uploads in under 7 s where 11.1 MB failed at 20.7 s. 2000 px is ~590 DPI for an ID-1 card, well above what MRZ OCR needs. - The upload is retried up to three times with a fresh connection and backoff. A multi-megabyte PUT from a handset is the most fragile call in the flow, and it previously had no resilience — one dropped packet ended the verification. Genuine server rejections (413, 422) are not retried.
- Separate timeouts.
uploadTimeout(3 min) andresultTimeout(3 min) no longer share the 30 srequestTimeoutmeant for small JSON calls.
Configuration
- BREAKING —
baseUrlis gone. The SDK resolves the Apenia host itself; integrators shouldn't hardcode it.baseUrlOverrideremains for tests, mocks and self-hosted deployments. - New:
sessionEndpoint+authToken/authTokenProvider. Point the SDK at your integration backend's session endpoint and hand it the signed-in user's bearer token;sessionProvideris now optional and reserved for cases that need full control. Session responses are accepted bare or wrapped in adataenvelope, and duplicate slashes in the path are collapsed (/v1//x404s on most routers). - New:
onDiagnostic. Receives artifact sizes, archive total, upload attempts, status codes and timings — the data needed to diagnose a capture failure on a real device, wheredebugPrintis stripped from release builds. Sizes and status codes only; never image bytes, tokens or personal data. - Error messages now read Laravel-style
{"message": …, "errors": {…}}bodies from integration backends, not just Apenia's{"detail": …}.
- Fixed: a valid response could crash the flow after a successful upload.
The live API returns
-
0.0.719 Jul 2026Release notes
Open source →- Sharpness on iOS. The focus proxy is now measured on both platforms with
the same computation (mean absolute horizontal luma gradient over the centre
of the frame), so the values are comparable across devices. Both platforms
also sample it on every analysed frame, so a manual shutter tap still carries
a reading. Note it is only available in
DocumentCaptureMode.auto, where the analyser runs — treat it as missing rather than zero elsewhere. - Cleaned up the published example. It is now a single, minimal, real
integration: base URL, publishable key,
sessionProviderpointing at your backend, andApeniaOnboarding. The fake capture platform and mock backend no longer ship with the package.
- Sharpness on iOS. The focus proxy is now measured on both platforms with
the same computation (mean absolute horizontal luma gradient over the centre
of the frame), so the values are comparable across devices. Both platforms
also sample it on every analysed frame, so a manual shutter tap still carries
a reading. Note it is only available in
-
0.0.619 Jul 2026Release notes
Open source →- Capture telemetry in
ClientAnalysis. The payload now describes how the capture went, not only what it produced — behavioural features the server's model can learn from:retake_count— how many captures the user rejected. Repeated retakes on one document is a stronger fraud signal than any single frame.manual_shutter— auto-detect never fired and the user forced the shot. Correlates with screens, photocopies and damaged cards.elapsed_ms— time on the capture screen; both tails are informative.sharpness— the measured focus value at capture (Android), so a borderline-but-accepted frame is distinguishable from a crisp one.
- README install now points at the published package rather than a path dependency, and documents exactly what the upload archive contains — the images and the on-device analysis ship together in one request.
- The example shows production and demo wiring side by side behind a
kUseMockBackendflag.
- Capture telemetry in
-
0.0.519 Jul 2026Release notes
Open source →Verified the client against the official docs at https://verify.apenia.com/docs and corrected two real mismatches.
- Branch on
code, not on scores.VerifyVerdictnow parses Apenia'scode,decisionandmessage, and derives its status from the code (1xxx approved · 2xxx rejected · 3xxx review · 4xxx failed), falling back todecisionand thenstatus. Previously the SDK inferred the outcome from the coarsestatusstring alone, which the docs explicitly warn against — a high face-match score can still accompany a rejection. AddedVerifyVerdict.isApprovedand aVerifyStatus.rejectedstate distinct fromfailed. - BREAKING —
ApeniaConfig.appKeyis required again. Apenia requires the publishableX-App-Keyalongside the sessionclient_tokenfor device reads; 0.0.3–0.0.4 sent only the bearer token and would have been rejected with 401 against the live API. The secret key still never touches the device. - Upload limits are enforced client-side (32 MB archive, 8 MB per file) so an oversized capture fails with a clear message instead of a server 413.
ApeniaApiExceptionnow exposes.detail(Apenia's{"detail": …}body) and.reason(401 auth · 404 not found · 410 expired · 413 too large · 422 invalid format).- The result screen shows Apenia's own
messagewhen present, and distinguishes a declined verification from a failed one.
- Branch on
-
0.0.419 Jul 2026Release notes
Open source →Device-level pre-screening: the SDK now reports what it worked out locally, and scores it.
ClientAnalysispayload. Everything derived on-device — MRZ fields, liveness challenges satisfied, capture mode / aspect / sides, SDK and platform — is packaged and sent to the server inside the upload manifest (client_analysis). SetApeniaConfig.analysisEndpointto additionally POST it to an ML validation service; that call is best-effort and never blocks a verification.- On-device check scorecard (
LocalChecks) with a weighted confidence, floored at 5% and capped at 97%:- MRZ check digits — full ICAO 9303 7-3-1 validation of the document number, date of birth, expiry and composite digits. Catches OCR misreads and altered data.
- Front vs MRZ cross-check — the printed face is OCR'd and its ID number, date of birth and name compared against what the machine-readable zone encodes. Disagreement is a forgery signal. Skipped for single-page documents rather than counted as a failure.
- Portrait present on the document, document in date, liveness challenges completed, all sides captured.
- Liveness results are no longer discarded — which challenges the user satisfied now reach the server.
- Auto-capture waits for focus. Text presence alone fired the shutter
before autofocus settled, which hit dense pages (passports) hardest. Capture
now requires a settled focus (
isAdjustingFocuson iOS, a luma-gradient sharpness measure on Android), a 1.2 s dwell, and stability that only accrues while sharp. Android also nudges autofocus when a document appears.
Scope, stated plainly: these are integrity and quality checks, not identity verification. The device cannot compare the selfie to the document portrait — neither ML Kit nor Vision expose face embeddings — so face matching, passive anti-spoofing and document authenticity remain server-side decisions. A high local confidence means the capture is internally consistent and usable, nothing more.
-
0.0.318 Jul 2026Release notes
Open source →- ~23 MB smaller on Android. Switched from the bundled ML Kit artifacts
(
com.google.mlkit:face-detection,:text-recognition) to the unbundled Play Services variants. The models are now served by Google Play Services instead of being shipped in the APK. Measured on a release arm64 build, the SDK's footprint drops from +25.5 MB to +1.8 MB. - The plugin manifest now requests the
faceandocrmodels at install time so the first capture doesn't stall on a download. - Breaking for offline/non-GMS devices: face detection and MRZ OCR now require Google Play Services. If you need the SDK to work without it, pin 0.0.2, which bundles the models.
- ~23 MB smaller on Android. Switched from the bundled ML Kit artifacts
(
-
0.0.218 Jul 2026Release notes
Open source →- Fix dependency conflict.
archivewas pinned to^3.6.1, which made the package unresolvable in any app depending onimage4.x (e.g. viaflutter_native_splash). The constraint is now>=3.6.1 <5.0.0; verified building and passing tests against both 3.6.1 and 4.0.9. - Corrected stale documentation: the capture flows are device-tested on
Android and iOS, and the architecture notes no longer refer to VisionKit or
the old
prep_uploadcontract.
- Fix dependency conflict.
-
0.0.118 Jul 2026Release notes
Open source →Initial release — KYC capture SDK for Apenia Verify.
- Single-entry onboarding flow (
ApeniaOnboarding): intro → choose document → document capture → live selfie → upload → result, in Apenia branding. - Document capture with a fixed, document-shaped enclosure (ID-1 card and passport data-page ratios) on a contained camera. Built on CameraX (Android) and AVFoundation (iOS) rather than the OS document scanner, so there are no enhancement, filter or re-crop options; the image is cropped to the frame.
- Confirm / retake step after each captured side.
- Auto or manual capture via
ApeniaConfig.documentCaptureMode. Auto only fires once the document is detected inside the frame; the shutter always works as a manual override. - On-device MRZ extraction (ML Kit on Android, Vision on iOS) parsing TD1 and 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.
- Active-liveness selfie with a controlled face-scanner: oval overlay and a positioning gate (centred, sized, frontal, eyes open) before randomised blink / turn / smile challenges. Challenges rotate if a signal won't trigger. Capture only succeeds if the full sequence completes with a face present throughout — there is no static-image path.
- Upload client implementing the onboarding-session contract (create session → pre-signed upload → poll / IPN), with no secret key on the device.
- Web/desktop safe: no top-level
dart:io, so the package compiles for web.
- Single-entry onboarding flow (