Mobile Development

Kotlin Integration Guide

Complete guide for integrating v2Client verification into your Android application using Kotlin and secure backend patterns.

Architecture Overview

Android App (Kotlin)

Your mobile application with verification UI

Your Backend

Generates verification IDs with your API key

v2Client

okID proxy with verification endpoints

Data Flow

Your Backendv2Client(Generate ID)
Your BackendAndroid App(Pass verification_id)
Android Appv2Client(WebView or Native APIs)
v2ClientYour Backend(Webhook results)

Security Note

API keys are never stored in the mobile app. Your backend generates verification IDs securely, passes them to the mobile app, and receives completion notifications via webhooks from v2Client.

Step 1: Backend Setup (Verification ID Generation & Webhook Handling)

Create backend endpoints to generate verification IDs with webhook URLs and handle completion notifications from v2Client. Your backend manages the secure communication while the mobile app handles the user interface.

Backend Dependencies (Spring Boot Example)

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-webflux") // For WebClient
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
}

// application.yml
verification:
  v2client:
    base-url: "https://verify.test.okid.io"
    api-key: "${V2CLIENT_API_KEY}" # Set via environment variable
    webhook-url: "https://your-backend.com/api/webhooks/verification-complete"

Simple Backend API Controller

@RestController
@RequestMapping("/api/mobile")
@Validated
class MobileController(
    private val verificationService: VerificationService
) {
    
    @PostMapping("/generate-verification")
    suspend fun generateVerification(
        @RequestHeader("Authorization") authHeader: String
    ): ResponseEntity<VerificationResponse> {
        val userId = extractUserIdFromToken(authHeader)
        
        return try {
            val response = verificationService.generateVerification(userId)
            ResponseEntity.ok(response)
        } catch (e: Exception) {
            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(VerificationResponse(error = e.message))
        }
    }
    
    @GetMapping("/verification-status/{verificationId}")
    suspend fun getVerificationStatus(
        @PathVariable verificationId: String,
        @RequestHeader("Authorization") authHeader: String
    ): ResponseEntity<VerificationStatusResponse> {
        val userId = extractUserIdFromToken(authHeader)
        
        return try {
            val status = verificationService.getVerificationStatus(userId, verificationId)
            ResponseEntity.ok(status)
        } catch (e: Exception) {
            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(VerificationStatusResponse(error = e.message))
        }
    }
}

@RestController
@RequestMapping("/api/webhooks")
class WebhookController(
    private val verificationService: VerificationService
) {
    
    @PostMapping("/verification-complete")
    suspend fun handleVerificationComplete(
        @RequestBody webhookData: VerificationWebhookData
    ): ResponseEntity<String> {
        return try {
            verificationService.handleVerificationComplete(webhookData)
            ResponseEntity.ok("Webhook processed successfully")
        } catch (e: Exception) {
            logger.error("Webhook processing failed", e)
            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Webhook processing failed")
        }
    }
}

Simple Verification Service

@Service
class VerificationService(
    @Value("${verification.v2client.base-url}") private val v2clientBaseUrl: String,
    @Value("${verification.v2client.api-key}") private val apiKey: String,
    @Value("${verification.v2client.webhook-url}") private val webhookUrl: String,
    private val webClient: WebClient = WebClient.builder().build(),
    private val verificationRepository: VerificationRepository
) {
    
    suspend fun generateVerification(userId: String): VerificationResponse {
        val requestBody = mapOf(
            "modules" to listOf("document", "liveness"),
            "webhook_url" to webhookUrl,
            "settings" to mapOf(
                "redirect_url" to "https://your-app.com/verification-complete"
            )
        )
        
        val response = webClient.post()
            .uri("$v2clientBaseUrl/api/generate-verification")
            .header("Content-Type", "application/json")
            .header("X-SDK-Key", apiKey)
            .bodyValue(requestBody)
            .awaitBody<VerificationResponse>()
        
        // Store verification ownership in your database
        verificationRepository.save(
            VerificationOwnership(
                verificationId = response.verificationId,
                userId = userId,
                status = "pending",
                createdAt = Instant.now()
            )
        )
        
        return response
    }
    
    suspend fun handleVerificationComplete(webhookData: VerificationWebhookData) {
        // Update verification status in your database
        verificationRepository.updateStatus(
            webhookData.verificationId,
            webhookData.status,
            webhookData.results
        )
        
        // Optional: Fetch images from v2Client
        if (webhookData.status == "verified") {
            fetchAndStoreVerificationImages(webhookData.verificationId)
        }
        
        // Send push notification to mobile app if needed
        sendPushNotificationToUser(webhookData.verificationId, webhookData.status)
    }
    
    suspend fun getVerificationStatus(userId: String, verificationId: String): VerificationStatusResponse {
        if (!userOwnsVerification(userId, verificationId)) {
            throw UnauthorizedException("User does not own this verification")
        }
        
        return verificationRepository.getVerificationStatus(verificationId)
            ?: throw NotFoundException("Verification not found")
    }
    
    private suspend fun fetchAndStoreVerificationImages(verificationId: String) {
        try {
            // Fetch document image
            val documentImage = webClient.get()
                .uri("$v2clientBaseUrl/api/get-verification-image/$verificationId")
                .header("X-SDK-Key", apiKey)
                .awaitBody<ByteArray>()
            
            // Store image (implement your storage logic)
            imageStorageService.storeVerificationImage(verificationId, "document", documentImage)
            
        } catch (e: Exception) {
            logger.warn("Failed to fetch verification images for $verificationId", e)
        }
    }
    
    private fun sendPushNotificationToUser(verificationId: String, status: String) {
        // Implement push notification logic
        // Get user device tokens and send notification about verification completion
    }
    
    fun userOwnsVerification(userId: String, verificationId: String): Boolean {
        return verificationRepository.existsByUserIdAndVerificationId(userId, verificationId)
    }
}

// Data classes for webhook
data class VerificationWebhookData(
    val verificationId: String,
    val status: String, // "verified", "rejected", "expired"
    val results: VerificationResults?
)

data class VerificationOwnership(
    val verificationId: String,
    val userId: String,
    val status: String,
    val createdAt: Instant,
    val completedAt: Instant? = null,
    val results: VerificationResults? = null
)

Step 2: Android App Dependencies

Set up your Android project with the necessary dependencies for network requests, image handling, and camera access.

Module build.gradle.kts

dependencies {
    // Networking
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
    implementation("com.google.code.gson:gson:2.10.1")
    
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // ViewModel and LiveData
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
    
    // Image loading
    implementation("com.github.bumptech.glide:glide:4.15.1")
    
    // Camera
    implementation("androidx.camera:camera-camera2:1.3.0")
    implementation("androidx.camera:camera-lifecycle:1.3.0")
    implementation("androidx.camera:camera-view:1.3.0")
    
    // WebView (if using WebView approach)
    implementation("androidx.webkit:webkit:1.8.0")
    
    // Permissions
    implementation("androidx.activity:activity-ktx:1.8.0")
}

AndroidManifest.xml Permissions

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-feature
    android:name="android.hardware.camera"
    android:required="true" />

<application
    android:usesCleartextTraffic="false"
    android:networkSecurityConfig="@xml/network_security_config">
    
    <!-- File Provider for camera -->
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

Step 3: Network Layer (Backend Communication)

Create the network layer to communicate with your backend for verification management and status checking. The mobile app will use WebView or native v2Client APIs only for the verification flow itself.

Data Models

// Backend API Response Models  
data class VerificationResponse(
    val verificationId: String,
    val expiresAt: String,
    val webviewUrl: String,
    val modules: List<String>,
    val flow: List<String>,
    val error: String? = null
)

data class VerificationStatusResponse(
    val verificationId: String,
    val status: String, // "pending", "in_progress", "verified", "rejected", "expired"
    val updatedAt: String,
    val results: VerificationResults? = null,
    val error: String? = null
)

data class VerificationResults(
    val document: DocumentResult? = null,
    val liveness: LivenessResult? = null,
    val form_data: FormDataResult? = null
)

