Android SDK

Available v1.0.0 Kotlin Coroutines

The Android SDK (io.xident:sdk) is a Kotlin-first client for the Xident age verification API. It uses coroutines (all API methods are suspend functions), a DSL builder for configuration, and offers both exception-based and sealed class (XidentResult) error handling.

Secret key required: The Android SDK is a server-side SDK meant for your backend (Kotlin/JVM). Use your secret key (sk_live_ / sk_test_) from the dashboard. For mobile apps, call your own backend which uses this SDK -- never embed the secret key in client-side code.

Installation

Gradle (Kotlin DSL)

// build.gradle.kts
dependencies {
    implementation("io.xident:sdk:1.0.0")
}

Maven

<!-- pom.xml -->
<dependency>
    <groupId>io.xident</groupId>
    <artifactId>sdk</artifactId>
    <version>1.0.0</version>
</dependency>

Quick Start

import io.xident.sdk.Xident
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val xident = Xident.create("sk_test_xxx")

    // 1. Create a verification session
    val init = xident.verification.init(
        callbackUrl = "https://yoursite.com/webhook",
        minAge = 18,
    )
    println("Redirect user to: ${init.verifyUrl}")

    // 2. After user completes verification, check result
    val session = xident.verification.getResult(init.token)
    if (session.isVerified()) {
        println("Verified! Age bracket: ${session.ageBracket()}")
    }
}

Configuration

Configure the client using the Kotlin DSL builder or direct construction. All settings are optional -- defaults are production-ready.

// DSL builder (recommended)
val xident = Xident.create("sk_live_xxx") {
    baseUrl = "https://api.xident.io"
    timeout = 30_000L       // milliseconds
    maxRetries = 3
    headers = mapOf("X-Custom" to "value")
}

// Direct construction
val xident = Xident(XidentConfig(
    apiKey = "sk_live_xxx",
    baseUrl = "https://api.xident.io",
    timeout = 30_000L,
    maxRetries = 3,
))
Option Default Description
baseUrl https://api.xident.io API base URL. Override for staging or self-hosted.
timeout 30000 (30s) Request timeout in milliseconds.
maxRetries 3 Max retries on 5xx / network errors. Exponential backoff.
headers emptyMap() Extra HTTP headers sent with every request.

Verification

init (Create Session)

Create a verification session. Returns a short-lived token (10-minute TTL) and a URL to redirect the user to. This is a suspend function -- call it from a coroutine scope.

val result = xident.verification.init(
    // Required
    callbackUrl = "https://yoursite.com/webhook",

    // Optional
    minAge = 18,                         // 12, 15, 18, 21, or 25
    successUrl = "https://yoursite.com/success",
    failedUrl = "https://yoursite.com/failed",
    userId = "user_123",                 // your internal user ID
    theme = "dark",                      // "light" or "dark"
    locale = "de",                       // "en", "de", "fr", etc.
    metadata = "order_456",              // up to 500 chars, passed to webhook
    livenessDifficulty = "hard",         // "easy", "medium", "hard"
    purpose = "age_verification",
)

// result.token    -> "xit_abc123..." (10-minute TTL)
// result.verifyUrl -> "https://verify.xident.io/v/xit_abc123"

Parameters

Parameter Type Required Description
callbackUrlStringYesWebhook URL for verification result
minAgeInt?NoAge threshold: 12, 15, 18, 21, or 25
successUrlString?NoRedirect URL on success
failedUrlString?NoRedirect URL on failure
userIdString?NoYour internal user ID (passed through to webhook)
themeString?No"light" or "dark"
localeString?NoWidget language: "en", "de", "fr", etc.
metadataString?NoOpaque string (up to 500 chars) passed to webhook
livenessDifficultyString?No"easy", "medium", or "hard"
purposeString?NoVerification purpose (shown to user)

getResult (Check Session)

Retrieve the verification result for a token. Never trust URL parameters alone -- always re-verify server-side.

val session = xident.verification.getResult("xit_abc123")

println("Status: ${session.status}")              // COMPLETED, FAILED, PENDING, etc.
println("Verified: ${session.isVerified()}")       // true if completed
println("Failed: ${session.isFailed()}")           // true if failed
println("Pending: ${session.isPending()}")         // true if pending or in_progress
println("Terminal: ${session.isTerminal()}")       // true if no more changes possible
println("Age bracket: ${session.ageBracket()}")    // Int?: 12, 15, 18, 21, 25, or null
println("Method: ${session.method()}")             // "ml_fast", "ocr", "self_declaration"

