Integrate the structured result#
First read the installation, permissions and Release-build requirements in the README. The SDK does not work in Expo Go or approve in a browser.
APIs#
| Import | API |
|---|---|
@workspace/face-local-sdk/expo | runFaceLocalVerification(options): Promise<VerificationResult> |
@workspace/face-local-sdk/expo | releaseFaceLocalVerification(verificationId): Promise<void> |
@workspace/face-local-sdk/expo | createExpoEngine(): EvidenceEngine |
@workspace/face-local-sdk | runVerification(options) for a custom adapter |
@workspace/face-local-sdk | Types VerificationResult, VerificationOptions, EvidencePhoto, EvidenceEngine |
runFaceLocalVerification handles photo authorization, discovery and Face Match.
The app provides consent, the camera-permission request and optional UI callbacks.
There is no intermediate search or compare button, and no reference-image picker.
Integration example#
The UI functions below belong to the integrating app, not the SDK:
showResult presents the result and resolves only when the user closes the screen.
This avoids removing files while the screen still needs them.
import {
FaceLocalError,
type Consent,
type VerificationResult,
} from "@workspace/face-local-sdk";
import {
runFaceLocalVerification,
releaseFaceLocalVerification,
} from "@workspace/face-local-sdk/expo";
import { requestCameraPermissionsAsync } from "expo-image-picker";
async function verify(
consentFromYourUI: Consent,
signal: AbortSignal,
showResult: (result: VerificationResult) => Promise<void>,
) {
let result: VerificationResult | undefined;
try {
result = await runFaceLocalVerification({
// Consent must come from your interface, not be assumed by the SDK.
consent: { ...consentFromYourUI, photoLibrary: false },
signal,
policy: {
matchThreshold: 0.83,
maxPhotos: 100,
captureTimeoutSeconds: 15,
requireCaptureMetadata: true,
},
evidence: {
format: "uri", // Default. No Base64 in the object.
ttlSeconds: 300, // 30–300 seconds; default 300.
},
requestCameraAccess: async () => {
// Requests permission; does NOT open a still camera or an image picker.
const permission = await requestCameraPermissionsAsync();
if (!permission.granted) {
throw new FaceLocalError("CAMERA_PERMISSION", "Camera not authorized.");
}
},
});
await showResult(result);
} finally {
if (result?.verificationId) {
// Await and handle failures; do not claim cleanup that did not happen.
await releaseFaceLocalVerification(result.verificationId);
}
}
}Display the returned images with React Native's Image:
{result.photos.liveness && (
<Image source={{ uri: result.photos.liveness.uri }} style={{ width: 160, height: 200 }} />
)}
{result.photos.compared && (
<Image source={{ uri: result.photos.compared.uri }} style={{ width: 160, height: 200 }} />
)}Remove references from the interface and any image caches after release or expiry. The images are temporary, not permanent profile photos.
Status: distinguish rejection from an inability to decide#
switch (result.status) {
case "approved":
// Continue onboarding; approved === true.
break;
case "not_approved":
// The attempt did not meet the criteria; approved === false.
break;
case "inconclusive":
// Insufficient evidence; approved === null.
break;
case "cancelled":
// End the attempt without treating cancellation as rejection.
break;
case "error":
// Handle errorCode: permission, environment, integrity, expiry or cleanup.
break;
}The result uses plain objects that can be serialized with JSON.stringify(result).
Serialization does not mean saving, logging or sending it to a server.
The JSON contains biometrics, private paths and sensitive metadata.
Failures in the high-level API flow are returned as structured statuses, including invalid options and callbacks that throw errors. Explicit cleanup operations return a Promise that can reject; handle that error in the app. The previous APIs still throw errors.
Options and callbacks#
| Option | Rule |
|---|---|
consent | camera and biometricProcessing must be true after consent; photos start unauthorized |
requestCameraAccess | Must request actual permission and throw on denial |
signal | Optional AbortSignal to cancel an in-progress attempt |
policy.matchThreshold | Finite number from 0.5 to 0.99; default 0.83 |
policy.maxPhotos | Integer from 1 to 1000; default 100 |
policy.captureTimeoutSeconds | Integer from 6 to 22; default 15 |
policy.requireCaptureMetadata | Must be true; there is no mode without the EXIF filter |
evidence.format | uri by default, or explicitly base64 |
evidence.ttlSeconds | Integer from 30 to 300, starting at the first native export |
onPhase | Onboarding phase, for updating the UI |
onCapture | Receipt without image/template; do not alter the session based on this callback |
onSearchProgress | Library discovery progress |
onProgress | Comparison progress |
The operating system controls permissions. You cannot force “full access” or hide the limited-access option. The API requires actual full scope; limited or denied access ends the flow without scanning unauthorized photos. The filter does not upload/download cloud originals or request GPS permission.
Optional Base64#
Use evidence.format: "base64" only when a local consumer needs the bytes in
JavaScript. The result still includes local URIs; base64 contains only the
encoded data, without a data: prefix.
This increases memory usage. Do not put it in AsyncStorage, a database, the clipboard, analytics or crash reports. The SDK cannot revoke or zero a JavaScript string that your code has already copied. TTL and release remove the native files, not copies of strings held by consumers; remove your own references.
Retention, expiry and cleanup#
There are two separate lifecycles:
- The facial reference and embedding remain in native memory for at most five minutes. They are cleared at the end of the scan or on error and are never returned in JSON.
- The new API explicitly requests up to two normalized JPEG copies in private cache, without original EXIF/GPS. These are the only images exported.
Before export, the five-minute capture limit applies. After the first native
export, the evidence TTL is 30–300 seconds, defaulting to 300. Reading the evidence
again does not extend it. expiresAt is an ISO timestamp for consumers; the native
implementation also uses a monotonic deadline.
Cleanup occurs on:
releaseFaceLocalVerification(verificationId);- cancellation or error before delivery;
- starting a new verification;
engine.clear();- expiry, when the process can run;
- native-module initialization after a process restart.
On iOS, moving the app into the background clears the session. On Android, the host must explicitly end the flow when appropriate; entering the capture Activity does not count as leaving the app.
If the operating system suspends or kills the process, a timer cannot guarantee
removal at the exact second. Access checks and startup cleanup complement the
timer. A file:// URI is not a cryptographic expiry token: respect expiresAt
and release explicitly. TTL is a maximum, not a guarantee of availability;
system cleanup, revocation or other lifecycle events can remove files earlier.
There is no guarantee of forensic erasure from flash storage or external caches.
Release is idempotent and scoped to the matching verification ID, never an arbitrary path. Releasing an older ID does not clear a newer session. Original gallery photos are never deleted. If removal fails, the session binding is retained so a later attempt does not falsely report that cleanup already occurred.
Android uses private cache excluded from backup. iOS uses complete file protection and excludes the files from backup. These measures do not protect against a compromised operating system.
Cancel and prevent late results#
const controller = new AbortController();
// Pass controller.signal to runFaceLocalVerification.
// To cancel the in-progress attempt:
controller.abort();
// Await the verification Promise; the SDK discards late callbacks.After the Promise returns a result, use releaseFaceLocalVerification to remove
the copies; aborting an already completed signal is not the cleanup API.
Do not run parallel verifications: the SDK has one active native session per
process. A second high-level call returns BUSY without deleting the first.
Do not mix manual capture/comparison calls with an in-progress onboarding flow.
Migrate from the previous report#
| Previous API | New API |
|---|---|
runFaceLocalOnboarding | runFaceLocalVerification |
outcome: "positive" | status: "approved", approved: true |
outcome: "negative" | status: "not_approved", approved: false |
outcome: "inconclusive" | status: "inconclusive", approved: null |
| Thrown errors | error or cancelled status, with reason/code |
| Report with library candidates | Filtered JSON with up to two private copies |
| No capture export | Explicit export with TTL and mandatory cleanup |
The SDK retains the previous APIs for compatibility; they do not start saving image copies automatically. The demo app may still use the previous flow until it is migrated. This does not limit use of the new API in another app.
Install package 0.5+, rebuild the binary and reinstall it. Changing only Metro's
JavaScript or installing a .tgz does not update an installed Android/iOS binary.
Without capabilities().evidenceVersion === 1, the new API returns
EVIDENCE_UNAVAILABLE. There is no fallback that fabricates photos, EXIF, a score
or approval.
Custom adapter#
runVerification receives an EvidenceEngine, requestPhotoAccess and
readPhotos, in addition to shared options. Detailed types are exported by the
package. Permission must come from the operating system, with
accessPrivileges: "all".
The adapter must:
- preserve all
BiometricEngineprotections; - implement
captureReferenceWithEvidencewithout accepting external images, URIs or frames; - return native EXIF in
NativeComparison.captureMetadata, from the same bytes analyzed by the engine; - retain the best comparison until the first match, without associating images from another session;
- implement
clearReferenceto remove the template/camera while preserving only the explicitly requested copies for delivery; - implement
getEvidenceafter clearing the reference, with the session, score, original compared URI, EXIF, normalized copies, hash and actual deadline; this internal object must not be exposed directly as integration JSON; - implement
releaseEvidenceandclearwith actual removal, limited to files owned by the SDK, and report removal failures; - respect TTL without extending it on repeated reads, and reject exports from expired or mismatched sessions.
Satisfying a TypeScript interface is not proof of a working biometric implementation. Deterministic test doubles do not validate real liveness, similarity or attack resistance.
Checklist before integrating into another app#
- Use a Release build on a physical device, not Expo Go/web.
- Obtain actual permissions and consent; continue automatically after liveness.
- Do not provide a reference-image picker/importer.
- Handle
status,approvedandreasonseparately. - Keep results/photos in UI memory, without logs, persistence or upload.
- Do not present similarity as accuracy/probability.
- Do not advertise EXIF or the model profile as proof of ownership.
- Release explicitly on completion/close; understand expiry and Base64.
- Preserve the original gallery after release, cancellation and expiry.
- Rebuild the native binary, including the native export code.
- Run real Android/iPhone tests, including backgrounding, late results and airplane mode.
- Review the plugin policy: it removes INTERNET from the entire Android Release app.
- Respect the Face Local terms: own-brand use is permitted; SDK resale is prohibited.