Skip to content

Usage Guide ​

Quick Start ​

1. Acquire an IsoDep ​

Acquire an IsoDep from Android. Refer to the NFC basics guide on how to configure your app and allow your Activity to receive the ACTION_TECH_DISCOVERED intent.

2. Connect to the DocVal Service ​

Create an EmrtdConnector instance and start the session:

kotlin
val clientId = "example_client"
val serverUrl = "wss://docval.kurzdigital.com/ws2/validate"

val emrtdConnector = EmrtdConnector(
    clientId, serverUrl, ::closedListener, ::statusListener, ::emrtdPassportListener
)

fun connect(isoDep: IsoDep) {
    // Unique transaction ID, usually from your server
    val validationId = UUID.randomUUID().toString()

    val options = ConnectionOptions.Builder()
        .setValidationId(validationId)
        .setChipAccessKeyFromMrz(
            "123456789",  // Document number
            "970101",     // Date of birth (yyMMdd)
            "221212"      // Date of expiry (yyMMdd)
        )
        .build()

    emrtdConnector.connect(isoDep, options)
}

fun closedListener(code: Int, reason: String, remote: Boolean) {
    if (code != 1000) {
        println("Session closed with error. Reason: $reason")
    } else {
        println("Session closed successfully.")
    }
}

fun statusListener(status: String) {
    println("Status: $status")
}

fun emrtdPassportListener(emrtdPassport: EmrtdPassport?, exception: JSONException?) {
    if (emrtdPassport != null) {
        println("Result received: $emrtdPassport")
    }
}

Reading Selected Data Groups ​

By default, a validation reads DG1 (MRZ), DG2 (face image), and the optional data groups the document carries. If you only need some of them, pass a DataGroupSet. Reading less is faster, because DG2 alone accounts for most of the chip reading time:

kotlin
val options = ConnectionOptions.Builder()
    .setValidationId(validationId)
    .setChipAccessKeyFromCan("123456")
    .setFilesToRead(DataGroupSet.MINIMAL_KYC) 
    .build()

Besides the predefined selections you can combine data groups freely, e.g. DataGroupSet.of(1, 11) or DataGroupSet.STANDARD.union(DataGroupSet.of(13)).

SelectionData GroupsUse Case
DataGroupSet.MINIMALDG1MRZ data alone, no biometrics
DataGroupSet.MINIMAL_KYCDG1, DG2MRZ plus face image
DataGroupSet.STANDARDDG1, DG2, DG7, DG11, DG12Same as a validation without a selection
DataGroupSet.ALLDG1 to DG16All sixteen data groups

How a selection behaves:

  • The SOD is always read, and the DocVal Service verifies the data groups it receives. A reduced selection shows up as fewer entries in data_groups_checked of the validation result.
  • Selected data groups the document does not contain are skipped, and an optional data group that fails to read is left out instead of ending the session. DG3 and DG4 are the common case: they need Extended Access Control, which this SDK does not implement, so DataGroupSet.ALL reads what it can and skips those two.
  • DG1 and DG2 must read successfully while they are part of the selection, otherwise the validation fails.
  • The selection never affects the authenticity checks: DG14 (Chip Authentication) and DG15 (Active Authentication) are read whenever the document carries them.

See Relevant Data Groups for what each data group contains.

Using With CAN ​

For documents that support PACE with a Card Access Number (a 6-digit number printed on the document):

kotlin
val options = ConnectionOptions.Builder()
    .setValidationId(UUID.randomUUID().toString())
    .setChipAccessKeyFromCan("123456") 
    .build()

emrtdConnector.connect(isoDep, options)

INFO

CAN authentication only works with documents that support PACE (Password Authenticated Connection Establishment). It cannot be used with BAC (Basic Access Control).

EmrtdConnectorActivity ​

The SDK includes a pre-built Activity that handles the complete eMRTD reading flow with a UI, including NFC tag discovery, progress display, and status messages. This is the simplest way to integrate the SDK.

