NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev
Apenia Verify — KYC capture SDK. Native active-liveness selfie + document scan; matching runs server-side in Apenia Verify.
Last release 2 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
2 months old
19 releases · first in 2026
One column per month.
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 e
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 t
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. 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.Verification wait is now a legible stepper. The single indefinite spinner became a two-step progress list — *Uploading your capture* → *Matching your
SubmitPhase. A minute-long server wait reads as steady progress rather
than a hang, with a "keep this screen open" reassurance."Document captured" confirmation. After the ID scan, a small elegant modal card — floating over a dimmed scrim, with a success mark and a *Continue* b
ApeniaModalCard, in
Apenia red.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
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.Result screen: privacy-first, and a clear action per outcome.
Result screen: privacy-first, and a clear action per outcome.
onComplete fires 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.Fixes polling against the current Apenia API — upgrade. Verdicts were never being read, so the flow polled until timeout on every job.
Fixes polling against the current Apenia API — upgrade. Verdicts were never being read, so the flow polled until timeout on every job.
schema_version: "1.0" response envelope. Production nests the
verdict under verdict (decision, code, reason, confidence) and no
longer sends top-level status/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).4010 (waiting for
upload) and 4020 (scoring) share the 4xxx family with real failures; the
old family bucket mapped them to failed, which would stop polling and show a
failure on a job that was merely still running. Now 4010 → pending,
4020 → processing (keep polling), 4030 → expired, 9010 → rejected,
matching the server's own overrides.verdict.reason (v1.0) as well as the old message,
and identity confidence reads verdict.confidence.Branch on VerifyVerdict.isApproved / .decision as before — the mapping is
unchanged, only where it's read from.
UX around submission, and illustration-led onboarding screens.
UX around submission, and illustration-led onboarding screens.
submitArtifacts reports its stage
through an onPhase callback (creatingSession → uploading → verifying),
and ApeniaOnboarding shows a matching screen for each. A multi-minute server
wait no longer sits under a single "uploading" spinner.onDiagnostic
(poll N -> processing (code …)), and a poll timeout is logged distinctly.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 dow
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.
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.UIGraphicsImageRenderer defaults 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.
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. See
doc/platform-findings.md for the server-side
issues found while integrating against production.
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 a
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.
.leftMirrored and
saved without normalising — jpegData writes raw pixels plus an EXIF tag,
and server decoders routinely ignore EXIF (PIL without exif_transpose,
OpenCV). On Android ImageProxy.toBitmap() discards rotationDegrees
entirely, 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.Existing client_analysis keys 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
as liveness.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, smiling rising on a smile)
instead of trusting challenges_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. Supply
ApeniaOnboarding.expectedName / expectedIdNumber to 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.
disjoint is 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.
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 th
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.
"code": "2000" as a string; the SDK read it with
as num?, which throws on a type mismatch rather than yielding null. The
upload succeeded, then parsing the result threw. code, identity_confidence
and the check scores now accept a number or a string."status": "completed" alongside "code": "2000" /
"decision": "Rejected". Branching on status would approve it. The SDK
derives from code first (then decision, then status) — now verified
against a real 2xxx response rather than inferred from the docs.{"score": 0.95, "match": true}; the live API sends match_score /
liveness_score / matched on 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.ApeniaOnboarding sent an invalid flow. It used
flow: "onboarding", which the API rejects with HTTP 422. It now defaults to
biometric_kyc — the flow this screen actually performs — and is overridable
via the new flow parameter.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.uploadTimeout (3 min) and resultTimeout (3 min) no
longer share the 30 s requestTimeout meant for small JSON calls.baseUrl is gone. The SDK resolves the Apenia host itself;
integrators shouldn't hardcode it. baseUrlOverride remains for tests, mocks
and self-hosted deployments.sessionEndpoint + authToken / authTokenProvider. Point the SDK
at your integration backend's session endpoint and hand it the signed-in
user's bearer token; sessionProvider is now optional and reserved for cases
that need full control. Session responses are accepted bare or wrapped in a
data envelope, and duplicate slashes in the path are collapsed (/v1//x
404s on most routers).onDiagnostic. Receives artifact sizes, archive total, upload
attempts, status codes and timings — the data needed to diagnose a capture
failure on a real device, where debugPrint is stripped from release builds.
Sizes and status codes only; never image bytes, tokens or personal data.{"message": …, "errors": {…}} bodies
from integration backends, not just Apenia's {"detail": …}.Sharpness on iOS. The focus proxy is now measured on both platforms with the same computation (mean absolute horizontal luma gradient over the centre
DocumentCaptureMode.auto, where the
analyser runs — treat it as missing rather than zero elsewhere.sessionProvider pointing at your
backend, and ApeniaOnboarding. The fake capture platform and mock backend
no longer ship with the package.Capture telemetry in `ClientAnalysis`. The payload now describes *how* the capture went, not only what it produced — behavioural features the server's
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.kUseMockBackend flag.Verified the client against the official docs at and corrected two real mismatches.
Verified the client against the official docs at https://verify.apenia.com/docs and corrected two real mismatches.
code, not on scores. VerifyVerdict now parses Apenia's
code, decision and message, and derives its status from the code
(1xxx approved · 2xxx rejected · 3xxx review · 4xxx failed), falling back to
decision and then status. Previously the SDK inferred the outcome from
the coarse status string alone, which the docs explicitly warn against — a
high face-match score can still accompany a rejection. Added
VerifyVerdict.isApproved and a VerifyStatus.rejected state distinct from
failed.ApeniaConfig.appKey is required again. Apenia requires the
publishable X-App-Key alongside the session client_token for 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.ApeniaApiException now exposes .detail (Apenia's {"detail": …} body)
and .reason (401 auth · 404 not found · 410 expired · 413 too large ·
422 invalid format).message when present, and
distinguishes a declined verification from a failed one.Device-level pre-screening: the SDK now reports what it worked out locally, and scores it.
Device-level pre-screening: the SDK now reports what it worked out locally, and scores it.
ClientAnalysis payload. 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). Set ApeniaConfig.analysisEndpoint to additionally POST
it to an ML validation service; that call is best-effort and never blocks a
verification.LocalChecks) with a weighted confidence,
floored at 5% and capped at 97%:
isAdjustingFocus on 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.
~23 MB smaller on Android. Switched from the bundled ML Kit artifacts (com.google.mlkit:face-detection, :text-recognition) to the unbundled Play Servi
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.face and ocr models at install time
so the first capture doesn't stall on a download.Fix dependency conflict. archive was pinned to ^3.6.1, which made the package unresolvable in any app depending on image 4.x (e.g. via flutter_native_
archive was pinned to ^3.6.1, which made the
package unresolvable in any app depending on image 4.x (e.g. via
flutter_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.prep_upload contract.Initial release — KYC capture SDK for Apenia Verify.
Initial release — KYC capture SDK for Apenia Verify.
ApeniaOnboarding): intro → choose document
→ document capture → live selfie → upload → result, in Apenia branding.ApeniaConfig.documentCaptureMode. Auto only
fires once the document is detected inside the frame; the shutter always
works as a manual override.dart:io, so the package compiles for web.Your coding agent can read these notes before it upgrades. Set up the MCP server →