Skip to main content

SmartComply Android SDK

The SmartComply Android SDK delivers a fully self-contained identity verification flow for Android apps. Launch one Activity and the SDK handles session management, country and ID-type selection, document capture, identity verification, and liveness detection automatically.

Features

  • Single-Activity launch — start verification with one Intent and receive a typed result back
  • Two verification modes — document photo capture or ID number data entry, configured from your Dashboard
  • Guide-box document capture — frames the ID card precisely so images are always clean and correctly cropped
  • ID photo checked on the phone: for passports and the older Kenyan national ID, with every other document checked by the backend as before, the code at the bottom of the document is read on the device before upload, so a cut-off or blurred photo is retaken before it costs a check. See Checking the photo on the phone
  • ID checked before the selfie: the backend reads the ID and checks it against the government registry first, so a cropped or misread ID is retaken on the spot. See Document check
  • Liveness detection — a passive camera scan (blink plus natural head movement) runs automatically after identity verification, with no step-by-step prompts
  • Dynamic ID types — channels and fields are fetched live from your Dashboard configuration
  • Multi-country support — renders a country picker automatically when more than one country is configured
  • Dark and light mode — theme adapts to the system setting; override via the launch intent
  • Your brand, not ours: colour and logo come from the Dashboard, your name appears while the session is being set up, and the font is passed by your app. See Branding
  • Any backend host: production, staging, or your own, without a rebuild
  • Resubmit: a failed verification can be reopened from the Dashboard, and the customer redoes only the step that failed. See Resuming a failed verification

Requirements

  • Android API 24 (Android 7.0) or later
  • Kotlin 2.1 or later — the SDK is compiled with the Kotlin 2.1 toolchain, so an older consumer compiler rejects it
  • Java 17 — the SDK targets JVM 17
  • Jetpack Compose enabled in your module

Installation

1 — Add Maven Central

In settings.gradle.kts (already present in most projects):

2 — Add the dependency

In your app or feature module build.gradle.kts:
1.4.0 checks the ID photo on the phone. For a Kenyan national ID (back) and a passport, the review screen reads the code at the bottom of the document before upload and asks for a retake if it is cut off or unclear. It adds one dependency, ML Kit text recognition, and the manifest entry changes from face to face,ocr. See Checking the photo on the phone.
1.3.0 adds Resubmit. Pass the resume token from a Dashboard Resubmit to buildIntent and the customer redoes only the step that failed. It changes nothing for a launch without one. A dead token returns TYPE_FAILURE with the new RESULT_ERROR_CODE extra. See Resuming a failed verification.
Use 1.0.5 or later. 1.0.3 and 1.0.4 crash the host app: both read HTTP responses on the main thread, and 1.0.3 additionally crashes when the SDK is closed from a UI callback. Neither can be withdrawn from Maven Central, so pin deliberately.
Take 1.2.1 or later, not 1.2.0. Up to 1.2.0, on some phones the autoshot and snapshot upload as a solid green image with vertical stripes instead of the user’s face. It depends on how the phone’s camera lays out its frames, so it shows on some models and never on others. Liveness and the face match are unaffected, because both read the frame correctly; only the stills on the dashboard are wrong. 1.2.1 is that one fix and changes nothing else.
1.2.0 checks the ID before the selfie, and holds the selfie until the face is in the oval. The ID now goes up on its own and the backend reads it and checks it against the government registry before the liveness camera opens, so a cropped or misread ID is turned back with a retake screen instead of failing after the whole flow. The selfie and the scan then wait until the user’s whole face is inside the oval, with no timeout. Two new ways to end the flow return TYPE_FAILURE. Read Document check and Liveness before you upgrade.
1.1.2 changes when the liveness selfie is taken. Up to 1.1.1 it fired on a fixed timer half a second after the camera opened, before anything had looked for a face, so the autoshot on the dashboard was routinely of nobody, of a ceiling, or of half a face. It now waits for a frame with a whole face in it. That adds up to 2.5 seconds to the start of the check for a user who is slow to get into frame, and nothing at all for one who is already there. 1.2.0 removes that 2.5 second limit; see Liveness.
Take 1.1.1 or later, not 1.1.0. In 1.1.0 every liveness selfie uploads rotated 90 degrees, so a verification that passes can still report “Face Detected: No” in the dashboard’s Biometrics panel. 1.1.1 is that one fix and changes nothing else.
1.1.0 changes how documents are captured. The camera step now takes a photograph rather than a still of the viewfinder, so it binds a second CameraX use case in your process, uploads an image roughly five times larger in pixels, and takes a moment on the shutter where it used to be instant. Read Document images before you upgrade, and re-test your document flow. It also fixes a genuine ID being rejected with “No face detected” and a first run on a device that has never used ML Kit.
1.0.7 changed two behaviours and they still apply. RESULT_STATUS holds the real verdict, and the success screen does not mean a pass. Read Result Extras before upgrading from 1.0.6 or below.

