Skip to content

Quickstart

This page builds a complete verification for the Turkish ID card (front + back) and liveness in Kotlin. It assumes you have finished Installation. Each step links to its detailed page, where the Java version and all options are.

An integration has three parts:

  1. Your server signs in to Prove ID, starts the transaction for the signed-in user and gives the app a token.
  2. Your app runs the camera steps and sends images and videos straight to Prove ID.
  3. Your server reads the result with the transaction ID it started and makes the decision.

Your server needs two endpoints that only your signed-in users can call:

Your endpoint What it does
POST /api/kyc/mobile-session Gets a Prove ID access token, starts the transaction (POST /id/start), remembers which user it belongs to and returns accessToken, transactionId and gestureType to the app.
POST /api/kyc/mobile-session/complete Accepts the transactionId only if it was started for this user, reads the result (GET /id/id-info-with-tid/{transactionId}) and decides.
// Node.js 18+ with Express. requireSignedInUser and db are your own.
const API = process.env.PROVEID_API ?? 'https://test.api.proveid.io';
let cached; // { token, expiresAt }
async function accessToken() {
// Hand out tokens that stay valid for at least 10 more minutes.
if (cached && cached.expiresAt > Date.now() + 10 * 60_000) return cached.token;
const res = await fetch(`${API}/oauth/access_token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.PROVEID_CLIENT_ID,
client_secret: process.env.PROVEID_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Prove ID sign-in failed: ${res.status}`);
const data = await res.json();
cached = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
return cached.token;
}
app.post('/api/kyc/mobile-session', requireSignedInUser, async (req, res) => {
const token = await accessToken();
const start = await fetch(`${API}/id/start`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
optionalData: { idType: 'IDCARD', controls: ['FRONT', 'BACK', 'LIVENESS'] },
}),
});
if (!start.ok) return res.status(502).end();
const { transactionId, gestureType } = await start.json();
await db.kycSessions.insert({ userId: req.user.id, transactionId, status: 'started' });
res.json({ accessToken: token, transactionId, gestureType });
});
app.post('/api/kyc/mobile-session/complete', requireSignedInUser, async (req, res) => {
const session = await db.kycSessions.findOne({
userId: req.user.id, transactionId: String(req.body.transactionId), status: 'started',
});
if (!session) return res.status(404).end(); // not this user's transaction
const info = await fetch(
`${API}/id/id-info-with-tid/${encodeURIComponent(session.transactionId)}`,
{ headers: { Authorization: `Bearer ${await accessToken()}` } },
).then((r) => { if (!r.ok) throw new Error(`Result failed: ${r.status}`); return r.json(); });
const controls = info.controlResults ?? [];
const passed = controls.length > 0 && controls.every((c) => c.entryInterpretation === true);
await db.kycSessions.update(session.id, { status: passed ? 'approved' : 'rejected' });
res.json({ passed });
});

The rule above accepts the verification only when every check passed; adapt it to your own risk policy (see Results).

Point the SDK to the same environment when the app starts:

App.kt
class App : Application() {
override fun onCreate() {
super.onCreate()
EndpointManager.setServerUrl("https://test.api.proveid.io")
Authentication.setSharedPreferences(applicationContext)
}
}

ServerCall reports through callbacks. This helper turns them into suspend functions so the flow reads top to bottom:

ProveIdCall.kt
import com.techsign.rkyc.services.TechsignServiceListener
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun <T> proveId(call: (TechsignServiceListener<T>) -> Unit): T =
suspendCancellableCoroutine { cont ->
call(object : TechsignServiceListener<T> {
override fun onSuccess(result: T) { if (cont.isActive) cont.resume(result) }
override fun onFailure(e: Exception) { if (cont.isActive) cont.resumeWithException(e) }
})
}
fun Bitmap.toBase64(): String {
val out = ByteArrayOutputStream()
compress(Bitmap.CompressFormat.PNG, 100, out)
return Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
}

One Activity hosts the steps in a FrameLayout with the ID container. backend is your own API client for the two endpoints in step 1. Ask for the camera permission before you open it (see Installation).

