Developer ManualVeris Capture
Guide 2 of 4

Veris Capture

On-device face capture with a quality gate and liveness detection. Every result is signed, and no image ever leaves the device to Veris servers.

01Overview

Veris Capture runs entirely on-device. When a user completes a capture session, the SDK returns a signed result (signedResult) that carries a hash of the captured image along with quality and liveness scores. Your backend sends this result to Veris to be verified, and decides whether to proceed - all without Veris ever seeing the actual face image.

The SDK handles:

  • Quality gate - 12 checks including lighting, blur, occlusion, and face positioning
  • Human face validation - confirms the image contains a real human face
  • Passive liveness (LBP) - detects spoofing from printed photos and screens (Launch, Scale, and Pro)
  • Active liveness challenges - requires the user to perform specific facial actions (Scale: 1 random; Pro: 2–4 configurable)
  • Video capture - records the full session as a video (Pro only)
◆

Veris Capture does not perform identity matching. It confirms that a live human face was captured and returns a signed record of that event. Use Veris Compare to match the captured face against a reference image.

02Quick start

Pick your platform below. The integration follows the same three-step pattern across all platforms: install the dependency, init once at startup, start a capture session when you need one.

1. Add the dependency

build.gradle (app module)
dependencies {
    implementation 'com.verisinfra:veris-capture-android:1.3.0'
}

2. Register for results

Declare the launcher as a field in your Activity. This must be registered before onStart, so declare it at the top level - not inside a function.

Kotlin
class MainActivity : AppCompatActivity() {

    // Declare as a field - must be registered before onStart
    val verisLauncher = VerisCapture.registerForResult(this) { result ->
        when (result) {
            is VerisResult.Success              -> verifyWithBackend(result)
            is VerisResult.Failure              -> showError(result.humanReadableReason)
            is VerisResult.Cancelled            -> { /* user dismissed */ }
            is VerisResult.SubscriptionInactive -> showUpgradePrompt()
        }
    }
}

3. Initialise in onCreate

Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    VerisCapture.init(
        context  = this,
        config   = VerisConfig(
            licenseKey = "veris_sandbox_reg_xxxx",
            initNonce  = fetchNonceFromBackend(),
        ),
    ) { initResult ->
        if (initResult is VerisInitResult.Ready) {
            // SDK ready - initResult.licenseInfo.featureSummary available
        }
    }
}

4. Start a capture session

Call this each time you need a verification. Always fetch a fresh nonce from your backend for each session.

Kotlin
VerisCapture.startCapture(
    launcher = verisLauncher,
    config   = VerisSessionConfig(nonce = fetchNonceFromBackend()),
)

Pro: configuring active liveness rounds

Active liveness uses an active liveness challenge: the user performs randomised facial actions such as head turns, blinks, or nods. Pro plans allow 2–4 rounds with independently randomised directions.

enforceChallenge = true forces the active liveness challenge to run even when passive liveness has already passed. Use this when your use case requires the strongest possible assurance — for example, high-value transactions or regulated onboarding flows. By default the SDK skips active liveness if passive already passed, keeping the session as frictionless as possible.

Kotlin
// 3 active liveness rounds (Pro only, 2–4)
VerisSessionConfig(
    nonce                   = fetchNonceFromBackend(),
    proRandomChallengeCount = 3,
)

// Always require active liveness, even if passive already passed (Pro only)
VerisSessionConfig(
    nonce            = fetchNonceFromBackend(),
    enforceChallenge = true,
)

1. Add the dependency

Podfile
pod 'VerisCapture', '~> 1.3'

Or with Swift Package Manager - add https://github.com/Veris-Lab/veris-capture-ios in Xcode.

Requires iOS 15.0 or later. Set platform :ios, '15.0' in your Podfile.

2. Initialise once

Swift
VerisCapture.initialise(
    config: VerisConfig(
        licenseKey: "veris_sandbox_xxxx",
        initNonce: nonceFromBackend
    )
) { result in
    if case .ready = result { /* SDK ready */ }
}

3. Start a capture session

Call this each time you need a verification. Always fetch a fresh nonce from your backend for each session.

Swift
VerisCapture.startVerification(
    from: self,
    session: VerisSessionConfig(nonce: nonceFromBackend)
) { [weak self] result in
    switch result {
    case .success(let capture):
        self?.verifyWithBackend(capture.signedResult)
    case .failure(_, let reason, let retryable):
        self?.showError(reason, canRetry: retryable)
    case .cancelled:
        break
    case .subscriptionInactive:
        self?.showUpgradePrompt()
    case .error(_, let message):
        self?.showError(message, canRetry: true)
    }
}

