Palm Verification SDK Integration
The VeryAI Palm Verification SDK adds palm biometric enrollment and verification to your mobile app. Available for iOS (Swift/ObjC), Android (Kotlin/Java), and React Native.
Getting an SDK key
Create a Palm Verification SDK app in the Developer Portal to get your sdkKey. Palm Verification SDK integrations do not need OAuth client_id or client_secret; those are only for separate browser OAuth flows.
Important: Treat the SDK key as sensitive. If it is extracted from an app, it can be used to call SDK endpoints as that app. It does not grant dashboard access, and keys can be revoked or rotated.
Installation#
iOS#
Swift Package Manager (recommended):
dependencies: [
.package(url: "https://github.com/veroslabs/very-sdk-ios.git", from: "1.0.64")
] CocoaPods:
pod 'VerySDK', '~> 1.0.64' Then run pod install.
Add camera permission to Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is needed for palm biometric verification.</string> Android#
Add the dependency to your app-level build.gradle:
dependencies {
implementation("org.very:sdk:1.0.64")
} The SDK is published on Maven Central.
Add permissions to AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> React Native#
npm install @veryai/react-native-sdk@latest For iOS, install the native dependency:
cd ios && pod install Add camera permission to Info.plist (same as native iOS above).
Published on npm.
Using Expo? See Expo below instead — the steps above are for bare React Native.
Expo#
Install the same npm package. The SDK works with the Expo managed workflow — you never
need to run pod install yourself or commit ios/ and
android/, because expo prebuild and EAS Build handle that.
There is no config plugin to install; the package does not ship one and does not need one.
Two requirements are non-negotiable:
- A custom dev client. The SDK carries native Swift/Kotlin code and the PalmID native matcher, so Expo Go cannot load it.
- A physical device. Palm capture needs a real camera and the native matcher; simulators and emulators will not work.
npx expo run:ios # or: eas build --profile development iOS — declare the camera usage string in app.json.
expo prebuild writes it into the generated Info.plist, and this is
the only native configuration the host app owes the SDK:
{
"expo": {
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Camera access is required for palm biometric verification."
}
}
}
} Android — nothing to configure. The SDK's AAR manifest already declares
CAMERA, INTERNET, and ACCESS_NETWORK_STATE, and
manifest merge folds them into the host app.
Omitting the usage string makes authenticate() fail on iOS with error
6105 instead of proceeding. In a Release
build that arrives through the normal error callback; in a Debug build — which is what a
default dev client is — it trips an assertion instead. Either way, add the key and rebuild:
a JS reload is not enough, since the value lives in the compiled Info.plist.
Slim asset loading is not available in a pure managed workflow. Switching to
it edits ios/Podfile and android/app/build.gradle, both of which
are prebuild output that gets regenerated. Using it means either committing the native
directories or writing a config plugin with dangerous mods. Stay on the default bundled mode
unless binary size is a hard constraint.
Enroll a new user#
Pass nil / null / undefined for userId to register a new user.
The SDK opens a consent screen, then guides the user through a palm scan.
iOS (Swift)#
import VerySDK
guard VerySDK.isSupported() else {
print("Device not supported")
return
}
let config = VeryConfig(
sdkKey: "your_sdk_key",
userId: nil, // nil = new enrollment
clientReferenceId: "gate-withdrawal-42", // your transaction/session ID
themeMode: "dark"
)
VerySDK.authenticate(
from: self,
config: config,
presentationStyle: .modal
) { result in
if result.isSuccess {
print("User ID: \(result.userId)")
print("User status: \(result.userStatus ?? "unknown")")
print("Signed token: \(result.signedToken ?? "")")
} else {
print("Error: \(result.errorType) — \(result.errorMessage ?? "")")
}
} Android (Kotlin)#
import org.very.sdk.VerySDK
import org.very.sdk.VeryConfig
import org.very.sdk.VeryPresentationStyle
if (!VerySDK.isSupported(context)) {
Log.w("Very", "Device not supported")
return
}
val config = VeryConfig(
sdkKey = "your_sdk_key",
userId = null, // null = new enrollment
clientReferenceId = "gate-withdrawal-42", // your transaction/session ID
themeMode = "dark"
)
VerySDK.authenticate(
context = this,
config = config,
presentationStyle = VeryPresentationStyle.FULL_SCREEN
) { result ->
if (result.isSuccess) {
Log.d("Very", "User ID: ${result.userId}")
Log.d("Very", "User status: ${result.userStatus}")
Log.d("Very", "Signed token: ${result.signedToken}")
} else {
Log.e("Very", "Error: ${result.errorType} — ${result.errorMessage}")
}
} React Native#
import { VerySDK } from '@veryai/react-native-sdk';
const supported = await VerySDK.isSupported();
if (!supported) {
console.warn('Device not supported');
return;
}
const result = await VerySDK.authenticate({
sdkKey: 'your_sdk_key',
userId: undefined, // undefined = new enrollment
clientReferenceId: 'gate-withdrawal-42', // your transaction/session ID
themeMode: 'dark',
presentationStyle: 'fullScreen',
});
if (result.isSuccess) {
console.log('User ID:', result.userId);
console.log('User status:', result.userStatus);
console.log('Signed token:', result.signedToken);
} else {
console.error('Error:', result.error, result.errorMessage);
} Verify an existing user#
Pass the user's ID from a previous enrollment to verify their identity.
iOS (Swift)#
let config = VeryConfig(
sdkKey: "your_sdk_key",
userId: "vu-1ed0a927-...", // from previous enrollment
themeMode: "dark"
)
VerySDK.authenticate(from: self, config: config) { result in
if result.isSuccess {
print("Verified user: \(result.userId)")
print("Signed token: \(result.signedToken ?? "")")
}
} Android (Kotlin)#
val config = VeryConfig(
sdkKey = "your_sdk_key",
userId = "vu-1ed0a927-...",
themeMode = "dark"
)
VerySDK.authenticate(context = this, config = config) { result ->
if (result.isSuccess) {
Log.d("Very", "Verified user: ${result.userId}")
Log.d("Very", "Signed token: ${result.signedToken}")
}
} React Native#
const result = await VerySDK.authenticate({
sdkKey: 'your_sdk_key',
userId: 'vu-1ed0a927-...', // from previous enrollment
themeMode: 'dark',
});
if (result.isSuccess) {
console.log('Verified user:', result.userId);
console.log('Signed token:', result.signedToken);
} Verify the signed token (backend)#
When authentication succeeds, the SDK returns userId and signedToken.
Send signedToken to your backend:
Pending is a successful submission
A successful SDK result can have userStatus: "pending" while review continues. Persist the userId and clientReferenceId, then use signed webhook events as the source of truth for later status changes.
POST /api/verify-palm
Content-Type: application/json
{
"signedToken": "eyJhbGciOiJFZERTQSIsImtpZCI6..."
}
Verify the token on your trusted backend, never in the mobile app. The following Node.js example fetches and caches VeryAI's public keys, selects the verification key by the token's kid header, restricts the algorithm to EdDSA, and validates the full token contract:
npm install jose import { createRemoteJWKSet, jwtVerify } from 'jose';
const VERY_ISSUER = 'https://api.very.org';
const VERY_JWKS = createRemoteJWKSet(
new URL('https://api.very.org/.well-known/jwks.json')
);
export async function verifyVerySdkToken(
signedToken,
{ appId, expectedAction, expectedUserId }
) {
if (typeof appId !== 'string' || appId.length === 0) {
throw new Error('Palm Verification SDK app ID is required');
}
if (expectedAction !== 'enroll' && expectedAction !== 'verify') {
throw new Error('Expected action must be enroll or verify');
}
// jwtVerify selects the verification key from the JWKS by the token's
// kid header, so VeryAI signing-key rotation needs no code change here.
const { payload } = await jwtVerify(signedToken, VERY_JWKS, {
algorithms: ['EdDSA'],
issuer: VERY_ISSUER,
audience: appId,
requiredClaims: ['sub', 'iat', 'exp', 'act', 'sid', 'jti'],
clockTolerance: 5,
});
if (payload.act !== expectedAction) {
throw new Error('Unexpected SDK action');
}
if (typeof payload.sub !== 'string' || payload.sub.length === 0 ||
typeof payload.sid !== 'string' || payload.sid.length === 0 ||
typeof payload.jti !== 'string' || payload.jti.length === 0 ||
typeof payload.iat !== 'number' || typeof payload.exp !== 'number') {
throw new Error('Invalid SDK token claims');
}
const now = Math.floor(Date.now() / 1000);
if (payload.iat > now + 60) {
throw new Error('SDK token was issued in the future');
}
if (payload.exp <= payload.iat || payload.exp - payload.iat > 300) {
throw new Error('Invalid SDK token lifetime');
}
if (expectedAction === 'verify' &&
(!expectedUserId || payload.sub !== expectedUserId)) {
throw new Error('SDK token is for a different user');
}
return {
userId: payload.sub,
sessionId: payload.sid,
tokenId: payload.jti,
};
} Pass the app ID and expected action from server-side configuration. For a verification, also pass the VeryAI user ID already stored for the authenticated account; never take any of those expected values from the mobile request.
After verification, the claims have this shape:
{
"iss": "https://api.very.org",
"sub": "vu-1ed0a927-a336-45dd-9c73-20092db9ae8d",
"aud": "your_app_id",
"act": "verify",
"sid": "sdk_session_id",
"jti": "34fa6d08-28d4-4a29-85f8-5064ea8ae32e",
"exp": 1761010175,
"iat": 1761009875
} - Require
alg: EdDSA, issuerhttps://api.very.org, and audience equal to your Palm Verification SDK app ID. - Select the verification key from the JWKS by the token's
kidheader, asjwtVerifydoes above. Do not pin a specific key id:sdk-v1is only the current key and will change when VeryAI rotates signing keys. - Require and validate
expandiat. Tokens expire five minutes after issue; do not accept expired or materially future-issued tokens. - Require the expected
act(enrollorverify) and non-emptysidandjtiaudit identifiers. - For enrollment: store the verified
suband pass it asuserIdfor future verifications. - For verification: compare the verified
subwith the expected user from your server-side account record. - If accepting a token triggers a one-time or financial action, persist
jtiand reject a replay. codeis an SDK status string, not an OAuth authorization code
Important: Do not create an OAuth app for this flow. Palm Verification SDK verification uses signedToken and JWKS, not an OAuth token exchange.
Configuration reference#
VeryConfig#
| Parameter | Type | Default | Description |
|---|---|---|---|
sdkKey | String | required | Your SDK API key |
userId | String? | nil | Nil for enrollment, user ID for verification |
clientReferenceId | String? | nil | Your transaction or session ID (up to 255 characters). Included in webhook events for the review started by this SDK call. |
language | String? | device | BCP 47 locale code (e.g. "en", "es", "en-IN", "zh-HK") — see supported languages below. Defaults to the device locale; unsupported locales fall back to the default language. |
themeMode | String | "dark" | "dark" or "light" |
presentationStyle | Enum | modal / fullScreen | iOS: .modal, .push, .embed. Android: FULL_SCREEN, BOTTOM_SHEET. RN: "fullScreen", "bottomSheet". |
customStrings | Map | nil | Override the palm-scan status copy, keyed by VeryCustomString (showYourHand, showYourFirstHand, connectDots). A caller value wins over the built-in localized string; blank values and unknown keys fall back to the default. See the iOS / Android guides. |
VeryResult#
| Property | Type | Description |
|---|---|---|
isSuccess | Bool | Whether authentication completed successfully |
code | String | SDK status string, not an OAuth authorization code |
userId | String | The user's app-scoped VeryAI ID. The same palm returns the same userId within the same Palm Verification SDK app/environment. |
signedToken | String? | Ed25519-signed JWT for backend verification |
userStatus | String? | approved, pending, rejected, restricted, or unknown. Pending does not make isSuccess false. |
error | String? | Error code string |
errorType | VeryErrorType | Typed error (see Error Handling) |
errorMessage | String? | Human-readable error message |
Supported languages#
The SDK ships with 37 localizations. Pass any code below as
language. Codes are case-insensitive, the listed aliases resolve to the
same localization, and an unknown or empty code falls back to English.
| Language | Native name | Code | Also accepted |
|---|---|---|---|
| English | English | en | en-US, en-GB, en-TR |
| English (India) | English (India) | en-IN | enIn |
| Chinese (Simplified) | 简体中文 | zh | zh-MY |
| Chinese (Traditional) | 繁體中文 | zh-TW | zhTw |
| Chinese (Hong Kong) | 繁體中文(香港) | zh-HK | zhHk |
| Japanese | 日本語 | ja | ja-JP |
| Korean | 한국어 | ko | ko-KR |
| French | Français | fr | fr-FR |
| German | Deutsch | de | de-DE |
| Spanish | Español | es | es-ES |
| Portuguese | Português | pt | pt-BR, pt-PT |
| Italian | Italiano | it | it-IT |
| Dutch | Nederlands | nl | nl-NL |
| Russian | Русский | ru | ru-RU |
| Arabic | العربية | ar | ar-AE |
| Hebrew | עברית | he | he-IL, iw |
| Turkish | Türkçe | tr | tr-TR, tr-CT |
| Vietnamese | Tiếng Việt | vi | vi-VN |
| Indonesian | Bahasa Indonesia | id | id-ID |
| Filipino | Filipino | fil | fil-PH |
| Swedish | Svenska | sv | sv-SE |
| Danish | Dansk | da | da-DK |
| Polish | Polski | pl | pl-PL |
| Romanian | Română | ro | ro-RO |
| Hungarian | Magyar | hu | hu-HU |
| Czech | Čeština | cs | cs-CZ |
| Slovak | Slovenčina | sk | sk-SK |
| Slovenian | Slovenščina | sl | sl-SI |
| Bulgarian | Български | bg | bg-BG |
| Ukrainian | Українська | uk | uk-UA |
| Greek | Ελληνικά | el | el-GR |
| Latvian | Latviešu | lv | lv-LV |
| Persian | فارسی | fa | fa-IR |
| Azerbaijani | Azərbaycanca | az | az-AZ |
| Kazakh | Қазақша | kk | kk-KZ |
| Lao | ລາວ | lo | lo-LA |
| Sinhala | සිංහල | si | si-LK |
Requirements#
iOS
- iOS 13.0+
- Swift 5.0+ / Xcode 12+
- Camera permission
- Physical device for palm capture
Android
- Android 6.0 (API 23, Marshmallow)+
- 1GB+ RAM
- SDK runtime memory typically under 100MB
- Kotlin 1.7+
- Camera + Internet permissions
React Native
- React Native 0.73+
- iOS and Android requirements above
- Physical device for palm capture
Use isSupported() to check device compatibility at runtime before showing the verification UI.
Current support checks include OS version, camera capability, and SDK runtime compatibility. The SDK does not reject Android devices solely because they have less than 6GB RAM.
Packages#
- iOS: github.com/veroslabs/very-sdk-ios (SPM / CocoaPods)
- Android: org.very:sdk (Maven Central)
- React Native: @veryai/react-native-sdk (npm)