Palm Verification SDK Guides /

Palm Verification SDK Integration

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, issuer https://api.very.org, and audience equal to your Palm Verification SDK app ID.
  • Select the verification key from the JWKS by the token's kid header, as jwtVerify does above. Do not pin a specific key id: sdk-v1 is only the current key and will change when VeryAI rotates signing keys.
  • Require and validate exp and iat. Tokens expire five minutes after issue; do not accept expired or materially future-issued tokens.
  • Require the expected act (enroll or verify) and non-empty sid and jti audit identifiers.
  • For enrollment: store the verified sub and pass it as userId for future verifications.
  • For verification: compare the verified sub with the expected user from your server-side account record.
  • If accepting a token triggers a one-time or financial action, persist jti and reject a replay.
  • code is 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#

VeryAI

Get the VeryAI app

Scan the QR code to download the app