Skip to main content

Overview

The Vouch Android SDK provides email validation and device fingerprinting for native Android applications. Built with Kotlin, it integrates seamlessly with both Jetpack Compose and traditional Views.
Package: com.github.Vouch-IN:vouch-sdk-android Platform: Android 8.0+ (API 26+) Language: Kotlin 1.9+ Distribution: JitPack (Gradle/Maven)

Installation

Add JitPack repository to your project’s settings.gradle.kts:
settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}
Add the dependency to your app’s build.gradle.kts:
build.gradle.kts
dependencies {
    implementation("com.github.Vouch-IN:vouch-sdk-android:vouch-sdk-android-v0.1.12")
}

Permissions

Add internet permission to your AndroidManifest.xml:
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
No other permissions required. The SDK only needs internet access to communicate with the Vouch API.

Quick Start

import expert.vouch.sdk.Vouch
import kotlinx.coroutines.launch

// Initialize the SDK
val vouch = Vouch(
    context = applicationContext,
    projectId = "your-project-id",
    apiKey = "your-client-api-key"
)

// Validate an email
lifecycleScope.launch {
    val result = vouch.validate("[email protected]")

    if (result.isAllowed) {
        println("Valid: ${result.email}")
    } else {
        println("Error: ${result.errorMessage}")
    }
}

Jetpack Compose Integration

Basic Form Validation

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import expert.vouch.sdk.Vouch
import kotlinx.coroutines.launch

@Composable
fun SignUpScreen() {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    val vouch = remember {
        Vouch(
            context = context.applicationContext,
            projectId = "your-project-id",
            apiKey = "your-client-api-key"
        )
    }

    var email by remember { mutableStateOf("") }
    var isValidating by remember { mutableStateOf(false) }
    var validationMessage by remember { mutableStateOf<String?>(null) }
    var isValid by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        OutlinedTextField(
            value = email,
            onValueChange = {
                email = it
                validationMessage = null
            },
            label = { Text("Email Address") },
            modifier = Modifier.fillMaxWidth(),
            enabled = !isValidating,
            singleLine = true
        )

        Button(
            onClick = {
                scope.launch {
                    isValidating = true
                    validationMessage = null

                    val result = vouch.validate(email)

                    isValidating = false

                    if (result.isAllowed) {
                        isValid = true
                        validationMessage = "Email validated"
                    } else {
                        isValid = false
                        validationMessage = result.errorMessage ?: "Email validation failed"
                    }
                }
            },
            enabled = !isValidating && email.isNotEmpty(),
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(if (isValidating) "Validating..." else "Sign Up")
        }

        validationMessage?.let { message ->
            Text(
                text = message,
                color = if (isValid) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall
            )
        }

        if (isValidating) {
            LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
        }
    }
}

Real-Time Validation

import androidx.compose.runtime.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

@Composable
fun RealtimeEmailField() {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    val vouch = remember {
        Vouch(
            context = context.applicationContext,
            projectId = "your-project-id",
            apiKey = "your-client-api-key"
        )
    }

    var email by remember { mutableStateOf("") }
    var validationState by remember { mutableStateOf<ValidationState>(ValidationState.Idle) }
    var validationJob by remember { mutableStateOf<Job?>(null) }

    LaunchedEffect(email) {
        validationJob?.cancel()

        if (email.isEmpty()) {
            validationState = ValidationState.Idle
            return@LaunchedEffect
        }

        validationState = ValidationState.Validating

        validationJob = scope.launch {
            delay(500) // Debounce

            val result = vouch.validate(email)

            if (result.isAllowed) {
                validationState = ValidationState.Valid
            } else {
                validationState = ValidationState.Invalid(result.errorMessage ?: "Invalid email")
            }
        }
    }

    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            modifier = Modifier.weight(1f),
            singleLine = true,
            isError = validationState is ValidationState.Invalid
        )

        when (validationState) {
            ValidationState.Idle -> {}
            ValidationState.Validating -> CircularProgressIndicator(modifier = Modifier.size(24.dp))
            ValidationState.Valid -> Icon(
                imageVector = Icons.Default.CheckCircle,
                contentDescription = "Valid",
                tint = MaterialTheme.colorScheme.primary
            )
            is ValidationState.Invalid -> Icon(
                imageVector = Icons.Default.Error,
                contentDescription = "Invalid",
                tint = MaterialTheme.colorScheme.error
            )
        }
    }

    if (validationState is ValidationState.Invalid) {
        Text(
            text = (validationState as ValidationState.Invalid).message,
            color = MaterialTheme.colorScheme.error,
            style = MaterialTheme.typography.bodySmall
        )
    }
}

