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
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
Backend Setup
Generate verification IDs and handle webhooks
Android Integration
WebView or native API flow with v2Client
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.