3 — Enable Compose


Credentials

Both values come from your Adhere Dashboard and both are permanent. Reuse the same pair for every verification.
clientId is not a per-attempt value. Generating your own UUID returns 404 SDK_CONFIG_NOT_FOUND on the very first call, because the server resolves it against an existing SDK Config record.

Permissions

The SDK declares these permissions automatically via manifest merge. You do not need to add them manually unless your project uses a custom manifest merge strategy:
SmartComplyActivity requests the CAMERA permission at runtime before it starts the flow, so if you launch through the Activity your app does not need to request it separately. If you embed SmartComplyFlowScreen directly, see Advanced: Custom Host Activity. From 1.1.0 one meta-data entry merges in as well. From 1.4.0 it reads:
It asks Play Services to fetch the face detection model, and from 1.4.0 the text recognition model, when your app installs. Without it the model is fetched the first time a detector runs, and that first call fails: the document check reported it as “Could not verify image. Please retake.” no matter how many times the user retook, and liveness ignored every action the user performed until it timed out. If your build strips unknown meta-data, keep this one. If your own manifest declares com.google.mlkit.vision.DEPENDENCIES with tools:replace, include both face and ocr in your value, or the photo check on the phone is skipped until the model arrives.

Quick Start

1 — Register the result launcher

In your Activity or Fragment:

2 — Launch verification

Jetpack Compose

If you are launching from a composable, use rememberLauncherForActivityResult:

buildIntent Parameters


Environments

SANDBOX targets a server running on the handset itself, for local backend development. It is not a hosted test environment. Use PRODUCTION for all integration work.

A different host

Environment covers the two hosts you ship against. For anything else, a staging deployment or a QA build that has to reach more than one backend without being rebuilt, override it. Added in 1.0.7.
A trailing slash is trimmed, and a blank value falls back to environment rather than being treated as a host, so a build reading this off an empty field does not post verifications to a relative URL.
SANDBOX is a plain http:// target, which Android blocks by default from API 28 onwards. Without an opt-in the flow dies at the loading screen with a network error that looks like a backend outage. If you genuinely need it, scope the exception to localhost rather than reaching for android:usesCleartextTraffic="true", which permits cleartext for every host your app talks to.
PRODUCTION is HTTPS and needs none of this.
SmartComplyActivity.buildIntent defaults to PRODUCTION, but the SDKConfig constructor used by the embedded flow defaults to SANDBOX. Set environment explicitly when you construct SDKConfig yourself.

Result Extras

Read from the Intent returned to your activity result callback.
From 1.0.7 the Activity returns TYPE_FAILURE when apiKey or clientId is blank, which is the usual sign of an unset BuildConfig field or a missing CI value. That result carries no other extras. Before 1.0.7 the same input crashed the host app.From 1.2.0 TYPE_FAILURE is also returned when the user taps End session on a rejected ID, with the backend’s reason in RESULT_ERROR_MSG, and when they tap Close after the session has used its three document attempts, with "Verification sent to manual review". See Document check.
RESULT_VERIFIED_NAME and RESULT_ID_NUMBER are populated from the result poll as well as from submit, from 1.0.7. In document mode they previously appeared only when OCR happened to finish before the user completed liveness, so whether a user saw their own name came down to timing.
Branch on RESULT_STATUS. Reaching this callback is not a pass.From 1.0.7 the SDK polls the backend for the terminal outcome and puts it here, so "passed" means passed and "failed" means failed. But every submitted entry shows the end user the success screen, including a failed one. That is deliberate and was asked for: tenants wanted their user to see a submitted verification as accepted and to read the real verdict themselves. The screen is neutral-positive; this value is not.So a TYPE_SUCCESS result with RESULT_STATUS of "failed" is a normal outcome you must handle. Code that treats TYPE_SUCCESS as a pass will admit users who failed verification."processing" means the backend had not resolved within about 15 seconds. The entry exists and is billed, and the outcome arrives on your webhook.Before 1.0.7 RESULT_STATUS was always "processing" and carried no verdict at all, so the result was a submission receipt.
The liveness.completed webhook is the authoritative record. The value here is the same verdict delivered earlier for convenience, and it is absent when the worker is slow; the webhook always arrives.
Also changed in 1.0.7: pressing back on the success screen now returns TYPE_SUCCESS, not TYPE_CANCELLED. It previously reported a cancellation for a verification that had succeeded and been billed. If your TYPE_CANCELLED branch assumed “nothing happened”, it was already wrong, but it will now be reached less often.