sealed class ValidationState {
    object Idle : ValidationState()
    object Validating : ValidationState()
    object Valid : ValidationState()
    data class Invalid(val message: String) : ValidationState()
}

Traditional View/Activity Integration

Basic Usage

import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import expert.vouch.sdk.Vouch
import kotlinx.coroutines.launch

class SignUpActivity : AppCompatActivity() {
    private val vouch by lazy {
        Vouch(
            context = applicationContext,
            projectId = "your-project-id",
            apiKey = "your-client-api-key"
        )
    }

    private lateinit var emailEditText: EditText
    private lateinit var signUpButton: Button
    private lateinit var resultTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_signup)

        emailEditText = findViewById(R.id.emailEditText)
        signUpButton = findViewById(R.id.signUpButton)
        resultTextView = findViewById(R.id.resultTextView)

        signUpButton.setOnClickListener {
            val email = emailEditText.text.toString()

            if (email.isEmpty()) {
                resultTextView.text = "Please enter an email address"
                resultTextView.setTextColor(ContextCompat.getColor(this, android.R.color.holo_red_dark))
                return@setOnClickListener
            }

            signUpButton.isEnabled = false
            resultTextView.text = "Validating..."
            resultTextView.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray))

            lifecycleScope.launch {
                val result = vouch.validate(email)

                signUpButton.isEnabled = true

                if (result.isAllowed) {
                    resultTextView.text = "Valid email"
                    resultTextView.setTextColor(ContextCompat.getColor(this@SignUpActivity, android.R.color.holo_green_dark))
                    // Proceed with sign-up
                } else {
                    resultTextView.text = result.errorMessage ?: "Invalid email"
                    resultTextView.setTextColor(ContextCompat.getColor(this@SignUpActivity, android.R.color.holo_red_dark))
                }
            }
        }
    }
}

With ViewModel (MVVM Pattern)

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import expert.vouch.sdk.Vouch
import expert.vouch.sdk.models.ValidationResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class SignUpViewModel(private val vouch: Vouch) : ViewModel() {
    private val _validationState = MutableStateFlow<ValidationResult?>(null)
    val validationState: StateFlow<ValidationResult?> = _validationState

    private val _isValidating = MutableStateFlow(false)
    val isValidating: StateFlow<Boolean> = _isValidating

    fun validateEmail(email: String) {
        viewModelScope.launch {
            _isValidating.value = true
            _validationState.value = null

            val result = vouch.validate(email)

            _validationState.value = result
            _isValidating.value = false
        }
    }
}

Configuration

Custom Options

import expert.vouch.sdk.Vouch
import expert.vouch.sdk.models.VouchOptions
import expert.vouch.sdk.models.ApiVersion

val options = VouchOptions(
    endpoint = "https://api.vouch.expert",
    version = ApiVersion.Version(1)  // Use /v1/ endpoint
)

val vouch = Vouch(
    context = applicationContext,
    projectId = "your-project-id",
    apiKey = "your-client-api-key",
    options = options
)

API Version

Control which API version to use:
// Use latest (unversioned) endpoint
val options = VouchOptions(version = ApiVersion.Latest)

// Use specific version
val options = VouchOptions(version = ApiVersion.Version(1))  // Uses /v1/

Core Methods

validate()

Validate an email address with automatic device fingerprinting. Never throws — all errors are returned in the result.
val result = vouch.validate("[email protected]")

if (result.isAllowed) {
    println("Valid email: ${result.email}")
} else {
    println("Error: ${result.errorMessage}")
}
For more detail, access the full response:
val result = vouch.validate("[email protected]")

// Access recommendation directly
println("Recommendation: ${result.recommendation}")
println("Signals: ${result.signals}")

