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.
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
Kotlinoverride 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.
KotlinVerisCapture.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.
// 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
Podfilepod '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
SwiftVerisCapture.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.
SwiftVerisCapture.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.
// 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.yamldependencies:
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.
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.
Dartfinal 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.
// 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
Terminalnpm install @verisinfra/react-native-capture
# iOS - install native pods
cd ios && pod install
2. Initialise once
TypeScriptimport { 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.
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.
// 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).
{
"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"]
}
| Field | Type | Description |
|---|---|---|
ver | string | Version of the result format. |
status | string | Outcome of the session, for example "SUCCESS". |
session_id | string | Unique UUID for this capture session. Store it for your audit trail. |
nonce | string | The nonce you supplied for this session. Veris checks that it has not been used before. |
timestamp | string | ISO 8601 UTC timestamp of when the capture completed. |
environment | string | "production" or "sandbox". Sandbox results are rejected by the verify endpoint. |
plan | string | The plan the session ran under. |
sdk_version | string | Version of the SDK that produced the result. |
platform | string | "android" or "ios". |
package_name | string | The app package or bundle identifier the session ran in. |
quality_score | number | 0.0–1.0. Aggregate quality score from the 12-check quality gate. |
liveness_score | number | 0.0–1.0. Liveness confidence. Only populated when liveness is enabled on the plan. |
face_hash | string | SHA-256 hash of the captured image. The image stays on-device; Veris never receives it. |
reason_code | string | Present on non-success results. Why the session did not succeed. |
hardware_backed | boolean | Whether the signing key lives in the device's secure hardware. |
validation_state | string | How the license was confirmed for this session: "verified", "grace" or "unverified_offline". |
injection_risk | number | 0–1 on-device signal for camera-injection attempts. Advisory: use it in your own risk scoring. |
challenges_completed | array | Active 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.
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).
// HTTP 422 (other statuses below)
{
"ok": false,
"valid": false,
"error": "NONCE_REPLAYED",
"reason": "NONCE_REPLAYED",
"message": "NONCE_REPLAYED"
}
| HTTP | Reason | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST | The request is missing the license key or the token. |
| 401 | INVALID_LICENSE | The license key was not found or is not active. |
| 422 | INVALID_TOKEN | The token is not a valid signed result. |
| 422 | INVALID_SIGNATURE | The signature does not verify. The result was altered or did not come from the SDK. |
| 422 | SANDBOX_RESULT_REJECTED | The result came from a sandbox session. Production verification rejects it. |
| 422 | NONCE_MISSING | The result carries no nonce. |
| 422 | NONCE_NOT_FOUND | Veris has no record of this nonce for a session. |
| 422 | NONCE_EXPIRED | The nonce was not used within its 10-minute window. |
| 422 | NONCE_REPLAYED | The nonce has already been used. Reject the result. |
| 422 | NONCE_LICENSE_MISMATCH | The 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:
- Your backend generates a UUID v4 nonce for each session and stores it with a short TTL.
- Your mobile app passes the nonce when it starts a session (
startCaptureon Android,startVerificationon iOS, Flutter and React Native). - The SDK includes the nonce in its validation call to Veris. Veris holds it for 10 minutes.
- The SDK embeds the nonce in the signed result.
- Your mobile app sends
signedResultto your backend. - 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. - 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_REACHEDwith 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-resultrejects sandbox-signed results withSANDBOX_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, orSubscriptionInactive- 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
startCaptureis called and ends when the session result is returned.
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.