kotlin
val intent = Intent(context, EmrtdConnectorActivity::class.java).apply {
    putExtra(EmrtdConnectorActivity.CLIENT_ID, "example_client")
    putExtra(EmrtdConnectorActivity.VALIDATION_URI, "wss://docval.kurzdigital.com/ws2/validate")
    putExtra(EmrtdConnectorActivity.VALIDATION_ID, UUID.randomUUID().toString())
    // Use CAN:
    putExtra(EmrtdConnectorActivity.CAN, "123456")
    // Or use MRZ fields:
    putExtra(EmrtdConnectorActivity.DOCUMENT_NUMBER, "123456789")
    putExtra(EmrtdConnectorActivity.DATE_OF_BIRTH, "970101")
    putExtra(EmrtdConnectorActivity.DATE_OF_EXPIRY, "221212")
}
startActivityForResult(intent, REQUEST_CODE)

The activity is configured through intent extras alone, so it takes a data group selection as the numbers of the data groups to read:

kotlin
// MRZ and face image only
putExtra(EmrtdConnectorActivity.DATA_GROUPS, intArrayOf(1, 2)) 

Without the extra it reads DataGroupSet.STANDARD. See Reading Selected Data Groups for what a selection does and does not influence. A number outside 1 to 16 ends the activity with RETURN_ERROR.

Handle the result in onActivityResult:

kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode != REQUEST_CODE) {
        return
    }
    if (resultCode == RESULT_OK) {
        val passport = data?.getParcelableExtra<EmrtdPassport>(EmrtdConnectorActivity.RETURN_DATA)
        // Handle result
    } else {
        val error = data?.getStringExtra(EmrtdConnectorActivity.RETURN_ERROR)
        // Handle error
    }
}

Custom HTTP Headers ​

If your server requires custom headers (e.g., for authentication), you can provide them via ConnectionOptions. These headers are sent in the WebSocket handshake and forwarded by the DocVal Service to your result server:

kotlin
val options = ConnectionOptions.Builder()
    .setValidationId(UUID.randomUUID().toString())
    .setChipAccessKeyFromMrz(documentNumber, dateOfBirth, dateOfExpiry)
    .setHttpHeaders(mapOf( 
        "Authorization" to "Bearer your-token", 
        "X-Custom-Header" to "value"
    )) 
    .build()

Log Context ​

The DocVal Service can add your own fields to every log line of a session, from the WebSocket handshake to the close of the connection. You can use this to correlate the logs of the DocVal Service with your own logs:

kotlin
val options = ConnectionOptions.Builder()
    .setValidationId(UUID.randomUUID().toString())
    .setChipAccessKeyFromMrz(documentNumber, dateOfBirth, dateOfExpiry)
    .setBaggage(mapOf( 
        "example_id" to "1a2b3c4d5e6f", 
        "example_key" to "12345"
    )) 
    .build()

The SDK sends the entries as a W3C Baggage header with the WebSocket handshake and encodes the values for you.

The following rules apply:

  • A key must be a token of RFC 7230. This allows letters, digits and the characters !#$%&'*+-.^_`|~.
  • A value must not be empty and must have 128 characters or less.
  • All entries together must fit into 8192 characters after the encoding, which is the limit of the W3C Baggage specification.
  • An entry that does not follow these rules is dropped. The other entries are still sent. Each dropped entry gives a warning in the Android log.
  • The entries replace a baggage header from setHttpHeaders.

The operator of the DocVal Service has to put the keys on the allow-list of the server. The server ignores all other keys. See Log Context from the Baggage Header for the server side.

Diagnostics ​

Enable diagnostic data collection on the DocVal Service to help troubleshoot eMRTD reading problems:

kotlin
val options = ConnectionOptions.Builder()
    .setValidationId(UUID.randomUUID().toString())
    .setChipAccessKeyFromMrz(documentNumber, dateOfBirth, dateOfExpiry)
    .setEnableDiagnostics(true) 
    .build()

WARNING

When diagnostics are enabled, the DocVal Service collects additional debugging information that may contain personal data. Make sure to obtain user consent if required.

See Diagnostic Session for more information.

OpenTelemetry ​

The SDK supports OpenTelemetry for tracing the protocol communication. Configure a tracer provider before starting a session:

kotlin
// Set a global tracer provider
EmrtdConnector.setTracerProvider(yourTracerProvider)

// Access the current tracer (e.g., for custom spans)
val tracer = EmrtdConnector.getTracer()

If no tracer provider is configured, the SDK uses a no-op implementation (no tracing overhead).

Error Handling ​

The SDK reports errors through the ClosedListener callback. Check the close code and reason to determine what happened:

kotlin
fun closedListener(code: Int, reason: String, remote: Boolean) {
    when {
        code == 1000 -> println("Session completed successfully")
        reason == ClosedListener.NFC_CHIP_COMMUNICATION_FAILED ->
            println("NFC connection lost - hold passport steady")
        reason == ClosedListener.ACCESS_CONTROL_FAILED ->
            println("Wrong MRZ/CAN values")
        reason == ClosedListener.COMMUNICATION_FAILED ->
            println("Network error: ${emrtdConnector.webSocketClientException}")
        else -> println("Error: $reason (code $code)")
    }
}

Close Reasons ​

When the session closes, the ClosedListener receives a close code, a reason string, and whether the close was initiated by the server. A code of 1000 means success.

ReasonDescription
CANCELLED_BY_USERUser called cancel()
NFC_CHIP_COMMUNICATION_FAILEDNFC connection lost (passport moved away)
INVALID_CLIENT_IDClient ID is incorrect
INVALID_ACCESS_KEY_VALUESCAN, document number, or date formats are wrong
ACCESS_CONTROL_FAILEDAccess control failed (verify access key values)
COMMUNICATION_FAILEDWebSocket connection failed (check network)
FILE_READ_ERRORServer could not read a file from the chip
EMRTD_PASSPORT_READER_ERRORError in the eMRTD reader
POST_TO_RESULT_SERVER_FAILEDFailed to post result to your server
SERVER_ERRORUnexpected server error
TIMEOUT_WHILE_WAITING_FOR_START_MESSAGEServer timeout waiting for start
TIMEOUT_WHILE_WAITING_FOR_RESPONSEServer timeout waiting for APDU response
MAX_SESSION_TIME_EXCEEDEDSession exceeded maximum time
PROTOCOL_ERRORProtocol violation (implementation bug)

Exception Details ​

For certain close reasons, you can get more details from the EmrtdConnector instance:

kotlin
fun closedListener(code: Int, reason: String, remote: Boolean) {
    when (reason) {
        ClosedListener.COMMUNICATION_FAILED -> {
            val exception = emrtdConnector.webSocketClientException
            println("WebSocket error: $exception")
        }
        ClosedListener.NFC_CHIP_COMMUNICATION_FAILED -> {
            val exception = emrtdConnector.nfcException
            println("NFC error: $exception")
        }
    }
}

Status Updates ​

The StatusListener is called during the session to report progress. The following status values are defined:

StatusDescription
CONNECTING_TO_SERVERConnecting to the DocVal Service
ACCESS_CONTROLPerforming Access Control (PACE or BAC)
READ_ATR_INFOReading ATR/Info
READ_SODReading the Security Object (SOD)
READ_DG14Reading Data Group 14 (Chip Authentication info)
CHIP_AUTHENTICATIONPerforming Chip Authentication
READ_DG15Reading Data Group 15 (Active Authentication key)
ACTIVE_AUTHENTICATIONPerforming Active Authentication
READ_DG1Reading Data Group 1 (MRZ info)
READ_DG2Reading Data Group 2 (face photo)
READ_DG7Reading Data Group 7 (signature photos)
READ_DG11Reading Data Group 11 (additional personal details)
READ_DG12Reading Data Group 12 (additional document details)
PASSIVE_AUTHENTICATIONPerforming Passive Authentication
DONESession finished

Session Management ​

Canceling a Session ​

Cancel a running session at any time:

kotlin
emrtdConnector.cancel()
// ClosedListener will be called with reason CANCELLED_BY_USER

Checking Session State ​

kotlin
if (emrtdConnector.isOpen) {
    // Session is currently active
}