Pro: configuring active liveness rounds

Active liveness uses an active liveness challenge: the user performs randomised facial actions such as head turns, blinks, or nods. Pro plans allow 2–4 rounds.

enforceChallenge forces the active liveness challenge to run even when passive liveness has already passed. Use this for high-assurance flows. By default the SDK skips active liveness if passive already passed.

Swift
// 3 active liveness rounds (Pro only, 2–4)
VerisSessionConfig(nonce: nonceFromBackend, proRandomChallengeCount: 3)

// Always require active liveness, even if passive already passed (Pro only)
VerisSessionConfig(nonce: nonceFromBackend, enforceChallenge: true)

1. Add the dependency

pubspec.yaml
dependencies:
  veris_capture: ^1.3.0

iOS: Set platform :ios, '15.0' in ios/Podfile, then run cd ios && pod install.

2. Initialise once

Call this once at app startup, typically in main() or your root widget's initState.

Dart
await VerisCapture.instance.initialise(
  config: VerisConfig(
    licenseKey: 'veris_sandbox_xxxx',
    initNonce: await fetchNonceFromBackend(),
  ),
);

3. Start a capture session

Always fetch a fresh nonce from your backend for each session.

Dart
final result = await VerisCapture.instance.startVerification(
  session: VerisSessionConfig(nonce: await fetchNonceFromBackend()),
);

switch (result) {
  case VerisSuccess s:
    verifyWithBackend(s.signedResult);
  case VerisFailure f:
    showError(f.humanReadableReason);
  case VerisCancelled _:
    break;
  case VerisSubscriptionInactive _:
    showUpgradePrompt();
  case VerisError e:
    showError(e.message);
}

Pro: configuring active liveness rounds

Active liveness uses an active liveness challenge: the user performs randomised facial actions such as head turns, blinks, or nods. Pro plans allow 2–4 rounds.

enforceChallenge forces the active liveness challenge to run even when passive liveness has already passed. Use this for high-assurance flows. By default the SDK skips active liveness if passive already passed.

Dart
// 3 active liveness rounds (Pro only, 2–4)
final result = await VerisCapture.instance.startVerification(
  session: VerisSessionConfig(
    nonce: await fetchNonceFromBackend(),
    proRandomChallengeCount: 3,
  ),
);

// Always require active liveness, even if passive already passed (Pro only)
final result = await VerisCapture.instance.startVerification(
  session: VerisSessionConfig(
    nonce: await fetchNonceFromBackend(),
    enforceChallenge: true,
  ),
);

1. Install the package

Terminal
npm install @verisinfra/react-native-capture

# iOS - install native pods
cd ios && pod install

2. Initialise once

TypeScript
import { VerisCapture } from '@verisinfra/react-native-capture';

await VerisCapture.initialise({
  licenseKey: 'veris_sandbox_xxxx',
  initNonce: await fetchNonceFromBackend(),
});

3. Start a capture session

Always fetch a fresh nonce from your backend for each session. The call always resolves; check the type field.

TypeScript
const result = await VerisCapture.startVerification({
  nonce: await fetchNonceFromBackend(),
});

switch (result.type) {
  case 'success':
    verifyWithBackend(result.signedResult);
    break;
  case 'failure':
    Alert.alert('Verification failed', result.reason);
    break;
  case 'cancelled':
    break;
  case 'subscriptionInactive':
    showUpgradePrompt();
    break;
  case 'error':
    Alert.alert('Something went wrong', result.message);
    break;
}

Pro: configuring active liveness rounds

Active liveness uses an active liveness challenge: the user performs randomised facial actions such as head turns, blinks, or nods. Pro plans allow 2–4 rounds.

enforceChallenge forces the active liveness challenge to run even when passive liveness has already passed. Use this for high-assurance flows. By default the SDK skips active liveness if passive already passed.

TypeScript
// 3 active liveness rounds (Pro only, 2–4)
const result = await VerisCapture.startVerification({
  nonce: await fetchNonceFromBackend(),
  proRandomChallengeCount: 3,
});

// Always require active liveness, even if passive already passed (Pro only)
const result = await VerisCapture.startVerification({
  nonce: await fetchNonceFromBackend(),
  enforceChallenge: true,
});

03Result states

