Android SDK
A native verification flow built entirely on Jetpack Compose. Add one dependency, launch the flow with a config and a listener, and receive a typed decision — the API mirrors the Web and iOS SDKs.
Drop-in identity verification for Android — document capture, selfie / face match, liveness, and OTP — launched from your app with a few lines of Kotlin.
The SDK takes over the foreground in a full-screen flow, drives the whole
verification, and hands you a typed decision (Approved / Declined /
InReview) through a listener. The API surface mirrors the web SDK 1:1, so
error codes, session statuses, and analytics events line up across platforms.
- Coordinate:
com.othento:othento-core:0.1.3 - Min SDK: 24 · compile/target: 34 · Kotlin: 2.0+
- UI: renders with Jetpack Compose internally — your app does not need Compose.
Requirements
Before wiring the SDK in, make sure you have:
- A partner account on the Othento platform with at least one workflow configured. Workflow IDs are issued from your dashboard.
- A public API key (
pk_sandbox_…orpk_live_…). Sandbox keys hit the test environment; live keys bill against your plan. There is no environment flag — the key decides. See Sandbox vs production.
| Setting | Value |
|---|---|
minSdk |
24 (Android 7.0) |
compileSdk / targetSdk |
34 |
| Kotlin | 2.0+ |
| JDK (build) | 17 |
If you don't have credentials yet, contact your account manager to be onboarded.
Permissions
The SDK declares the permissions it needs in its own AndroidManifest.xml;
they are merged automatically into your app — you do not declare them
yourself.
| Permission | Why | Granted |
|---|---|---|
INTERNET |
API communication with the Othento backend. | Install-time (no prompt). |
CAMERA |
Document capture, selfie, and liveness. | Runtime — the SDK requests it at the moment of first capture, and shows an in-flow rationale + "Open Settings" path if permanently denied. |
Notes:
- The SDK records audio-less video for liveness — it does not request
RECORD_AUDIO. - It does not request
READ_MEDIA_*/ storage permissions; the optional "upload from library" fallback uses the system photo picker. <uses-feature android:name="android.hardware.camera.any" android:required="true" />is declared, so on devices without a camera the app is filtered on the Play Store. If you support camera-less devices, overriderequiredtofalsein your manifest.
Install
The SDK is served from a GitHub static-Maven repository, and transitively depends on one component hosted on JitPack, so add both repositories.
// settings.gradle.kts → dependencyResolutionManagement (or your root repositories block)
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
// Othento SDK:
maven { url = uri("https://raw.githubusercontent.com/Othento/android-sdk/main") }
// Required transitively (document-detection component):
maven { url = uri("https://jitpack.io") }
}
}⚠️ Without
jitpack.iothe build fails to resolvecom.github.pqpo:SmartCropper.
dependencies {
implementation("com.othento:othento-core:0.1.3")
}Core-library desugaring is required
The SDK's AWS Face Liveness component requires core-library desugaring. Gradle does not inherit this setting from a library module, so you must enable it in your own app module:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("com.othento:othento-core:0.1.3")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}Without it the build fails with:
Dependency 'com.amplifyframework:core:2.29.0' requires core library desugaring to be enabled for :app.
Versions the SDK brings onto your classpath
The SDK pulls these transitively. If you pin any of them yourself, pin at or above these versions:
| Dependency | Version | Why |
|---|---|---|
| OkHttp | 5.0.0-alpha.14 |
Required by the AWS streaming client (it needs okhttp-coroutines, which has no 4.x equivalent). |
| Compose BOM | 2025.03.01 |
Carried by the AWS Face Liveness UI component. |
Quick start
Launch the flow from an Activity and observe the listener:
import com.othento.core.api.OthentoSDK
import com.othento.core.api.OthentoConfig
import com.othento.core.api.OthentoSDKListener
import com.othento.core.api.OthentoDecision
import com.othento.core.api.OthentoError
import com.othento.core.api.OthentoCancelReason
import com.othento.core.api.OthentoSessionStatus
val config = OthentoConfig.Builder()
.create(
workflowExternalId = "<<YOUR_WORKFLOW_ID>>",
clientData = "user-123", // your end-user identifier
)
.apiKey("<<YOUR_PUBLIC_API_KEY>>") // pk_sandbox_… or pk_live_…
.build()
OthentoSDK.launch(activity, config, object : OthentoSDKListener {
override fun onReady() {}
override fun onSessionCreated(externalId: String, sessionUrl: String) {
// Persist externalId against your user record.
}
override fun onStatusChanged(status: OthentoSessionStatus) {}
override fun onCompleted(decision: OthentoDecision, externalId: String) {
// decision = Approved | Declined | InReview
}
override fun onCancelled(reason: OthentoCancelReason) {}
override fun onError(error: OthentoError, displayMessage: String) {
// error.code is a stable identifier; displayMessage is user-facing.
}
})launch(activity, config, listener) constructs and starts the SDK in one call.
activity is your current Activity (e.g. this). Configuration is validated
synchronously — missing or malformed fields throw OthentoConfigException
from build(); they never arrive via onError.
Tearing down
val sdk = OthentoSDK(activity, config, listener) sdk.start() // later, to cancel before completion: sdk.destroy() // fires onCancelled(HostDestroy); a safe no-op after a terminal event
Exactly one of onCompleted / onCancelled / onError fires per session, and
only one SDK instance may run at a time.
Sandbox vs production
There is no environment flag. The API-key prefix decides: pk_sandbox_…
hits the test environment, pk_live_… bills real verifications. Same SDK build.
Configuration reference
OthentoConfig.Builder:
| Method | Required | Purpose |
|---|---|---|
create(workflowExternalId, clientData) |
✅ | The workflow id + your end-user identifier. |
apiKey(String) |
✅ | Public API key (pk_sandbox_… / pk_live_…). |
callbackUrl(String) |
optional | Redirect URL forwarded to the create-session call. |
callbackReceiver(OthentoCallbackReceiver) |
optional | Which webhook the platform invokes: Initiator / Completer / Both. |
metadata(String) |
optional | Free-form string round-tripped on session events / webhooks. |
expectedDetails(OthentoExpectedDetails) |
optional | Identity hints to compare against extracted data (see below). |
closeOnComplete(Boolean) |
optional (default false) |
Auto-dismiss the SDK on any terminal screen instead of leaving it up. |
loggingEnabled(Boolean) |
optional (default false) |
Verbose, auth-redacted HTTP logging in release builds for debugging. |
language(String) |
optional | BCP-47 language code ("en", "ar", …) recorded on the session at create time and used to resolve SDK copy. Omit it and the backend applies the tenant default. |
In token mode the session already carries a language (chosen by whoever
minted the SAT), so language(...) acts only as a fallback when the session
does not report one. Either way the backend is the authority: request a
language the tenant does not publish and it resolves to the tenant default.
Users can also switch language in-flow when the tenant publishes more than
one; right-to-left languages lay the whole flow out RTL.
OthentoExpectedDetails
All fields are optional. Send only what you already know about the user. The backend compares these values with the data read from the document, the selfie, and the user's connection. Leave out any field you don't know. Don't send an empty string or a placeholder, because it is compared like a real value.
| Field | Type | What to send | Example |
|---|---|---|---|
firstName |
String? |
The user's first (given) name. | "Sara" |
lastName |
String? |
The user's last (family) name. | "Haddad" |
dateOfBirth |
String? |
Date of birth. Must be yyyy-MM-dd: 4-digit year, 2-digit month, 2-digit day, zero-padded. |
"1988-01-01" |
gender |
String? |
"M" or "F", a single uppercase letter. |
"M" |
nationality |
String? |
The user's nationality as an ISO 3166-1 alpha-3 country code (3 uppercase letters). | "JOR" |
country |
String? |
The user's country as an ISO 3166-1 alpha-3 country code (3 uppercase letters). | "JOR" |
address |
String? |
The user's address as free text. | "Amman, Jordan" |
documentNumber |
String? |
The ID document number. | "A1234567" |
ipAddress |
String? |
The IPv4 address you expect the end user to connect from. | "203.0.113.10" |
OthentoExpectedDetails(
firstName = "Sara",
lastName = "Haddad",
dateOfBirth = "1988-01-01", // must be yyyy-MM-dd
gender = "F", // "M" or "F"
nationality = "JOR", // ISO 3166-1 alpha-3
country = "JOR", // ISO 3166-1 alpha-3
address = "Amman, Jordan",
documentNumber = "A1234567",
ipAddress = "203.0.113.10", // expected end-user IPv4
)Lifecycle & callbacks
launch() → onReady → onSessionCreated
↓
onStatusChanged (deduped, n×)
↓
exactly one terminal callback:
onCompleted | onCancelled | onErrorAll callbacks are dispatched on the Android main thread, asynchronously —
they never run synchronously inside start()/launch(). Exceptions thrown
from your callback are caught and logged; they do not crash the SDK.
| Callback | When it fires |
|---|---|
onReady() |
SDK is up. Fires once, before any other event. |
onSessionCreated(externalId, sessionUrl) |
A new session was minted. Save externalId against your user record. |
onStatusChanged(status) |
Session status transitioned. Deduplicated — never the same status twice in a row. |
onCompleted(decision, externalId) |
Terminal decision reached: Approved, Declined, or InReview. |
onCancelled(reason) |
User dismissed, host called destroy(), or the SDK cancelled before a terminal status. |
onError(error, displayMessage) |
Non-decision terminal error. error.code is stable; displayMessage is the localized user-facing string. |
Status & decision values
enum class OthentoSessionStatus { Created, InProgress, Processing, Completed, Expired, Failed }
enum class OthentoDecision { Approved, Declined, InReview }These names are identical to the web SDK's, so the same analytics and webhook payloads work across platforms.
Error codes
OthentoError is a sealed class; error.code is the stable wire string. Switch
on the subtype (or code) for exhaustive handling.
code |
Meaning | Recommended action |
|---|---|---|
config_invalid |
Required field missing/malformed (thrown sync from build()). |
Fix at build time. |
missing_api_key |
No apiKey. |
Provide the public API key. |
missing_workflow |
No workflowExternalId. |
Provide the workflow ID. |
missing_client_data |
No clientData. |
Pass your end-user identifier. |
session_not_found |
Session id didn't resolve (HTTP 404). | Start a fresh session. |
network |
Connectivity / DNS / TLS failure. | Retry. |
create_failed |
Session could not be created. | Verify apiKey + workflowExternalId. |
documents_failed |
Document list/processing failed. | Prompt retry with better lighting. |
initiate_failed |
Flow failed to start after session creation. | Retry on a fresh instance. |
upload_failed |
Evidence upload failed; error.stage says which step (UploadUrl / S3Put / ConfirmUpload). |
Retry; check connectivity. |
poll_failed |
Polling for the result failed. | Retry; the session may still resolve. |
file_too_large |
Picked file exceeded the upload limit (>10 MB). | Prompt the user to retry capture. |
unknown |
Unclassified runtime error. | Show displayMessage; capture logs + externalId for support. |
override fun onError(error: OthentoError, displayMessage: String) {
when (error) {
is OthentoError.Network -> showRetry()
is OthentoError.SessionNotFound -> restartSession()
is OthentoError.UploadFailed -> showRetry() // error.stage available
else -> showGenericError(displayMessage)
}
}Cancellation reasons
onCancelled(reason) fires once with a typed OthentoCancelReason:
reason |
Trigger |
|---|---|
HostDestroy |
Host called sdk.destroy() before a terminal event. |
SdkInternal |
The SDK's own UI cancelled the session. |
BackButton |
System back button / back gesture. |
ProGuard / R8
Nothing to add. The AAR ships consumer-rules.pro keeping the public API, the
Moshi DTOs + generated adapters, and the bundled native ML components — your
release build works without host-side keep-rules.
Testing your integration
A healthy integration produces, in order:
onReadyshortly afterlaunch().onSessionCreatedwith a non-emptyexternalId.onStatusChanged(InProgress)at least once.- Exactly one terminal callback.
Use the sandbox test documents from your dashboard to exercise approved / declined / in-review paths deterministically.
- Sandbox key in place; the flow launches and
onReadyfires - Approved-path document →
onCompleted(Approved) - Declined-path document →
onCompleted(Declined) - In-review document →
onCompleted(InReview) - Back out mid-flow →
onCancelled(BackButton)
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Build error: cannot resolve com.github.pqpo:SmartCropper |
jitpack.io not in repositories. |
Add maven { url = uri("https://jitpack.io") }. |
Build error: cannot resolve com.othento:othento-core |
Othento Maven repo missing or wrong branch. | Add https://raw.githubusercontent.com/Othento/android-sdk/main. |
onError(create_failed) immediately |
Wrong/expired apiKey or workflowExternalId. |
Verify credentials and that the key matches the environment. |
| Camera screen never appears | Camera permission permanently denied. | The SDK shows an "Open Settings" path; the user must grant it. |
| Release build crashes during JSON parse | Aggressive R8 in a non-standard setup stripping DTOs. | The shipped consumer rules cover this; file an issue if you've customized R8. |
Versioning
SemVer. While on 0.x the public API may change between
minor versions; the first frozen API ships as 1.0.0. Pin an exact version in
production. Pre-1.0.0 versions may be re-published on the GitHub repo.
Support
Include the SDK version (0.1.3), the externalId of the affected session, and
a logcat capture (enable loggingEnabled(true) while reproducing) when
contacting your account manager or opening a ticket.
License
MIT — see LICENSE. Release history in CHANGELOG.md.