data class DocumentResult(
    val status: String,
    val confidence: Double,
    val extracted_data: Map<String, Any>
)

data class LivenessResult(
    val status: String,
    val confidence: Double,
    val metadata: Map<String, Any>
)

data class FormDataResult(
    val status: String,
    val data: Map<String, Any>
)

// Native v2Client API Models (for native implementation)
data class StartVerificationRequest(
    val verification_id: String
)

data class FormDataRequest(
    val verification_id: String,
    val form_data: Map<String, Any>
)

API Interfaces

// Your Backend API (primary communication)
interface BackendApi {
    @POST("api/mobile/generate-verification")
    suspend fun generateVerification(
        @Header("Authorization") bearerToken: String
    ): Response<VerificationResponse>
    
    @GET("api/mobile/verification-status/{verificationId}")
    suspend fun getVerificationStatus(
        @Path("verificationId") verificationId: String,
        @Header("Authorization") bearerToken: String
    ): Response<VerificationStatusResponse>
}

// v2Client API (for native implementation only)
interface V2ClientApi {
    @POST("api/start-verification")
    suspend fun startVerification(
        @Body request: StartVerificationRequest
    ): Response<ResponseBody>
    
    @Multipart
    @POST("api/upload-document")
    suspend fun uploadDocument(
        @Part("verification_id") verificationId: RequestBody,
        @Part document: MultipartBody.Part
    ): Response<ResponseBody>
    
    @Multipart
    @POST("api/upload-selfie")
    suspend fun uploadSelfie(
        @Part("verification_id") verificationId: RequestBody,
        @Part selfie: MultipartBody.Part,
        @Part("metadata_json") metadata: RequestBody
    ): Response<ResponseBody>
    
    @POST("api/submit-form-data")
    suspend fun submitFormData(
        @Body formData: FormDataRequest
    ): Response<ResponseBody>
    
    @POST("api/validate-verification")
    suspend fun validateVerification(
        @Body request: Map<String, String> // verification_id
    ): Response<ResponseBody>
    
    @POST("api/accept-terms")
    suspend fun acceptTerms(
        @Body request: Map<String, String> // verification_id
    ): Response<ResponseBody>
}

Network Client Setup

object NetworkClient {
    