Verification Flow

The SDK steps through these screens automatically.

Document check

From 1.2.0 the ID is checked before the liveness camera opens. Up to 1.1.2 it went up with the selfie once the scan had started, so a cropped or misread ID was only rejected after the whole flow, and Try Again threw away both photos. After the last photo the SDK uploads the ID on its own, polls for the result for up to 90 seconds, and shows the user one of these: The message on the rejection screens is the backend’s own. A retake reuses the same entry, and liveness then submits to that entry rather than creating a new one. A rejected ID is not billed. End session and Close return TYPE_FAILURE with the reason in RESULT_ERROR_MSG. Data mode is unchanged: it has no document to check.

Checking the photo on the phone

From 1.4.0, before a photo is uploaded, the SDK reads the machine readable zone (the two or three lines of letters, numbers and < at the bottom of the document) on the phone and checks every line is there and every check digit adds up. A cut-off, blurred or covered photo is caught in under a second, instead of after the backend has read it. This is an extra check in front of the backend, not a limit on which documents are supported. Every document type your Dashboard offers still works on both SDKs. More documents will get the check on the phone as their code formats are confirmed. What the user sees on the review screen: The message says the document is cut off or unclear, or, when no code is found at all, that the user may have photographed the wrong side. The check never blocks a photo it cannot judge. If the text model is not on the phone yet, the check takes longer than 5 seconds, or the card is a different Kenyan ID (the Maisha Namba), the photo goes through exactly as in 1.3.0 and the backend checks it. Nothing read from the code is sent anywhere or logged.

Document images

Document photos are capped at 5 MB server-side, in JPG or PNG. Both routes are held to 1600px on the long edge and re-encoded at JPEG quality 85, so a full-resolution phone photo does not fail with File too large. Maximum size is 5MB.
From 1.1.0 the camera route produces a real photograph. Up to 1.0.7 it read the viewfinder and cropped that, which on a 1080p screen is roughly 700 real pixels across the card. It now binds a CameraX ImageCapture alongside the preview, tied to the preview’s field of view so the guide box crops the same rectangle out of both, and only that rectangle is decoded so a 12MP frame never becomes a 48 MB bitmap.Three things follow, and all three are worth re-testing before you ship 1.1.0:
  • Your app binds Preview + ImageCapture where it bound only Preview. This is well inside the three use cases every CameraX device guarantees.
  • A front capture goes from about 918x578 to about 1586x999. If anything downstream of you was sized around the old output, it will see something different.
  • The shutter is no longer instant and shows a progress indicator while the still is taken. UI tests that tapped the shutter and immediately asserted on the review screen need to wait.
A gallery pick is also rotated to match its EXIF orientation from 1.1.0. Before that it was uploaded as decoded, so a normal phone photo reached the backend sideways. Before the review screen the SDK checks the front photo contains a face. A failure is a warning, not a block: the user can still confirm, because a false negative would otherwise trap someone holding a genuine but unusual ID. The backend is the check that actually rejects, and from 1.2.0 it does so before the selfie: see Document check. From 1.4.0 the back of a Kenyan ID and a passport photo page are also checked on the device: see Checking the photo on the phone. From 1.1.0 that check runs ML Kit’s accurate model. The fast one it used before does not find the portrait on a laser-engraved card, so genuine IDs of that kind, Kenya’s Maisha Namba among them, were told “No face detected”.

Resuming a failed verification

From 1.3.0. When a verification fails, your team can click Resubmit on it in the Adhere Dashboard. That issues a single-use resume token, valid for 24 hours. Launch the SDK with it and the customer redoes only the step that failed, on the same verification, instead of starting again.
1

Receive the token on your backend

It arrives in the liveness.resubmit_requested webhook. Without a webhook URL on your SDK Config, it is shown in the Resubmit dialog for your team to copy.
2

Get it to the customer's app

Getting it onto the phone is up to you; see the warning below.
3

Launch with it