Safe Variants (XidentResult)

Every method has a *Safe variant that returns XidentResult<T> instead of throwing. This sealed class provides onSuccess/onError chaining, getOrNull(), getOrThrow(), getOrDefault(), and map().

// initSafe returns XidentResult<InitResult> instead of throwing
when (val result = xident.verification.initSafe(
    callbackUrl = "https://yoursite.com/webhook",
    minAge = 18,
)) {
    is XidentResult.Success -> {
        println("Token: ${result.data.token}")
        openVerifyUrl(result.data.verifyUrl)
    }
    is XidentResult.Error -> {
        println("Failed: ${result.exception.message}")
        println("Code: ${result.exception.errorCode}")
    }
}

// getResultSafe with chaining
xident.verification.getResultSafe("xit_abc123")
    .onSuccess { session ->
        if (session.isVerified()) grantAccess()
    }
    .onError { error ->
        log.error("Check failed: ${error.message}")
    }

// Other XidentResult utilities
result.getOrNull()          // T? -- null on error
result.getOrThrow()         // T -- rethrows on error
result.getOrDefault(fallback)  // T -- fallback on error
result.map { transform(it) }  // XidentResult<R> -- transform success value

Webhook Verification

Xident sends webhooks to your callbackUrl when verification sessions complete, fail, or expire. The X-Xident-Signature header uses HMAC-SHA256 with the format t=TIMESTAMP,v1=HMAC_HEX. The SDK uses constant-time comparison (MessageDigest.isEqual) to prevent timing attacks.

// In your webhook endpoint handler
val event = xident.webhooks.constructEvent(
    payload = requestBody,                        // raw JSON string
    signature = request.getHeader("X-Xident-Signature"),
    secret = "whsec_your_webhook_secret",
    tolerance = 300,                              // max age in seconds (default: 300)
)

when (event.type) {
    "session.completed" -> {
        println("Session completed: ${event.data}")
        // Grant access
    }
    "session.failed" -> {
        println("Session failed: ${event.data}")
    }
}

// Verify signature only (throws on failure)
xident.webhooks.verifySignature(payload, signature, secret)

// Parse event without signature verification (not recommended)
val event = xident.webhooks.parseEvent(payload)

WebhookEvent Fields

Field Type Description
typeStringEvent type: "session.completed", "session.failed"
dataMap<String, String>Event payload (session details)
idString?Unique event identifier (for deduplication)
createdLong?Unix timestamp of event creation

Error Handling

The SDK provides two error handling styles: exception-based (try/catch) and sealed class (XidentResult). All exceptions extend XidentException, which carries errorCode, requestId, and httpStatus.

// Exception-based API (init, getResult)
try {
    val result = xident.verification.init(
        callbackUrl = "https://yoursite.com/webhook",
        minAge = 18,
    )
} catch (e: AuthenticationException) {
    // HTTP 401/403 -- invalid or missing API key
    println("Auth failed: ${e.message} (code: ${e.errorCode})")

} catch (e: ValidationException) {
    // HTTP 400 -- bad request parameters
    println("Validation: ${e.message}")

} catch (e: NotFoundException) {
    // HTTP 404 -- token or resource not found
    println("Not found: ${e.message}")

} catch (e: RateLimitException) {
    // HTTP 429 -- rate limited
    println("Rate limited, retry after ${e.retryAfter} seconds")

} catch (e: ServerException) {
    // HTTP 5xx -- server error (SDK auto-retries these)
    println("Server error: ${e.message}")

} catch (e: NetworkException) {
    // DNS, timeout, connection refused
    println("Network error: ${e.message}")

} catch (e: XidentException) {
    // Catch-all for any SDK error
    println("SDK error: ${e.message} (code: ${e.errorCode})")
    e.requestId?.let { println("Request ID: $it") }
}

Exception Hierarchy

Exception HTTP Status Description
AuthenticationException401, 403Invalid or missing API key
ValidationException400Bad request parameters, invalid webhook
NotFoundException404Token or resource not found
RateLimitException429Rate limit exceeded (.retryAfter seconds)
ServerException5xxServer error (auto-retried by SDK)
NetworkException0DNS, timeout, SSL, connection refused
XidentExceptionvariesBase class -- catches all SDK errors

Framework Examples

Android ViewModel

// In a ViewModel (Android Jetpack)
class VerifyViewModel(
    private val xident: Xident,
) : ViewModel() {

    private val _state = MutableStateFlow<VerifyState>(VerifyState.Idle)
    val state: StateFlow<VerifyState> = _state.asStateFlow()

    fun startVerification() {
        viewModelScope.launch {
            _state.value = VerifyState.Loading

            xident.verification.initSafe(
                callbackUrl = "https://yoursite.com/webhook",
                minAge = 18,
                theme = "dark",
            )
            .onSuccess { result ->
                _state.value = VerifyState.Ready(result.verifyUrl)
                // Open verifyUrl in Chrome Custom Tab
            }
            .onError { error ->
                _state.value = VerifyState.Error(error.message ?: "Unknown error")
            }
        }
    }

    fun checkResult(token: String) {
        viewModelScope.launch {
            xident.verification.getResultSafe(token)
                .onSuccess { session ->
                    when {
                        session.isVerified() -> _state.value = VerifyState.Verified(session.ageBracket())
                        session.isFailed()   -> _state.value = VerifyState.Failed
                        session.isPending()  -> _state.value = VerifyState.Pending
                    }
                }
                .onError { error ->
                    _state.value = VerifyState.Error(error.message ?: "Unknown error")
                }
        }
    }
}

sealed interface VerifyState {
    data object Idle : VerifyState
    data object Loading : VerifyState
    data class Ready(val verifyUrl: String) : VerifyState
    data class Verified(val ageBracket: Int?) : VerifyState
    data object Failed : VerifyState
    data object Pending : VerifyState
    data class Error(val message: String) : VerifyState
}

Spring Boot / Ktor

// Spring Boot / Ktor server-side usage
import io.xident.sdk.Xident
import org.springframework.web.bind.annotation.*

@RestController
class VerificationController {

    private val xident = Xident.create(System.getenv("XIDENT_SECRET_KEY"))
    private val webhookSecret = System.getenv("XIDENT_WEBHOOK_SECRET")

    @PostMapping("/verify")
    suspend fun startVerification(): Map<String, String> {
        val result = xident.verification.init(
            callbackUrl = "https://example.com/webhook",
            minAge = 18,
        )
        return mapOf(
            "token" to result.token,
            "verify_url" to result.verifyUrl,
        )
    }

    @PostMapping("/webhook")
    fun handleWebhook(
        @RequestBody body: String,
        @RequestHeader("X-Xident-Signature") signature: String,
    ) {
        val event = xident.webhooks.constructEvent(body, signature, webhookSecret)
        when (event.type) {
            "session.completed" -> println("Verified: ${event.data}")
            "session.failed" -> println("Failed: ${event.data}")
        }
    }

    @GetMapping("/result/{token}")
    suspend fun getResult(@PathVariable token: String): Map<String, Any?> {
        val session = xident.verification.getResult(token)
        return mapOf(
            "verified" to session.isVerified(),
            "age_bracket" to session.ageBracket(),
            "method" to session.method(),
        )
    }
}

Response Types

SessionResult

Field Type Description
idStringUnique session identifier (UUIDv7)
statusSessionStatusPENDING, IN_PROGRESS, COMPLETED, FAILED, CANCELED, CLAIMED
livenessResultMap?Liveness check outcome
ageResultMap?Age verification outcome
ocrResultMap?Document OCR outcome
faceMatchResultMap?Face matching outcome
countryCodeString?ISO 3166-1 alpha-2 country code
minAgeInt?Minimum age threshold for this session
externalUserIdString?Your user ID (from init userId param)
requiredMethodsList<String>?Verification methods required
remainingAttemptsInt?Attempts remaining before failure
createdAtStringISO 8601 timestamp
completedAtString?ISO 8601 timestamp (null if in progress)
expiresAtString?ISO 8601 timestamp

Helper Methods

Method Returns Description
isVerified()BooleanTrue if status is COMPLETED
isFailed()BooleanTrue if status is FAILED
isPending()BooleanTrue if PENDING or IN_PROGRESS
isTerminal()BooleanTrue if completed, failed, canceled, or claimed
ageBracket()Int?Verified age bracket (12/15/18/21/25) or null
method()String?Verification method used or null

Environment Variables

Variable Description
XIDENT_SECRET_KEYYour secret API key (sk_live_ or sk_test_)
XIDENT_WEBHOOK_SECRETWebhook signing secret (whsec_)

Related