Result contract — Face Local SDK#
SDK 0.5+ · schemaVersion: "1.0" · exported type: VerificationResult.
This document describes the result of runFaceLocalVerification, not the
previous API's internal report. These details are for the integrating app;
they do not need to appear in the user's onboarding screens.
1. What “approved” means#
approved: true means that a native capture passed liveness and matched a local,
single-face photo with eligible EXIF for the same device model and similarity
equal to or greater than the configured threshold.
It does not confirm legal identity, a name, an identity document, phone ownership or EXIF authenticity. A person may appear in photos from another device of the same model. Metadata can also be copied or edited.
There is no upload, remote face lookup, biometric database registration or manual reference-image selection. Templates are internal to the native module, temporary and never returned in the JSON.
2. Status and approval#
status | approved | Meaning |
|---|---|---|
approved | true | First eligible match; the search stops immediately |
not_approved | false | Liveness rejected, or eligible search completed without a match |
inconclusive | null | Insufficient evidence for a decision |
cancelled | null | The user or integration cancelled the attempt |
error | null | Permission, environment, evidence or another operation prevented completion |
Never convert null into “rejected”. Use status for control flow.
not_approved means the attempt did not meet the criteria, not that the person
is an impostor or does not own the phone.
Common reasons:
reason | Interpretation |
|---|---|
match-found | An approved comparison exists |
no-match | Valid comparisons completed below the threshold |
liveness-failed | Capture was not approved; there is no exportable reference |
no-eligible-photos | No eligible candidates |
photos-missing-metadata | Required EXIF fields are missing |
different-device-model | Candidate photos report another manufacturer/model |
no-single-face | No valid single-face comparison was obtained |
search-limit | The limit was reached with photos still unexamined |
unreadable-photos | Reading was incomplete |
comparison-failed | The engine did not complete required comparisons |
photo-full-access-required | The system granted limited access or did not report full scope |
photo-permission | Photo access denied |
cancelled | Attempt cancelled |
reference-expired | The reference is no longer valid |
evidence-unavailable | Native binary predates the export API |
evidence-invalid | Photos, session, score or metadata do not match the result |
cleanup-failed | Cleanup could not be confirmed |
Additional errors use the sanitized native code, lowercased and hyphenated, in
reason. errorCode retains its uppercase form, such as EVIDENCE_EXPIRED.
Do not use error messages as business logic; unknown codes must also block
approval. Stack traces and internal messages are not included.
An unreadable photo does not prevent a valid match found later. However, integrity errors, expiry, cancellation, export failures or cleanup failures abort the entire attempt, with no partial approval.
3. Similarity is not accuracy#
The methods used are evaluateFaceSimiByFeature on Android and
evaluateSimilarity(f1:f2:) on iOS. The adapter receives a similarity score from
the engine, not a calibrated estimate of identity probability.
| Field | Description |
|---|---|
faceMatch.similarity | Original score of the selected comparison; null without a valid comparison |
faceMatch.threshold | Actual threshold applied, default 0.83 |
faceMatch.similarityPercent | A 0–100 display index; null without a score |
faceMatch.thresholdPercent | Threshold on the same display scale |
faceMatch.percentageIsProbability | Always false |
faceMatch.selectedComparison | matched, best-compared or none |
The index is calculated as follows:
Math.round(Math.max(0, Math.min(1, similarity)) * 10000) / 100The native score must be finite and within -1 to 1. Negative values remain
unchanged in similarity, but are displayed as zero on the 0–100 scale.
The decision uses the unrounded original score, compared with >= threshold.
For example, 0.82996 may display as 83, but does not meet a threshold of 0.83.
A score of 0.91 displays as 91. That does not mean 91% certainty, 91%
accuracy or a 9% chance of fraud.
Accuracy requires a labeled dataset of same-person and different-person pairs,
calibration and held-out validation, including false acceptance/rejection and
conditions representative of the intended use. An isolated benchmark is not
the accuracy of your application or of a particular photo.
The default threshold has not been calibrated for your scenario; there is no
fabricated accuracy field.
4. Which photo and comparison are returned#
At most two copies are returned:
photos.liveness: the image from the same approved live capture. The caller cannot provide a replacement reference.photos.compared: the image actually analyzed in the first comparison that passes the threshold. If there is no match, it is the best valid comparison, with the highest score; the first wins a tie.
Without a valid comparison, photos.compared is null.
If liveness fails, or the attempt is cancelled or ends in error, both are null.
An inconclusive result may contain a best-compared pair, but that does not make
the attempt approved or rejected.
The photo, score and EXIF refer to the same selected pair, with native session binding. The JSON does not expose original filenames, gallery identifiers or the full list of library candidates.
Each photo contains:
| Field | Description |
|---|---|
uri | file:///... in the app's private cache; not a public URL |
mimeType | image/jpeg |
width, height | Copy dimensions in pixels |
sizeBytes | JPEG size, limited to 8 MiB |
sha256 | Hash of the JPEG copy's bytes; 64 hexadecimal characters |
representation | normalized-jpeg |
base64 | Only present when requesting format: "base64"; no data: prefix |
These are normalized/recompressed copies of images already used for inference, not byte-for-byte originals. Export does not reread a gallery file that could have changed. Orientation is normalized; original EXIF, GPS and other metadata are not embedded in the exported JPEG.
The score was calculated on the engine's image before export recompression. Matching the exported JPEGs again may produce a numerically different score. The hash is not a manufacturer signature, proof of camera origin, a hash of original EXIF or external integrity attestation.
5. EXIF evidence#
exif is null when there is no selected valid comparison. When present:
| Field | Description |
|---|---|
source | original-photo-bytes: native reading of the same bytes decoded for the engine |
comparison | matched or best-compared, consistent with the selected pair |
fields.Make | Manufacturer recorded in the original EXIF |
fields.Model | Model recorded in the original EXIF |
fields.DateTimeOriginal | Original date as EXIF text, without inventing a time zone |
expected | Capture manufacturer/model and any verified hardware code |
manufacturerMatches, modelMatches | Results of the matching rule |
matchRule | exact-model-or-verified-hardware-alias |
editable | Always true: EXIF can be changed |
provesDeviceOrigin | Always false |
All three fields are required. The filter normalizes only whitespace and case; it does not accept approximate matches from the same model family. An alternative hardware code requires an explicitly verified equivalence in the native module. EXIF never teaches the SDK what the current device model should be.
DateTimeOriginal is a required presence check, not forensic validation of
chronology or authenticity. Shared/edited dates may be false.
GPS, location, serial numbers, MakerNote and full EXIF are not returned.
6. Device Fingerprint: a profile, not a unique identity#
In this contract, deviceFingerprint describes a model profile:
kind: "model-profile"
make: manufacturer used for capture
model: exact name/code resolved natively
hardwareModel?: actual system code, when a verified Android alias exists
platform: android or ios
osVersion: version reported by the system, or null if unavailable
uniqueDevice: false
provesOwnership: falseThe object is returned only after an approved capture, bound to the current device. On Android, the manufacturer/code comes from the system and verified equivalences. On iPhone, DeviceKit resolves the model locally. An unknown model is rejected; it never becomes simply “iPhone” or “Android”.
This contract does not collect IDFA, IDFV, IMEI, MAC, serial numbers or a user-defined device name. It does not build a tracking identifier. Thousands of units share the same profile. Therefore:
- A matching
Modeldoes not prove the photo came from that physical unit. - Similar photos in the album do not prove phone ownership.
- Adding the OS version does not make the profile unique.
- This field does not replace App Attest, Play Integrity or legal identity.
7. Decision audit#
| Field | Description |
|---|---|
checks.livenessPassed | true after validated capture, false for explicit rejection, null without a conclusion |
checks.faceMatchPassed | true for a positive result, false after a completed search without a match, otherwise null |
checks.exifModelMatched | Result for the selected comparison; null if there was none |
decision.policy | local-face-continuity-v1 |
decision.liveness | Actual score, exclusive minimum 0.85, method passive-active and source native-camera; or null |
decision.engine | Provider, engine version and platform; or null if unavailable |
decision.requirements | Camera with liveness, full permission, current-model EXIF, a single face and the configured threshold |
decision.comparedPhotos | Number of pairs with one face and a valid similarity |
decision.nativeAttempts | Native comparison calls, including no-face results and failures |
decision.stoppedOnFirstMatch | true only when approval was returned |
decision.exclusions | Aggregate counts by reason, without listing album contents |
Liveness also requires the camera sequence and local integrity checks; it is not decided solely by the scalar value. The liveness score is not a calibrated probability either.
Counts describe only the report that actually completed. An abort/error without a final report can have zero counts even if an attempt occurred; they are not a permanent processing log.
verificationId is a random session identifier, not a person or device identifier.
completedAt is an ISO 8601 UTC timestamp based on the local clock, not an
attested time source. processing: "on-device" describes this integration's path.
identityVerified: false prevents interpreting the result as legal-identity verification.
8. Format example#
Synthetic, abbreviated example, with illustrative values, never usable as an actual verification result:
{
"schemaVersion": "1.0",
"verificationId": "random-session-identifier",
"status": "approved",
"approved": true,
"reason": "match-found",
"errorCode": null,
"checks": {
"livenessPassed": true,
"faceMatchPassed": true,
"exifModelMatched": true
},
"faceMatch": {
"similarity": 0.91,
"threshold": 0.83,
"similarityPercent": 91,
"thresholdPercent": 83,
"percentageIsProbability": false,
"selectedComparison": "matched"
},
"processing": "on-device",
"identityVerified": false
}Do not decide from a rounded percentage, an isolated photo, EXIF alone or display
text. Use the final status. The complete JSON is sensitive even without Base64.
9. Integrity and limitations#
The template is cleared before returning the result; image copies have a separate TTL. See the integration and cleanup guide. Cancellation, validation or cleanup failures must not yield approval.
Native camera capture, EXIF/session binding and consistency checks between images, hashes and scores reduce known substitutions. They do not make a hostile process trustworthy. Root/jailbreak, hooks, compromised drivers or a modified app can bypass local checks.
The JSON is not externally signed or remotely attested and is not irrefutable proof. Unit tests and a successful build do not validate real accuracy or security. The iOS implementation has been tested on an iPhone. Build your integration with Mac/Xcode and validate your app's biometrics, network and storage behavior on a physical device.