The SDK always ends a session with a typed result - it never throws an unhandled exception or silently fails. The four outcomes below are the ones to handle. The iOS, Flutter and React Native wrappers also report an error result for unexpected problems; treat it as retryable.

State When it fires What to do
Success Face captured, all configured checks passed. Contains signedResult, a signed token. Send signedResult to your backend. Your backend calls POST /v1/sdk/verify-result.
Failure One or more checks failed (quality, liveness, etc.). Contains a human-readable reason (humanReadableReason on Android and Flutter, reason on iOS and React Native). Display the reason to the user and offer a retry. Do not pass this to your backend.
Cancelled User dismissed the capture screen without completing. No action required. Return the user to your flow.
SubscriptionInactive The license key's subscription has expired or payment has failed. Show an upgrade or renewal prompt. Never crash or block the host app.

04Signed result

On a successful capture, the SDK returns signedResult: a compact signed token (a JWS signed with ES256 by a key held in the device's secure hardware). It has three dot-separated parts, and the middle part is the JSON below. Send the token to your backend as-is; do not modify it. Do not make decisions from a decoded copy on the phone. Your backend verifies it with Veris (next section).

JSON (decoded payload)
{
  "ver": "2.0",
  "status": "SUCCESS",
  "session_id": "uuid-v4",
  "nonce": "uuid-v4-from-your-backend",
  "timestamp": "2026-03-01T12:00:00Z",
  "environment": "production",
  "plan": "scale",
  "sdk_version": "1.3.0",
  "platform": "android",
  "package_name": "com.example.app",
  "quality_score": 0.87,
  "liveness_score": 0.82,
  "face_hash": "sha256-of-captured-image",
  "hardware_backed": true,
  "validation_state": "verified",
  "injection_risk": 0.0,
  "challenges_completed": ["head_turn", "blink"]
}
FieldTypeDescription
verstringVersion of the result format.
statusstringOutcome of the session, for example "SUCCESS".
session_idstringUnique UUID for this capture session. Store it for your audit trail.
noncestringThe nonce you supplied for this session. Veris checks that it has not been used before.
timestampstringISO 8601 UTC timestamp of when the capture completed.
environmentstring"production" or "sandbox". Sandbox results are rejected by the verify endpoint.
planstringThe plan the session ran under.
sdk_versionstringVersion of the SDK that produced the result.
platformstring"android" or "ios".
package_namestringThe app package or bundle identifier the session ran in.
quality_scorenumber0.0–1.0. Aggregate quality score from the 12-check quality gate.
liveness_scorenumber0.0–1.0. Liveness confidence. Only populated when liveness is enabled on the plan.
face_hashstringSHA-256 hash of the captured image. The image stays on-device; Veris never receives it.
reason_codestringPresent on non-success results. Why the session did not succeed.
hardware_backedbooleanWhether the signing key lives in the device's secure hardware.
validation_statestringHow the license was confirmed for this session: "verified", "grace" or "unverified_offline".
injection_risknumber0–1 on-device signal for camera-injection attempts. Advisory: use it in your own risk scoring.
challenges_completedarrayActive liveness challenges the user completed. Absent when none ran. One entry on Scale, for example ["head_turn"]; up to four on Pro, for example ["head_turn", "blink", "nod"].

05Backend verification

After receiving signedResult from the SDK, your mobile app passes it to your backend, which then calls Veris to verify it. A successful response confirms the signature is genuine, the nonce has not been replayed, and the session belongs to your license. Once it is verified, you can read the scores from the token's payload.

POST /v1/sdk/verify-result

Request

JSON
{
  "license_key": "veris_live_xxxx",
  "token": "<the signedResult string from the SDK>"
}

Older SDKs that return a separate payload and signature can send signed_payload and signature instead of token.

Response - verified (HTTP 200)

JSON
{
  "ok": true,
  "valid": true,
  "environment": "production",
  "nonce_status": "fresh",
  "plan": "scale",
  "verified_at": "2026-03-01T12:00:03Z"
}

Response - rejected

A rejected result comes back with a non-200 status, valid: false, and the reason in reason (also in error).

JSON
// HTTP 422 (other statuses below)
{
  "ok": false,
  "valid": false,
  "error": "NONCE_REPLAYED",
  "reason": "NONCE_REPLAYED",
  "message": "NONCE_REPLAYED"
}
HTTPReasonMeaning
400INVALID_REQUESTThe request is missing the license key or the token.
401INVALID_LICENSEThe license key was not found or is not active.
422INVALID_TOKENThe token is not a valid signed result.
422INVALID_SIGNATUREThe signature does not verify. The result was altered or did not come from the SDK.
422SANDBOX_RESULT_REJECTEDThe result came from a sandbox session. Production verification rejects it.
422NONCE_MISSINGThe result carries no nonce.
422NONCE_NOT_FOUNDVeris has no record of this nonce for a session.
422NONCE_EXPIREDThe nonce was not used within its 10-minute window.
422NONCE_REPLAYEDThe nonce has already been used. Reject the result.
422NONCE_LICENSE_MISMATCHThe nonce belongs to a different license key.
⚠

Make this call from your backend, not your mobile app. Your license key must stay server-side. Never embed it in client-side code.

06Nonces

Nonces prevent replay attacks - an attacker capturing a valid signed result cannot reuse it. Here's the full flow:

  1. Your backend generates a UUID v4 nonce for each session and stores it with a short TTL.
  2. Your mobile app passes the nonce when it starts a session (startCapture on Android, startVerification on iOS, Flutter and React Native).
  3. The SDK includes the nonce in its validation call to Veris. Veris holds it for 10 minutes.
  4. The SDK embeds the nonce in the signed result.
  5. Your mobile app sends signedResult to your backend.
  6. Your backend calls POST /v1/sdk/verify-result. Veris checks the nonce hasn't been used before, marks it used, and returns a verified response.
  7. Any attempt to reuse the same nonce is rejected with NONCE_REPLAYED.
◆

Nonce is optional on Spark and Build. On Launch, Scale, and Pro it is required - if not provided, the SDK proceeds with a warning logged to your console but skips replay protection. Always provide a nonce in production.

07Plans and features

Feature availability is controlled by your plan. The SDK reads the feature flags on each validation call and never needs a code change when you upgrade.

Feature Spark Build Launch Scale Pro
Face capture + all quality checks ✓ ✓ ✓ ✓ ✓
Human face validation ✓ ✓ ✓ ✓ ✓
Voice instructions ✓ ✓ ✓ ✓ ✓
Signed result payload ✓ ✓ ✓ ✓ ✓
Passive liveness (LBP) — — ✓ ✓ ✓
Active liveness — 1 random round — — — ✓ ✓
Active liveness — 2–4 configurable rounds — — — — ✓
Video capture — — — — ✓
Advanced config (strictness, timeout) — — — — ✓

When you downgrade, the SDK reads the updated feature flags on its next validation call (max 1 hour delay). It never crashes on downgrade - features simply become unavailable and the liveness checks your plan no longer includes do not run.

08Sandbox

Sandbox keys (veris_sandbox_*) let you build and test your integration without a paid subscription.

  • 50 validations per day - hard cap, resets at midnight UTC. Exceeding this returns SANDBOX_LIMIT_REACHED.
  • 10 Compare calls lifetime - total across all sandbox sessions. Returns SANDBOX_COMPARE_LIMIT_REACHED with an upgrade URL when exhausted.
  • SANDBOX badge - the SDK shows a small amber "SANDBOX" badge in the top-right corner of the camera preview. It does not interfere with the face oval or liveness instructions. Completely removed when a production key is used.
  • Environment flag - all results include "environment": "sandbox".
  • Sandbox results rejected in production - POST /v1/sdk/verify-result rejects sandbox-signed results with SANDBOX_RESULT_REJECTED. You cannot mix environments.

09SDK guarantees

These are published commitments about SDK behaviour. They hold across all plan levels and all platforms.

  • Never throws an unhandled exception into the host app. All errors are returned as typed result states.
  • Always returns a final state. Every session ends with Success, Failure, Cancelled, or SubscriptionInactive - never silent failure.
  • Never silently retries indefinitely. If a validation call fails, the SDK surfaces the error rather than looping.
  • Releases all camera, CPU, and memory on completion, backgrounding, or dismiss. The SDK does not hold resources after the session ends.
  • Adds about 20 MB per device architecture. Ship an App Bundle (or per-ABI splits) so each device downloads only its own architecture. Includes the on-device face detector and anti-spoof model - no server round trips during capture.
  • Does not access the camera in the background under any circumstances. Camera access begins when startCapture is called and ends when the session result is returned.
i

The face capture engine (quality gate, LBP analyser, active liveness controller) is a fixed component. Do not attempt to modify it. Only the integration layer (init, startCapture, result handling) should be touched in your code.

Further reading.

Guides on the Veris blog that go with this page.

Stuck, or something here does not match what you see?

Write to us and a person will answer.

Email the team