The result comes back exactly as for any other run: TYPE_SUCCESS with RESULT_STATUS, and a liveness.completed webhook with the same verification_id as the original. A token that is expired, already used, or replaced by a newer Resubmit shows a “Link No Longer Valid” screen. Its Close returns TYPE_FAILURE with RESULT_ERROR_CODE set to SmartComplyActivity.ERROR_RESUME_LINK_INVALID. Issue a new token rather than launching without one, which would start an unrelated verification. The attempt limit carries over. Three failed attempts per verification, counted across the original session and the resume. A verification that has used them cannot be resubmitted; it goes to manual review instead. A customer who reaches the limit during the resume sees the same We couldn’t finish your verification screen as in Document check.
The token is a credential. Whoever holds it can continue that customer’s verification until it is used or expires. The best route is an in-app fetch: after the customer signs in, your app asks your own backend whether a resubmit is pending, then launches the SDK. A push notification can prompt this without carrying the token. Android App Links are fine too, since only your verified app receives them. Avoid custom URL schemes such as myapp://resume?token=...: any app can register the same scheme and receive the link.
On the embedded flow, pass resumeToken to SmartComplyFlowScreen and handle onResumeLinkInvalid, which falls back to onError when you leave it out. Do not call createSession() yourself in that case: a new session replaces the token and the resume is silently lost.

Branding

The flow carries your brand, not Adhere’s. Colour and logo come from the SDK Config on the dashboard, so they change without an app release, and your name is shown while the session is being set up. The font is the one exception and is passed by the host. The welcome heading and subheading are fixed SDK copy: “Verify Your Identity”, then one of two lines keyed off the verification mode. Upload it under Settings → Integrations → SDK → Brand Details. PNG, JPEG or WebP, up to 512 KB and 1024x1024 pixels. Requires 1.0.7. It is drawn on its own, with no box or border behind it, and bounded by height rather than fitted into a square so a wordmark stays legible. Only the SDK’s fallback shield sits in a tinted frame. The logo belongs to the SDK Config, not the company. A branch running several configs for different products gives each its own mark, and a config with no logo shows the SDK’s own shield rather than falling back to the company logo on your profile page. If you want the same mark everywhere, upload it to each config.
A logo never delays or blocks a verification. The SDK fetches it after the welcome screen is already on screen, on a three second budget, and keeps its own shield if the fetch fails. It is also decoded with a size cap, so an oversized image cannot exhaust memory on the handset.
The logo is rendered by the Android and iOS SDKs. The Web SDK does not show it yet, so a config used by both will carry your mark on mobile and the default on web.

Font

Passed by your app, because the SDK neither ships nor downloads font files: your loading, caching and licensing stay yours. Requires 1.0.7. Through buildIntent, with a font resource:
Or, if you host SmartComplyFlowScreen yourself, with a FontFamily directly, which is the only way to control each weight:
Point fontResId at an XML font family rather than a single font file and the platform resolves real faces per weight on API 28+. A single file is used for every weight, which flattens the SDK’s type hierarchy.
A font resource that cannot be loaded, including one R8 has stripped, falls back to the platform font. It does not fail the verification.

Liveness

Two actions are drawn from BLINK and TURN_HEAD in random order and detected passively. Both are watched on every frame, neither is named to the user, and the HUD only reports how many are done: there is no prompted sequence. The scan has a 20 second cap.

When the autoshot is taken

Two stills leave the device. The autoshot is taken before the scan starts and the snapshot once both actions are done. Both come off the analysis stream, so neither is a composed photograph: 720px on the long edge at JPEG quality 72. Up to 1.2.0 some phones uploaded both as a green, striped image; see the 1.2.1 note under Add the dependency. From 1.2.0, in document mode the autoshot goes up with the video, because the entry already exists from the document check. In data mode it goes up with liveness.create as before. From 1.2.0 nothing starts until the whole face is inside the oval. After a 400ms settle the SDK holds the autoshot until three consecutive frames show the face inside the oval the user is looking at, and there is no timeout. Up to 1.1.2 it gave up after 2.5 seconds and took whatever frame it had, which is how a selfie of nobody could still be uploaded, and up to 1.1.1 it fired on a fixed timer without looking for a face at all.
  • The prompt says what is wrong. “Position your face in the oval” when there is no face, “Move back a little” when the face is centred but too big to fit, and “Fit your whole face inside the oval” when it is off to one side or too low. It stays up until the face is in.
  • During the scan an action only counts while the face is in the oval. If the face is out for more than about half a second the prompt comes back and nothing is credited until it returns. The grace keeps a head turn, which swings the face across the edge, from flashing the prompt.
  • The 20 second cap starts after the autoshot, so a user who takes a while to get into position still has the full window.
  • How the oval is judged. ML Kit’s face box is mapped onto the preview as it is displayed, and the face is taken as 80% of the box because the box runs wider than the face: on a TECNO KI7, a face comfortably inside the oval measured a box about 1.1 times its width. The face must also be wholly inside the camera frame with an 8% margin, at least 18% of the frame width, and within 30 degrees of facing the camera.