// Or unwrap the full response data
if (result.data != null) {
    when (val data = result.data) {
        is ValidationData.Validation -> {
            println("Checks: ${data.checks}")
            println("Metadata: ${data.metadata}")
        }
        is ValidationData.Error -> {
            println("Error: ${data.response.error} - ${data.response.message}")
        }
    }
}
Returns: ValidationResult
email
String?
The normalized email address
error
String?
Error message if request failed
data
ValidationData?
Response data from server (sealed class with Validation or Error)
statusCode
Int?
HTTP status code
Convenience properties:
isAllowed
Boolean
Whether the validation passed (recommendation is “allow”)
recommendation
String?
The recommendation string (“allow”, “block”, or “flag”)
signals
List<String>?
The validation signals list
errorMessage
String?
A descriptive error message for any failure scenario, null if allowed

generateFingerprint()

Get the device fingerprint directly without validating an email:
val fingerprint = vouch.generateFingerprint()

println("Device: ${fingerprint.hardware.deviceModel}")
println("Manufacturer: ${fingerprint.hardware.manufacturer}")
println("Screen: ${fingerprint.hardware.screenWidth}x${fingerprint.hardware.screenHeight}")
println("OS: Android ${fingerprint.system.osVersion}")
Returns: Fingerprint with the following signals:
hardware
HardwareSignals
Device hardware information (screen, CPU, memory, model, manufacturer)
fonts
FontSignals
System fonts and SHA-256 hash
system
SystemSignals
Android version, SDK level, language, locale, timezone
storage
StorageSignals
SharedPreferences, KeyStore, FileSystem availability
timestamp
Long
Unix timestamp in milliseconds
version
String
SDK version (e.g., “2.0.0”)

Error Handling

The SDK never throws from validate(). All errors (network failures, invalid format, API errors) are captured in the ValidationResult:
val result = vouch.validate(email)

if (result.isAllowed) {
    // Proceed with sign-up
} else {
    // result.errorMessage contains a descriptive error for any failure:
    // - "Invalid email format" (local validation)
    // - "Network error: ..." (connectivity issues)
    // - "Fingerprint generation failed: ..." (device signal error)
    // - "Email blocked: block" (API rejected the email)
    // - API error messages
    showError(result.errorMessage ?: "Validation failed")
}

Accessing Detailed Error Info

For cases where you need more control:
val result = vouch.validate(email)

if (result.data != null) {
    when (val data = result.data) {
        is ValidationData.Validation -> {
            when (data.recommendation) {
                "allow" -> {
                    // Proceed
                }
                "flag" -> {
                    // Show warning but allow
                    showWarning("Please verify your email")
                }
                else -> {
                    // Blocked
                    showError("Email not accepted")
                }
            }
        }
        is ValidationData.Error -> {
            // API returned a structured error
            println("Error code: ${data.response.error}")   // e.g., "invalid_email"
            println("Message: ${data.response.message}")    // e.g., "Email format is invalid"
        }
    }
} else if (result.error != null) {
    // Network or fingerprint error (no API response)
    println("Error: ${result.error}")
    println("Status code: ${result.statusCode ?: 0}")
}

Privacy & Permissions

Minimal Permissions

The Vouch Android SDK only requires the INTERNET permission:
<uses-permission android:name="android.permission.INTERNET" />
No dangerous permissions required. The SDK does not access location, camera, contacts, or any other sensitive data.
No permissions needed for:
  • Location
  • Camera/Photos
  • Contacts
  • Microphone
  • Bluetooth

Data Collection

The SDK collects device fingerprint data for fraud prevention. You must disclose this in:
  1. Your app’s privacy policy
  2. Google Play Data Safety form
  3. GDPR/CCPA notices (if applicable)
What data is collected:
  • Hardware signals: Screen dimensions, CPU cores, memory, device model, manufacturer
  • Font signals: System fonts and SHA-256 hash
  • System signals: Android version, SDK level, language, locale, timezone
  • Storage signals: SharedPreferences, KeyStore, FileSystem availability
No personally identifiable information (PII) is collected. All data is technical device and system information.

ProGuard/R8

The SDK is fully compatible with ProGuard and R8. Obfuscation rules are included automatically in the library. No additional configuration is required.

Performance

  • Fingerprint Generation: ~100-500ms (first time)
  • Email Validation: Instant local validation + network request time
  • Font Collection: ~200-1000ms (can be disabled)
The SDK starts fingerprint generation immediately when initialized, so the first validate() call can reuse the cached fingerprint.

Best Practices

1. Initialize with Application Context

Always use applicationContext to avoid memory leaks:
val vouch = Vouch(
    context = applicationContext,  // Good
    // context = this,  // Bad (Activity context)
    projectId = "...",
    apiKey = "..."
)

2. Use Dependency Injection