VerificationActivity.kt
data class KycSession(val accessToken: String, val transactionId: String, val gestureType: String?)
class VerificationActivity : AppCompatActivity() {
private lateinit var session: KycSession
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_verification)
lifecycleScope.launch {
session = backend.startKycSession() // POST /api/kyc/mobile-session
Authentication.setApiKey(session.accessToken) // memory only
showDocumentCamera(CardType.NEW_ID)
}
}
// --- Document front and back -------------------------------------------------------
private fun showDocumentCamera(side: CardType) {
val camera = IDCardDetectionBaseFragment.create(applicationContext)
camera.setConfiguration(IDCardDetectionConfiguration().apply {
cardTypeList = listOf(side)
messageToEncrypt = session.transactionId
isEncryptDeviceId = true
detectBothOrientation = true
})
camera.setListener(object : OcrListener {
override fun cardDetected(captured: Bitmap, cardType: CardType) {
camera.stopCapture()
lifecycleScope.launch { onDocumentCaptured(captured, side) }
}
override fun cardDetectionFailed(errors: CardDetectionErrorsModel) {
runOnUiThread { showCaptureHint(errors) } // see Errors
}
override fun cardOcrCompleted(result: OcrResultModel) = Unit
})
supportFragmentManager.beginTransaction().replace(R.id.container, camera).commitNow()
camera.startCapture()
}
private suspend fun onDocumentCaptured(image: Bitmap, side: CardType) {
val base64 = withContext(Dispatchers.Default) { image.toBase64() }
try {
if (side == CardType.NEW_ID) {
proveId<CheckIdReturnModel> {
ServerCall.verifyIDPhoto(CheckIDModel(base64, session.transactionId), it)
}
showDocumentCamera(CardType.NEW_ID_BACK)
} else {
proveId<AddSecondaryReturnModel> {
ServerCall.addSecondary(AddSecondaryModel(base64, session.transactionId), it)
}
showLiveness()
}
} catch (e: Exception) {
showError(e) // UnauthorizedException → get a new token from your server
}
}
// --- Liveness -----------------------------------------------------------------------
private fun showLiveness() {
LivenessBaseFragment.setLayerShape(LivenessBaseFragment.Shape.OVAL)
LivenessBaseFragment.setControlFaceCapture(true)
LivenessBaseFragment.MESSAGE_TO_ENCRYPT = session.transactionId
LivenessBaseFragment.LIVENESS_FROM_IMAGE_ENABLED = false
LivenessBaseFragment.RANDOMIZED_GESTURES = emptyList()
LivenessBaseFragment.GESTURE_TYPE = session.gestureType.toGestureType()
LivenessBaseFragment.IS_GESTURE_CONTROL_ENABLED = true
LivenessBaseFragment.VIDEO_PREPARATION_TIME_IN_MS = 3000
LivenessBaseFragment.VIDEO_RECORD_TIME_IN_MS = 5000
val liveness = LivenessBaseFragment.create(applicationContext)
liveness.setGestureListener(object : GestureListener {
override fun gestureControlled(model: GestureModel) {
liveness.stopFaceCapture()
lifecycleScope.launch { onLivenessVideo(model.videoPath, liveness.rotate) }
}
override fun positionSucceed(model: PositionSucceedModel) = Unit
override fun positionFailed(model: PositionFailedModel) = Unit
override fun positionCorrect() = Unit
override fun preparationPositionControlled(model: PreparationPositionModel) = Unit
})
liveness.setFragmentListener(object : FragmentListener {
override fun onVideoCaptured(videoPath: String) = Unit // passive liveness only
override fun onVideoStarting() = Unit
override fun onVideoStarted() = Unit
override fun onVideoCanceled() = runOnUiThread { showMessage("Keep your face in the frame") }
override fun onVideoProcessing() = Unit
override fun onFaceDetectionFailure(isAway: Boolean) = Unit
override fun onFaceDetectionSuccess() = Unit
override fun onFaceDetectionIdle() = Unit
override fun onMultiFaceDetected() = runOnUiThread { showMessage("Only one person, please") }
})
supportFragmentManager.beginTransaction().replace(R.id.container, liveness).commitNow()
liveness.startFaceCapture()
}
private suspend fun onLivenessVideo(videoPath: String, rotate: Int) {
val file = File(videoPath)
try {
val base64 = withContext(Dispatchers.IO) {
Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)
}
proveId<VerifyFaceReturnModel> {
ServerCall.checkLiveness(FaceVerificationModel(base64, session.transactionId, rotate), it)
}
finishVerification()
} catch (e: Exception) {
showError(e)
} finally {
file.delete() // never leave the video on the device
}
}
// --- Result -------------------------------------------------------------------------
private suspend fun finishVerification() {
Authentication.setApiKey(null)
val passed = backend.completeKycSession(session.transactionId) // POST .../complete
showOutcome(passed)
}
override fun onDestroy() {
super.onDestroy()
if (isFinishing) Authentication.setApiKey(null)
}
}
private fun String?.toGestureType(): GestureType = when (this) {
"MOUTH" -> GestureType.MOUTH
"HEAD-VERTICAL" -> GestureType.HEAD_VERTICAL
"HEAD-HORIZONTAL" -> GestureType.HEAD_HORIZONTAL
else -> GestureType.EYE
}
  • Add hologram or NFC steps by adding HOLOGRAM or NFC to controls on your server.
  • Handle every error with a clear message to the user: Errors.
  • Go through the Go-live checklist.
  • Common questions are answered in the FAQ.