Upgrade past 1.1.0 for the liveness stills. Up to and including 1.1.0 the autoshot and snapshot uploaded in the camera’s sensor orientation, rotated 90 degrees from upright, because the rotation arrives as metadata and was not applied. On-device detection was never affected, so the challenges always worked, but the backend was handed a sideways face: a passing verification could report Face Detected: No with no face confidence. Fixed in 1.1.1, where the rotation is baked into the pixels. If you read the autoshot or snapshot yourself, from 1.1.1 they are upright, 480x640 rather than 640x480 on a portrait capture, and carry no EXIF orientation tag. The liveness .mp4 was never affected.
Reaching the cap fails the check. Nothing is uploaded and nothing is billed, and the user is offered Try Again. Earlier versions submitted anyway and reported success with no actions completed: if you are on 1.0.6 or below, expect that behaviour.

What the success screen means

Nothing about the verdict. Every submitted entry reaches it, whether the backend passed or failed the check. The submit call returns processing, because the backend resolves face matching on a worker moments later. The SDK polls for up to about 15 seconds and puts whatever it learns in RESULT_STATUS, then shows the success screen either way. A user who failed is not told so and is not offered a retry. This is a product decision, not an oversight: tenants asked for the end user to see a submitted verification as accepted, and to act on the real verdict themselves from the dashboard and the webhook. It means you have to close the loop with a user who failed. Pre-submission failures are unaffected and still show their own screens: a denied camera, a configuration or network error, reaching the 20 second liveness cap, and from 1.2.0 a rejected ID. Nothing has been billed at that point and the user can recover on the spot.

SDK Configuration


Error Handling

SmartComplyActivity handles and displays all error states inside the flow: an invalid key, an expired session, or an exhausted upload retry all render the SDK’s own failure screen with a Try Again button. From 1.0.7 the Activity also returns immediately, before any screen renders, when apiKey or clientId is blank. Otherwise it returns a result only when the user completes the flow or backs out.

Advanced: Custom Host Activity

If you need to embed the verification flow directly inside your own ComponentActivity instead of launching a separate screen, you can use SmartComplyFlowScreen as a Compose composable:
Your host Activity must be a ComponentActivity and must be in the foreground with an active window. Embedding SmartComplyFlowScreen inside a Dialog or bottom sheet will cause the camera to fail on some devices.
You do not need to request CAMERA yourself. From 1.0.2 the composable routes through its own permission screen in both verification modes, before the ID-input or document-capture step, and skips it silently once the permission is granted.On 1.0.1 and earlier data mode never asked, so if the permission had not already been granted the camera never opened: the liveness preview stayed black and the flow ended with Recording failed: Recording finalized with error code 4. If you are pinned to an older version, request CAMERA yourself before entering the flow, or launch through SmartComplyActivity, which always asked for both modes.

Dependencies

From 1.0.3 the SDK no longer depends on Ktor. It uses OkHttp directly, so your own Ktor version cannot conflict with ours. From 1.4.0 it also depends on com.google.android.gms:play-services-mlkit-text-recognition:19.0.1, next to the face detection library it already used. Both come through Google Play Services, so they add a few hundred KB to your app and the models are fetched by Play Services, not bundled.
On 1.0.2 and earlier, an app that uses Ktor 3 crashes on launch:
Those versions were compiled against Ktor 2.3.12 and exported it, so Gradle resolved the conflict to your newer Ktor and linked our code against a version where the classes we call had been removed. There is no workaround on your side. Upgrade to 1.0.5.

ProGuard / R8

There is nothing to do. The SDK ships consumer ProGuard rules inside the AAR, so R8 applies them to your app automatically. They keep the generated kotlinx.serialization serializers and SmartComplyActivity; OkHttp and ML Kit ship their own rules. 1.4.0 was built into a minified release app with no new rules needed.
On 1.0.0, a minified build crashes on launch with a NoClassDefFoundError on a Ktor class. That version shipped no consumer rules. Upgrade to 1.0.5.