Inject the Vouch instance using Hilt or Koin:
// Hilt example
@Module
@InstallIn(SingletonComponent::class)
object VouchModule {
    @Provides
    @Singleton
    fun provideVouch(@ApplicationContext context: Context): Vouch {
        return Vouch(
            context = context,
            projectId = BuildConfig.VOUCH_PROJECT_ID,
            apiKey = BuildConfig.VOUCH_API_KEY
        )
    }
}

3. Handle Errors Gracefully

Always handle validation errors without blocking the user:
val result = vouch.validate(email)

if (result.isAllowed) {
    // Proceed with sign-up
} else if (result.recommendation == "flag") {
    // Show user-friendly warning but allow
    showWarning("Please verify your email")
} else {
    // Don't block the user entirely, allow retry
    showError(result.errorMessage ?: "Validation unavailable, please try again")
}

4. Use StateFlow for UI Updates

Combine with StateFlow for reactive UI updates:
class EmailViewModel(private val vouch: Vouch) : ViewModel() {
    private val _state = MutableStateFlow(EmailState())
    val state = _state.asStateFlow()

    fun validate(email: String) {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true) }

            val result = vouch.validate(email)

            _state.update { it.copy(
                isLoading = false,
                isValid = result.isAllowed,
                error = result.errorMessage
            )}
        }
    }
}

API Reference

Vouch

Main SDK class for email validation and fingerprinting.
class Vouch(
    context: Context,
    projectId: String,
    apiKey: String,
    options: VouchOptions = VouchOptions()
) {
    suspend fun validate(email: String): ValidationResult
    suspend fun generateFingerprint(): Fingerprint
}

VouchOptions

SDK configuration options.
data class VouchOptions(
    val endpoint: String = "https://api.vouch.expert",
    val version: ApiVersion = ApiVersion.Latest
)

ApiVersion

API version specification.
sealed class ApiVersion {
    object Latest : ApiVersion()  // Unversioned endpoint
    data class Version(val number: Int) : ApiVersion()  // Versioned endpoint
}

ValidationAction

Validation action enum used in toggle configuration.
enum class ValidationAction {
    ALLOW,
    BLOCK,
    FLAG
}

ValidationResult

Email validation response structure with convenience properties.
data class ValidationResult(
    val email: String?,
    val error: String?,
    val data: ValidationData?,
    val statusCode: Int?
) {
    // Convenience properties
    val isAllowed: Boolean
    val recommendation: String?
    val signals: List<String>?
    val errorMessage: String?
}

ValidationData

Validation response data — sealed class with two cases.
sealed class ValidationData {
    data class Validation(
        val checks: Map<String, CheckResult>,
        val message: String?,
        val metadata: ValidationMetadata,
        val recommendation: String,  // "allow", "block", or "flag"
        val signals: List<String>
    ) : ValidationData()

    data class Error(val response: ErrorResponseData) : ValidationData()
}

CheckResult

Individual check result.
data class CheckResult(
    val error: String?,
    val latency: Int,
    val metadata: Map<String, JsonElement>?,
    val pass: Boolean
)

ValidationMetadata

Validation metadata.
data class ValidationMetadata(
    val fingerprintHash: String?,  // Device fingerprint hash
    val previousSignups: Int,  // Previous signups from this device
    val totalLatency: Int  // Total validation latency in ms
)

DeviceData

Device fingerprint data returned in validation results.
data class DeviceData(
    val emailsUsed: Int,
    val firstSeen: Int,
    val isKnownDevice: Boolean,
    val isNewEmail: Boolean,
    val lastSeen: Int?,
    val previousSignups: Int
)

IPData

IP address analysis data.
data class IPData(
    val ip: String,
    val isAnonymous: Boolean,  // True if VPN, Tor, or datacenter IP detected
    val isFraud: Boolean
)

ValidationToggles

Toggle configuration for which validations to run.
data class ValidationToggles(
    val alias: ValidationAction?,
    val catchall: ValidationAction?,
    val device: ValidationAction?,
    val disposable: ValidationAction?,
    val ip: ValidationAction?,
    val mx: ValidationAction?,
    val roleEmail: ValidationAction?,
    val smtp: ValidationAction?,
    val syntax: ValidationAction?
)

ErrorResponseData

Error response from the API.
data class ErrorResponseData(
    val error: String,
    val message: String
)

Support

For issues and questions: