Android SDK
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 |
|---|---|---|---|
callbackUrl | String | Yes | Webhook URL for verification result |
minAge | Int? | No | Age threshold: 12, 15, 18, 21, or 25 |
successUrl | String? | No | Redirect URL on success |
failedUrl | String? | No | Redirect URL on failure |
userId | String? | No | Your internal user ID (passed through to webhook) |
theme | String? | No | "light" or "dark" |
locale | String? | No | Widget language: "en", "de", "fr", etc. |
metadata | String? | No | Opaque string (up to 500 chars) passed to webhook |
livenessDifficulty | String? | No | "easy", "medium", or "hard" |
purpose | String? | No | Verification 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 |
|---|---|---|
type | String | Event type: "session.completed", "session.failed" |
data | Map<String, String> | Event payload (session details) |
id | String? | Unique event identifier (for deduplication) |
created | Long? | 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 |
|---|---|---|
AuthenticationException | 401, 403 | Invalid or missing API key |
ValidationException | 400 | Bad request parameters, invalid webhook |
NotFoundException | 404 | Token or resource not found |
RateLimitException | 429 | Rate limit exceeded (.retryAfter seconds) |
ServerException | 5xx | Server error (auto-retried by SDK) |
NetworkException | 0 | DNS, timeout, SSL, connection refused |
XidentException | varies | Base 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 |
|---|---|---|
id | String | Unique session identifier (UUIDv7) |
status | SessionStatus | PENDING, IN_PROGRESS, COMPLETED, FAILED, CANCELED, CLAIMED |
livenessResult | Map? | Liveness check outcome |
ageResult | Map? | Age verification outcome |
ocrResult | Map? | Document OCR outcome |
faceMatchResult | Map? | Face matching outcome |
countryCode | String? | ISO 3166-1 alpha-2 country code |
minAge | Int? | Minimum age threshold for this session |
externalUserId | String? | Your user ID (from init userId param) |
requiredMethods | List<String>? | Verification methods required |
remainingAttempts | Int? | Attempts remaining before failure |
createdAt | String | ISO 8601 timestamp |
completedAt | String? | ISO 8601 timestamp (null if in progress) |
expiresAt | String? | ISO 8601 timestamp |
Helper Methods
| Method | Returns | Description |
|---|---|---|
isVerified() | Boolean | True if status is COMPLETED |
isFailed() | Boolean | True if status is FAILED |
isPending() | Boolean | True if PENDING or IN_PROGRESS |
isTerminal() | Boolean | True 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_KEY | Your secret API key (sk_live_ or sk_test_) |
XIDENT_WEBHOOK_SECRET | Webhook signing secret (whsec_) |
Related
- JavaScript SDK -- Client-side browser integration
- Go SDK -- Server-side Go alternative
- iOS SDK -- Swift SDK for Apple platforms
- All SDKs -- Overview of all Xident SDKs
- API Reference -- Full REST API documentation