Android SDK Integration Guide
Overview
The Width Identity SDK provides a drop-in identity verification flow for Android applications. It supports KYC and KYB workflows including document capture, liveness detection, and review. The SDK runs in its own Activity; you add a single dependency and call one entry point, and the SDK returns a single result.
Requirements
| Requirement | Minimum |
|---|---|
| Android SDK | API 26 (Android 8.0)+ |
| compileSdk |
36 — the published SDK artifacts declare
minCompileSdk=36 in their AAR metadata, and the build
tooling enforces it, so a lower value fails the build
|
| Kotlin | 1.9+ |
| Gradle / AGP | 8.0+ |
| Java | 11 source/target |
| Host app | Jetpack Compose-based or View-based (the SDK runs in its own Activity) |
Installation
Step 1: Add the Maven Repository
In settings.gradle.kts. The Width repo serves the SDK plus
its Width-hosted transitive dependencies
(com.width.risksec, com.width.camera);
everything else (AndroidX / Kotlin / Ktor / CameraX) resolves from
Google + Maven Central.
The document-capture engines bundled inside the SDK are vendor-hosted, not on Width's Maven, so add their repositories as well — they are required at build time even if a capture provider goes unused. Paste the two provider blocks below exactly as shown.
dependencyResolutionManagement {
repositories {
maven { url = uri("https://maven.width.com/repository/sdk-releases/") }
google()
mavenCentral()
// Document-capture provider repository (bundled transitively; required to resolve)
maven {
url = uri("https://cdn.veriff.me/android/")
content { includeGroup("com.veriff") }
}
// Document-reader provider repository (bundled transitively; required to resolve)
maven {
url = uri("https://maven-master.regulaforensics.com/RegulaDocumentReader/")
content {
includeGroup("com.regula.documentreader")
includeGroup("com.regula.documentreader.core")
includeGroup("com.regula.common")
}
}
}
}Step 2: Add the Dependency
In app-level build.gradle.kts. A single coordinate
transitively pulls the camera, liveness, and supporting libraries
— no BOM, no extra coordinates.
dependencies {
implementation("com.width.identity:sdk-activity:1.0.11")
}minSdk requirement: the SDK requires
minSdk 26.
Step 3: Required Build Settings
Two settings belong to your app module, not to the SDK. The build fails without them — neither touches your source code.
Core library desugaring is required. The bundled AWS
Rekognition liveness engine ships Amplify AARs whose metadata declares
requires core library desugaring, and AGP enforces that on
the app side
(checkReleaseAarMetadata /
checkDebugAarMetadata) regardless of your
minSdk. Add to your app module:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}
android.uniquePackageNames=false is required on AGP
9.
Two artifacts pulled in transitively by the AWS liveness engine
(com.google.ai.edge.litert:litert-support and
litert-support-api, both 1.4.1) declare the same library
namespace org.tensorflow.lite.support — an upstream
packaging bug. AGP 9's manifest merger validates namespace uniqueness
and fails the build with Validation failed, exiting. Add to
your project's gradle.properties:
# gradle.properties android.uniquePackageNames=false
sdk-bom platform plus a
separate sdk-ui-width-modern coordinate. These are no
longer needed — sdk-activity aggregates
everything.
AndroidManifest.xml <meta-data> entry
/ strings.xml. It is now passed at runtime as the
sdkToken field inside CredentialsConfig (see
Quick Start).
Quick Start
The SDK exposes two equivalent entry styles — use whichever fits your call site. Both drive the identical verification UI and return a single result.
Direct input (ActivityResultContract)
import com.width.identity.activity.CredentialsConfig
import com.width.identity.activity.CustomerConfig
import com.width.identity.activity.ThemeConfig
import com.width.identity.activity.UILanguage
import com.width.identity.activity.Verification
import com.width.identity.activity.VerificationInput
import com.width.identity.activity.VerificationResult
class MainActivity : ComponentActivity() {
private val launcher =
registerForActivityResult(Verification()) { result ->
when (result) {
is VerificationResult.Completed -> // .APPROVED / .REJECTED / .REVIEW
Log.i(TAG, "done: ${result.executionId} ${result.decision}")
is VerificationResult.Waiting ->
Log.i(TAG, "waiting: ${result.executionId}")
is VerificationResult.Dismissed ->
Log.i(TAG, "dismissed: ${result.executionId ?: "(no execution)"}")
is VerificationResult.Cancelled ->
Log.i(TAG, "cancelled: ${result.executionId}")
is VerificationResult.Failed ->
Log.w(TAG, "failed: ${result.code} ${result.message}")
}
}
private fun startVerification() {
val input = VerificationInput(
credentials = CredentialsConfig(
domain = "YOUR_DOMAIN",
sdkToken = "YOUR_SDK_TOKEN",
),
customer = CustomerConfig(
// Both customerId and email are required and must be non-blank.
customerId = "user-123",
email = "user@example.com",
),
theme = ThemeConfig(themeMode = "SYSTEM"), // optional: "SYSTEM" / "LIGHT" / "DARK"
eventType = "KYC_EVENT", // optional: "KYC_EVENT" / "KYB_EVENT" to skip in-SDK selection; omit to let the user pick
language = UILanguage.ZH, // optional: UILanguage.EN (default) / .ZH; omit = English (does not follow device locale)
)
launcher.launch(input)
}
}Builder entry (Compose)
A fluent builder assembles the launch input in a single chain —
supply the access token + credentials + applicant, set optional
handlers, then build and launch. It delivers a single
WIDTHCompletionResult. Register the launcher in composition
with the WIDTHVerification contract, then build and launch
at the call site.
import androidx.activity.compose.rememberLauncherForActivityResult
import com.width.identity.activity.WIDTHCompletionResult
import com.width.identity.activity.WIDTHDayNightMode
import com.width.identity.activity.WIDTHMobileSDK
import com.width.identity.activity.WIDTHVerification
@Composable
fun VerifyButton() {
val launcher = rememberLauncherForActivityResult(WIDTHVerification()) { result ->
when (result) {
is WIDTHCompletionResult.SuccessTermination -> // see status table
Log.i(TAG, "done: ${result.status} ${result.executionId}")
is WIDTHCompletionResult.AbnormalTermination ->
Log.w(TAG, "failed: ${result.exception.code} ${result.exception.message}")
}
}
Button(onClick = {
WIDTHMobileSDK.Builder()
.withAccessToken("YOUR_SDK_TOKEN") // RiskSec sdkToken (encodes appId/apiKey)
.withCredentials(domain = "YOUR_DOMAIN") // only domain is client-supplied
.withApplicant(customerId = "user-123", email = "user@example.com")
.withEventType("KYC_EVENT") // optional; "KYC_EVENT"/"KYB_EVENT" to skip in-SDK selection
.withForcedDayNightMode(WIDTHDayNightMode.NIGHT) // optional; omit to follow the system
.withHandlers(onEvent = myLogger) // optional; see Event logging
.withLauncher(launcher)
.build()
.launch()
}) { Text("Verify") }
}
This is an optional entry style, not a separate launch
path: the builder assembles a native VerificationInput and
reuses the same flow underneath. The withAccessToken value
is the sdkToken, which itself encodes the tenant
appId / apiKey (decrypted on-device); only
domain is client-supplied via
withCredentials, and the applicant identity via
withApplicant.
withEventType is optional
("KYC_EVENT" / "KYB_EVENT") — set it to
launch straight into that flow and skip the in-SDK selection screen;
omit it to let the user pick.
VerificationInput(...) throws
IllegalArgumentException on a blank
domain / sdkToken, a blank
customerId / email, or an
eventType that is a non-empty value other than
"KYC_EVENT" / "KYB_EVENT";
build() throws if a required builder field was not set.
Catch it at the call site.
Event logging (optional)
Register an SdkEventLogger to receive SDK lifecycle events
(INFO / WARN / ERROR) in your own pipeline (Crashlytics, Timber,
DataDog, etc.). Known PII is redacted before the callback fires.
SdkEventLogger is a functional interface, so a lambda works
anywhere one is expected.
import com.width.identity.activity.SdkLogLevel
import com.width.identity.activity.SdkLogging
SdkLogging.setLogger { level, tag, message, throwable ->
when (level) {
SdkLogLevel.INFO -> Log.i(tag, message)
SdkLogLevel.WARN -> Log.w(tag, message, throwable)
SdkLogLevel.ERROR -> Log.e(tag, message, throwable)
}
}
The logger is global and optional; call
SdkLogging.setLogger(null) to stop forwarding. You can also
register the same logger inline on the builder via
withHandlers(onEvent = ...).
Configuration Reference
VerificationInput
| Field | Type | Required | Description |
|---|---|---|---|
credentials |
CredentialsConfig |
Yes | SDK authentication |
customer |
CustomerConfig |
Yes | Customer identity |
theme |
ThemeConfig |
No | UI theme customization |
eventType |
String | No |
"KYC_EVENT" / "KYB_EVENT" to skip the
in-SDK selection screen; omit to let the user pick
|
language |
UILanguage |
No |
UI language: UILanguage.EN (default) /
.ZH
|
CredentialsConfig
| Field | Type | Required | Description |
|---|---|---|---|
domain |
String | Yes | Tenant identifier for multi-tenant routing |
sdkToken |
String | Yes |
SDK token (previously registered via manifest
<meta-data>)
|
CustomerConfig
| Field | Type | Required | Description |
|---|---|---|---|
customerId |
String | Yes | Unique customer identifier (must be non-blank) |
email |
String | Yes | Customer email address (must be non-blank) |
customerId and email must be provided
and non-blank. If either is blank, construction throws
IllegalArgumentException. (This tightens the earlier
rule, which only required at least one of the two.)
Public API
com.width.identity.activity is the only customer-facing
package — everything else is internal and produces a compile error
if referenced from your app.
| Symbol | Purpose |
|---|---|
Verification |
ActivityResultContract entry point
|
VerificationInput / CredentialsConfig /
CustomerConfig
|
Launch parameters |
VerificationResult / VerificationDecision
|
Direct-input result |
WIDTHMobileSDK.Builder + WIDTHVerification
|
Builder entry point (Compose) |
WIDTHCompletionResult / WIDTHStatus /
WIDTHException
|
Builder result types |
ThemeConfig / WIDTHDayNightMode
|
Optional theme / day-night override |
UILanguage |
Optional UI language (EN default /
ZH)
|
SdkLogging / SdkEventLogger /
SdkLogLevel
|
Optional event log callback |
UILanguage
| Value | Description |
|---|---|
EN |
English (default) |
ZH |
Simplified Chinese |
Omitting language keeps English — the SDK does not
follow the device locale. Third-party capture screens have limits:
Veriff shows Chinese only when the device is online and the tenant
integration enables it (English otherwise); Regula supports Simplified
Chinese only.
Handling Results
Direct-input VerificationResult
VerificationResult is a sealed class with five variants:
| Result | Fields | Description |
|---|---|---|
Completed |
executionId, decision |
Server returned a decision: APPROVED /
REJECTED / REVIEW (under manual review
out-of-band)
|
Waiting |
executionId |
Await server notification for the final decision |
Dismissed |
executionId? |
User exited intending to return later (X → keep progress, or
pre-workflow exit). The server execution is kept alive so the next
launch resumes; executionId is null if
the workflow had not started. Resumable.
|
Cancelled |
executionId |
User explicitly discarded the verification (X → discard).
The SDK awaited workflow.execution.cancel on the
server, so the execution is destroyed and
cannot be resumed.
|
Failed |
code, message, executionId?
|
Internal error |
Cancelled case is now split:
Dismissed means the user paused and can resume;
Cancelled means the user discarded and the server
execution is destroyed (not resumable).
Builder WIDTHCompletionResult
| Case | When |
|---|---|
SuccessTermination(status, executionId?) |
Flow ended normally; final state carried by
status
|
AbnormalTermination(exception, executionId?) |
Technical failure; exception.code /
exception.message carry detail
|
status is a WIDTHStatus:
Approved, FinallyRejected,
Pending (manual review), Incomplete (resumable
/ dismissed), Cancelled (destroyed).
exception is a WIDTHException: one of
Init, Network, Api,
Permission, Workflow,
Unknown — each exposing code and
message.
Theme Customization
ThemeMode
| ThemeMode | Behavior |
|---|---|
"SYSTEM" |
Follows device setting |
"LIGHT" |
Forces light theme |
"DARK" |
Forces dark theme |
Set the mode via ThemeConfig, or override day/night on the
Builder entry point. Omit either to follow the system setting.
ThemeConfig(themeMode = "SYSTEM") // "SYSTEM" / "LIGHT" / "DARK" // On the Builder entry point — omit to follow the system: .withForcedDayNightMode(WIDTHDayNightMode.NIGHT)
Resume & Lifecycle
| User Choice | Behavior | Result | Resumable? |
|---|---|---|---|
| Pause & Exit (keep progress) | Server execution kept alive | Dismissed |
Yes |
| End Verification (discard) | Server cancel, execution destroyed | Cancelled |
No |
To resume, call launcher.launch(...) with the same
customerId. The SDK auto-detects in-progress workflows.
Permissions
The SDK and its bundled capture engines declare every permission they need, and they are merged into your app automatically — you do not declare or request them:
<!-- WIDTH identity SDK core --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Added transitively by the capture / liveness engine --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.VIBRATE" /> <!-- Added transitively by the document-reader engine --> <uses-permission android:name="android.permission.FLASHLIGHT" />
| Permission | Type | Purpose / Source |
|---|---|---|
CAMERA |
Runtime | Document capture, liveness (SDK core) |
INTERNET |
Install-time | API communication (SDK core) |
ACCESS_NETWORK_STATE |
Install-time | Connectivity checks (SDK core) |
RECORD_AUDIO |
Runtime | Merged by the capture / liveness engine |
NFC |
Install-time | Merged by the capture / liveness engine |
WAKE_LOCK |
Install-time | Merged by the capture / liveness engine |
VIBRATE |
Install-time | Merged by the capture / liveness engine |
FLASHLIGHT |
Install-time | Merged by the document-reader engine |
CAMERA and
RECORD_AUDIO; the SDK and its capture engines request
them from inside their own Activities, so your app does not prompt for
them. Every permission above appears in your app's permission listing
and
must be reflected in your Google Play Data Safety
disclosure. The SDK requires no storage / media permission — picking a
document image uses the system photo picker.
ProGuard / R8: The SDK ships with embedded consumer rules. No additional configuration needed.
Troubleshooting
| Issue | Solution |
|---|---|
Failed to resolve com.width.identity:...
|
Ensure the Width Maven repo is in
settings.gradle.kts (not
build.gradle.kts), and that you use the single
coordinate com.width.identity:sdk-activity:1.0.11 (no
BOM). If the artifacts that fail to resolve are
com.veriff or com.regula.*, the two
provider repositories from Step 1 are missing.
|
IllegalArgumentException on launch
|
Check that domain / sdkToken and both
customerId and email are non-blank. Also
check eventType: if you pass it, the value must be
exactly "KYC_EVENT" or
"KYB_EVENT".
|
| Camera not working |
Check the merged manifest for permission conflicts; the SDK
auto-merges CAMERA from its library manifest.
|
| Theme colors not applied |
Verify hex values (no # prefix), case-sensitive
token names
|
Build fails on checkReleaseAarMetadata /
checkDebugAarMetadata with
requires core library desugaring
|
Enable core library desugaring in your app module and add the
desugar_jdk_libs dependency — see Installation,
Step 3. This is enforced regardless of your
minSdk.
|
Build fails on the same AAR metadata check because the module's
compileSdk is lower than the SDK requires
|
The SDK's AARs declare minCompileSdk=36 in their AAR
metadata and the build tooling enforces it, so a module compiling
against anything lower stops the build. Set
compileSdk = 36 in your app module — see
Requirements. This is separate from minSdk, which
stays 26.
|
Manifest merge fails with
Validation failed, exiting on AGP 9
|
Add android.uniquePackageNames=false to your
project's gradle.properties — see Installation,
Step 3.
|
build.gradle.kts; Gradle fetches the new artifacts from
maven.width.com on the next sync.
Integration notes
Use the package access and installation steps above. Keep the SDK build and issued credentials aligned with the intended WIDTH environment.