    private const val BACKEND_URL = "https://your-backend.com/" // Your backend URL
    private const val V2CLIENT_URL = "https://verify.test.okid.io/" // v2Client URL (for native only)
    
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = if (BuildConfig.DEBUG) {
            HttpLoggingInterceptor.Level.BODY
        } else {
            HttpLoggingInterceptor.Level.NONE
        }
    }
    
    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()
    
    // Backend client (primary communication)
    private val backendRetrofit = Retrofit.Builder()
        .baseUrl(BACKEND_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    
    // v2Client API (only needed for native implementation)
    private val v2ClientRetrofit = Retrofit.Builder()
        .baseUrl(V2CLIENT_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    
    val backendApi: BackendApi = backendRetrofit.create(BackendApi::class.java)
    val v2ClientApi: V2ClientApi = v2ClientRetrofit.create(V2ClientApi::class.java) // For native only
}

Step 4: Repository Implementation

Implement the repository pattern to handle communication with your backend. Status updates come via webhooks, eliminating the need for polling.

Verification Repository

import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import com.google.gson.Gson
import java.io.File
import java.util.UUID

class VerificationRepository(
    private val backendApi: BackendApi = NetworkClient.backendApi,
    private val v2ClientApi: V2ClientApi = NetworkClient.v2ClientApi,
    private val authManager: AuthManager
) {
    
    suspend fun generateVerification(): Result<VerificationResponse> {
        return try {
            val response = backendApi.generateVerification(
                bearerToken = "Bearer ${authManager.getAccessToken()}"
            )
            
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to generate verification: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun getVerificationStatus(verificationId: String): Result<VerificationStatusResponse> {
        return try {
            // Get status from your backend (updated via webhooks)
            val response = backendApi.getVerificationStatus(
                verificationId = verificationId,
                bearerToken = "Bearer ${authManager.getAccessToken()}"
            )
            
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to get status: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // === Native Implementation Functions (Optional) ===
    // Use these only if implementing native verification flow
    
    suspend fun startNativeVerification(verificationId: String): Result<ResponseBody> {
        return try {
            val request = StartVerificationRequest(verificationId)
            val response = v2ClientApi.startVerification(request)
            
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to start verification: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // Upload document for verification (native implementation)
    suspend fun uploadDocument(verificationId: String, imageUri: Uri): Result<ResponseBody> {
        return try {
            val file = File(imageUri.path!!)
            val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
            val body = MultipartBody.Part.createFormData("document", file.name, requestFile)
            val verificationIdBody = verificationId.toRequestBody("text/plain".toMediaTypeOrNull())
            
            val response = v2ClientApi.uploadDocument(verificationIdBody, body)
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to upload document: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    // Upload selfie for liveness verification
    suspend fun uploadSelfie(
        verificationId: String, 
        imageUri: Uri, 
        biometricData: BiometricData? = null
    ): Result<ResponseBody> {
        return try {
            val file = File(imageUri.path!!)
            val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
            val selfieBody = MultipartBody.Part.createFormData("selfie", "selfie.jpg", requestFile)
            val verificationIdBody = verificationId.toRequestBody("text/plain".toMediaTypeOrNull())
            
            // Prepare metadata for liveness
            val metadata = mapOf(
                "attempt_id" to UUID.randomUUID().toString(),
                "passive_mode" to true,
                "biometric_data" to (biometricData?.let {
                    mapOf(
                        "age" to it.age,
                        "gender" to it.gender,
                        "genderProbability" to it.genderProbability
                    )
                } ?: emptyMap())
            )
            val metadataJson = Gson().toJson(metadata)
            val metadataBody = metadataJson.toRequestBody("text/plain".toMediaTypeOrNull())
            
            val response = v2ClientApi.uploadSelfie(verificationIdBody, selfieBody, metadataBody)
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to upload selfie: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun submitFormData(verificationId: String, formData: Map<String, Any>): Result<ResponseBody> {
        return try {
            val request = FormDataRequest(verificationId, formData)
            val response = v2ClientApi.submitFormData(request)
            
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to submit form data: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun validateVerification(verificationId: String): Result<ResponseBody> {
        return try {
            val request = mapOf("verification_id" to verificationId)
            val response = v2ClientApi.validateVerification(request)
            
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to validate verification: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun acceptTerms(verificationId: String): Result<ResponseBody> {
        return try {
            val request = mapOf("verification_id" to verificationId)
            val response = v2ClientApi.acceptTerms(request)
            
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(Exception("Failed to accept terms: ${response.code()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Data class for biometric data
data class BiometricData(
    val age: Int,
    val gender: String,
    val genderProbability: Double
)
}

Step 5: ViewModel and UI State Management

Create ViewModels to manage UI state and coordinate between the repository and UI components. Status updates come via backend webhooks, eliminating the need for polling.

Verification ViewModel

class VerificationViewModel(
    private val repository: VerificationRepository
) : ViewModel() {
    
    private val _uiState = MutableLiveData<VerificationUiState>()
    val uiState: LiveData<VerificationUiState> = _uiState
    
    private val _verificationStatus = MutableLiveData<VerificationStatusResponse>()
    val verificationStatus: LiveData<VerificationStatusResponse> = _verificationStatus
    
    private val _verificationImage = MutableLiveData<Bitmap>()
    val verificationImage: LiveData<Bitmap> = _verificationImage
    
    fun generateVerification() {
        viewModelScope.launch {
            _uiState.value = VerificationUiState.Loading
            
            repository.generateVerification()
                .onSuccess { response ->
                    _uiState.value = VerificationUiState.Success(response)
                }
                .onFailure { error ->
                    _uiState.value = VerificationUiState.Error(error.message ?: "Unknown error")
                }
        }
    }
    
    fun checkVerificationStatus(verificationId: String) {
        viewModelScope.launch {
            repository.getVerificationStatus(verificationId)
                .onSuccess { status ->
                    _verificationStatus.value = status
                }
                .onFailure { error ->
                    _uiState.value = VerificationUiState.Error("Failed to get status: ${error.message}")
                }
        }
    }
    
    fun getWebViewUrl(verificationId: String): String {
        return "https://verify.test.okid.io/verify?verification_id=$verificationId"
    }

}

sealed class VerificationUiState {
    object Loading : VerificationUiState()
    data class Success(val response: VerificationResponse) : VerificationUiState()
    data class Error(val message: String) : VerificationUiState()
}

ViewModel Factory

class VerificationViewModelFactory(
    private val repository: VerificationRepository
) : ViewModelProvider.Factory {
    
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(VerificationViewModel::class.java)) {
            return VerificationViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

Step 6: UI Implementation (Two Approaches)

Choose between WebView integration (recommended for quick implementation) or native implementation following the v2Client API flow (for full control).

Option A: WebView Integration (Recommended)

✅ Quick to implement

✅ Consistent with web experience

✅ Automatic updates

✅ All modules supported

Option B: Native Implementation

✅ Full UI control

✅ Better performance

✅ Custom branding

✅ All modules supported via API

Complete API Support for Native Implementation

v2Client provides full API support for native mobile implementation across all verification modules.

Available APIs: Document upload, liveness/selfie upload, form data submission, status checking, image retrieval

Option A: WebView Integration Activity

class VerificationWebViewActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityVerificationWebviewBinding
    private lateinit var viewModel: VerificationViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVerificationWebviewBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupViewModel()
        setupWebView()
        setupObservers()
        
        // Generate verification
        viewModel.generateVerification()
    }
    
    private fun setupViewModel() {
        val repository = VerificationRepository(authManager = AuthManager(this))
        val factory = VerificationViewModelFactory(repository)
        viewModel = ViewModelProvider(this, factory)[VerificationViewModel::class.java]
    }
    
    private fun setupWebView() {
        binding.webView.apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            settings.allowFileAccess = true
            settings.allowContentAccess = true
            settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
            
            // Add JavaScript interface for communication
            addJavascriptInterface(WebViewInterface(), "Android")
            
            webViewClient = object : WebViewClient() {
                override fun onPageFinished(view: WebView?, url: String?) {
                    super.onPageFinished(view, url)
                    binding.progressBar.visibility = View.GONE
                }
                
                override fun onReceivedError(
                    view: WebView?,
                    errorCode: Int,
                    description: String?,
                    failingUrl: String?
                ) {
                    super.onReceivedError(view, errorCode, description, failingUrl)
                    showError("WebView error: $description")
                }
            }
        }
    }
    
    private fun setupObservers() {
        viewModel.uiState.observe(this) { state ->
            when (state) {
                is VerificationUiState.Loading -> {
                    binding.progressBar.visibility = View.VISIBLE
                }
                is VerificationUiState.Success -> {
                    binding.progressBar.visibility = View.VISIBLE
                    // Use the webview URL from backend response
                    binding.webView.loadUrl(state.response.webviewUrl)
                }
                is VerificationUiState.Error -> {
                    binding.progressBar.visibility = View.GONE
                    showError(state.message)
                }
            }
        }

    }
    
    inner class WebViewInterface {
        @JavascriptInterface
        fun onVerificationComplete(verificationId: String) {
            runOnUiThread {
                // Verification completed in WebView
                // Backend will receive webhook notification
                showSuccess("Verification submitted for processing")
                // Navigate to results screen or wait for push notification
                navigateToResults(verificationId)
            }
        }
        
        @JavascriptInterface
        fun onVerificationError(error: String) {
            runOnUiThread {
                showError("Verification error: $error")
            }
        }
    }
    
    private fun showSuccess(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
        // Navigate to results screen or close activity
    }
    
    private fun showError(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }
    
    private fun navigateToResults(verificationId: String) {
        // Navigate to results activity
        val intent = Intent(this, VerificationResultsActivity::class.java)
        intent.putExtra("verification_id", verificationId)
        startActivity(intent)
        finish()
    }
}

Option B: Native API Implementation

class NativeVerificationActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityNativeVerificationBinding
    private lateinit var viewModel: VerificationViewModel
    private lateinit var repository: VerificationRepository
    private var verificationId: String = ""
    private var currentStep = 0
    private val verificationFlow = listOf("document", "liveness", "form_data")
    
    private lateinit var capturedDocumentUri: Uri
    private lateinit var capturedSelfieUri: Uri
    
    private val documentLauncher = registerForActivityResult(
        ActivityResultContracts.TakePicture()
    ) { success ->
        if (success) {
            uploadDocument()
        } else {
            showError("Failed to capture document")
        }
    }
    
    private val selfieLauncher = registerForActivityResult(
        ActivityResultContracts.TakePicture()
    ) { success ->
        if (success) {
            uploadSelfie()
        } else {
            showError("Failed to capture selfie")
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityNativeVerificationBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupRepository()
        setupClickListeners()
        
        // Generate verification first
        generateAndStartVerification()
    }
    
    private fun setupRepository() {
        repository = VerificationRepository(authManager = AuthManager(this))
    }
    
    private fun generateAndStartVerification() {
        lifecycleScope.launch {
            try {
                binding.progressBar.visibility = View.VISIBLE
                binding.statusText.text = "Generating verification..."
                
                // Step 1: Generate verification ID from backend
                repository.generateVerification()
                    .onSuccess { response ->
                        verificationId = response.verificationId
                        startNativeVerification()
                    }
                    .onFailure { error ->
                        showError("Failed to generate verification: ${error.message}")
                    }
                    
            } catch (e: Exception) {
                showError("Verification generation failed: ${e.message}")
            }
        }
    }
    
    private suspend fun startNativeVerification() {
        try {
            binding.statusText.text = "Starting verification..."
            
            // Step 2: Start verification with v2Client
            repository.startNativeVerification(verificationId)
                .onSuccess {
                    runOnUiThread {
                        binding.progressBar.visibility = View.GONE
                        binding.statusText.text = "Verification started. Please capture your document."
                        binding.captureDocumentButton.visibility = View.VISIBLE
                    }
                }
                .onFailure { error ->
                    showError("Failed to start verification: ${error.message}")
                }
                
        } catch (e: Exception) {
            showError("Start verification failed: ${e.message}")
        }
    }
    
    private fun setupClickListeners() {
        binding.captureDocumentButton.setOnClickListener {
            captureDocument()
        }
        
        binding.captureSelfieButton.setOnClickListener {
            captureSelfie()
        }
        
        binding.submitFormButton.setOnClickListener {
            submitFormData()
        }
        
        binding.validateButton.setOnClickListener {
            validateVerification()
        }
        
        binding.acceptTermsButton.setOnClickListener {
            acceptTerms()
        }
    }
    
    private fun captureDocument() {
        val imageFile = File(externalCacheDir, "document_${System.currentTimeMillis()}.jpg")
        capturedDocumentUri = FileProvider.getUriForFile(
            this,
            "${packageName}.provider",
            imageFile
        )
        documentLauncher.launch(capturedDocumentUri)
    }
    
    private fun uploadDocument() {
        lifecycleScope.launch {
            try {
                binding.progressBar.visibility = View.VISIBLE
                binding.statusText.text = "Uploading document..."
                
                repository.uploadDocument(verificationId, capturedDocumentUri)
                    .onSuccess {
                        runOnUiThread {
                            binding.progressBar.visibility = View.GONE
                            binding.statusText.text = "Document uploaded. Please capture your selfie."
                            binding.captureDocumentButton.visibility = View.GONE
                            binding.captureSelfieButton.visibility = View.VISIBLE
                        }
                    }
                    .onFailure { error ->
                        showError("Document upload failed: ${error.message}")
                    }
                    
            } catch (e: Exception) {
                showError("Upload failed: ${e.message}")
            }
        }
    }
    
    private fun captureSelfie() {
        val imageFile = File(externalCacheDir, "selfie_${System.currentTimeMillis()}.jpg")
        capturedSelfieUri = FileProvider.getUriForFile(
            this,
            "${packageName}.provider",
            imageFile
        )
        selfieLauncher.launch(capturedSelfieUri)
    }
    
    private fun uploadSelfie() {
        lifecycleScope.launch {
            try {
                binding.progressBar.visibility = View.VISIBLE
                binding.statusText.text = "Uploading selfie..."
                
                repository.uploadSelfie(verificationId, capturedSelfieUri)
                    .onSuccess {
                        runOnUiThread {
                            binding.progressBar.visibility = View.GONE
                            binding.statusText.text = "Selfie uploaded. Please fill the form."
                            binding.captureSelfieButton.visibility = View.GONE
                            binding.formSection.visibility = View.VISIBLE
                            binding.submitFormButton.visibility = View.VISIBLE
                        }
                    }
                    .onFailure { error ->
                        showError("Selfie upload failed: ${error.message}")
                    }
                    
            } catch (e: Exception) {
                showError("Selfie upload failed: ${e.message}")
            }
        }
    }
    
    private fun submitFormData() {
        lifecycleScope.launch {
            try {
                binding.progressBar.visibility = View.VISIBLE
                binding.statusText.text = "Submitting form data..."
                
                val formData = mapOf(
                    "first_name" to binding.firstNameEdit.text.toString(),
                    "last_name" to binding.lastNameEdit.text.toString(),
                    "date_of_birth" to binding.dobEdit.text.toString()
                )
                
                repository.submitFormData(verificationId, formData)
                    .onSuccess {
                        runOnUiThread {
                            binding.progressBar.visibility = View.GONE
                            binding.statusText.text = "Form submitted. Please validate verification."
                            binding.submitFormButton.visibility = View.GONE
                            binding.validateButton.visibility = View.VISIBLE
                        }
                    }
                    .onFailure { error ->
                        showError("Form submission failed: ${error.message}")
                    }
                    
            } catch (e: Exception) {
                showError("Form submission failed: ${e.message}")
            }
        }
    }
    
    private fun validateVerification() {
        lifecycleScope.launch {
            try {
                binding.progressBar.visibility = View.VISIBLE
                binding.statusText.text = "Validating verification..."
                
                repository.validateVerification(verificationId)
                    .onSuccess {
                        runOnUiThread {
                            binding.progressBar.visibility = View.GONE
                            binding.statusText.text = "Verification validated. Please accept terms."
                            binding.validateButton.visibility = View.GONE
                            binding.acceptTermsButton.visibility = View.VISIBLE
                        }
                    }
                    .onFailure { error ->
                        showError("Validation failed: ${error.message}")
                    }
                    
            } catch (e: Exception) {
                showError("Validation failed: ${e.message}")
            }
        }
    }
    
    private fun acceptTerms() {
        lifecycleScope.launch {
            try {
                binding.progressBar.visibility = View.VISIBLE
                binding.statusText.text = "Accepting terms..."
                
                repository.acceptTerms(verificationId)
                    .onSuccess {
                        runOnUiThread {
                            binding.progressBar.visibility = View.GONE
                            binding.statusText.text = "Verification completed! Waiting for results..."
                            binding.acceptTermsButton.visibility = View.GONE
                            
                            // Navigate to results or wait for webhook
                            showSuccess("Verification submitted successfully!")
                            navigateToResults()
                        }
                    }
                    .onFailure { error ->
                        showError("Terms acceptance failed: ${error.message}")
                    }
                    
            } catch (e: Exception) {
                showError("Terms acceptance failed: ${e.message}")
            }
        }
    }
    
    private fun navigateToResults() {
        val intent = Intent(this, VerificationResultsActivity::class.java)
        intent.putExtra("verification_id", verificationId)
        startActivity(intent)
        finish()
    }
    
    private fun showSuccess(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }
    
    private fun showError(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
        binding.progressBar.visibility = View.GONE
    }
}

Step 7: Verification Results Display

Create a results screen to display verification status and retrieved images.

Results Activity

class VerificationResultsActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityVerificationResultsBinding
    private lateinit var viewModel: VerificationViewModel
    private var verificationId: String = ""
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVerificationResultsBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        verificationId = intent.getStringExtra("verification_id") ?: ""
        
        setupViewModel()
        setupObservers()
        setupClickListeners()
        
        // Load verification status from backend
        loadVerificationData()
    }
    
    private fun setupObservers() {
        viewModel.verificationStatus.observe(this) { status ->
            updateStatusUI(status)
        }
    }
    
    private fun setupClickListeners() {
        binding.refreshStatusButton.setOnClickListener {
            loadVerificationData()
        }
        
        binding.retryVerificationButton.setOnClickListener {
            // Start new verification
            finish()
            startActivity(Intent(this, VerificationWebViewActivity::class.java))
        }
    }
    
    private fun loadVerificationData() {
        // Get current status from backend via ViewModel
        viewModel.checkVerificationStatus(verificationId)
    }
    
    private fun updateStatusUI(status: VerificationStatusResponse) {
        binding.apply {
            verificationIdText.text = "ID: ${status.verificationId}"
            statusText.text = status.status.uppercase()
            lastUpdatedText.text = "Updated: ${status.updatedAt}"
            
            // Update status color and icon
            when (status.status) {
                "verified" -> {
                    statusText.setTextColor(ContextCompat.getColor(this@VerificationResultsActivity, R.color.green))
                    statusIcon.setImageResource(R.drawable.ic_check_circle)
                    statusIcon.setColorFilter(ContextCompat.getColor(this@VerificationResultsActivity, R.color.green))
                }
                "rejected" -> {
                    statusText.setTextColor(ContextCompat.getColor(this@VerificationResultsActivity, R.color.red))
                    statusIcon.setImageResource(R.drawable.ic_error)
                    statusIcon.setColorFilter(ContextCompat.getColor(this@VerificationResultsActivity, R.color.red))
                }
                "in_progress" -> {
                    statusText.setTextColor(ContextCompat.getColor(this@VerificationResultsActivity, R.color.orange))
                    statusIcon.setImageResource(R.drawable.ic_hourglass)
                    statusIcon.setColorFilter(ContextCompat.getColor(this@VerificationResultsActivity, R.color.orange))
                }
            }
            
            // Show/hide action buttons based on status
            when (status.status) {
                "verified", "rejected" -> {
                    refreshStatusButton.visibility = View.GONE
                    statusMessage.text = "Verification completed."
                }
                "in_progress" -> {
                    progressBar.visibility = View.VISIBLE
                    statusMessage.text = "Verification is being processed..."
                    refreshStatusButton.visibility = View.VISIBLE
                }
                "pending" -> {
                    statusMessage.text = "Verification is pending."
                    refreshStatusButton.visibility = View.VISIBLE
                }
                else -> {
                    retryVerificationButton.visibility = View.VISIBLE
                }
            }
        }
    }
    
    private fun showError(message: String) {
        Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
    }
}

Security Best Practices & Tips

✅ Security Do's

  • Store API keys only on your backend server
  • Use HTTPS for all network communications
  • Validate user ownership of verifications
  • Implement proper authentication on your backend
  • Use certificate pinning for production

❌ Security Don'ts

  • Never store API keys in the mobile app
  • Don't call v2Client APIs directly from mobile
  • Don't trust verification IDs without validation
  • Don't cache sensitive images permanently
  • Don't skip certificate validation

Performance Tips

  • • Use image compression for better performance
  • • Implement proper loading states and error handling
  • • Cache verification results locally when appropriate
  • • Use background processing for network operations
  • • Implement retry logic with exponential backoff

Quick Start Summary

1

Backend Setup

Generate verification IDs and handle webhooks

2

Android Integration

WebView or native API flow with v2Client

3

UI Implementation

Choose WebView or native (both fully supported)

Webhook-driven architecture: Your backend generates verification IDs and receives completion notifications via webhooks. WebView offers fastest implementation, while native provides complete control following the API flow: start-verification → upload-document → upload-selfie → validate-verification → accept-terms.