Appearance
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)).
| Selection | Data Groups | Use Case |
|---|---|---|
DataGroupSet.MINIMAL | DG1 | MRZ data alone, no biometrics |
DataGroupSet.MINIMAL_KYC | DG1, DG2 | MRZ plus face image |
DataGroupSet.STANDARD | DG1, DG2, DG7, DG11, DG12 | Same as a validation without a selection |
DataGroupSet.ALL | DG1 to DG16 | All 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_checkedof 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.ALLreads 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
baggageheader fromsetHttpHeaders.
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.
| Reason | Description |
|---|---|
CANCELLED_BY_USER | User called cancel() |
NFC_CHIP_COMMUNICATION_FAILED | NFC connection lost (passport moved away) |
INVALID_CLIENT_ID | Client ID is incorrect |
INVALID_ACCESS_KEY_VALUES | CAN, document number, or date formats are wrong |
ACCESS_CONTROL_FAILED | Access control failed (verify access key values) |
COMMUNICATION_FAILED | WebSocket connection failed (check network) |
FILE_READ_ERROR | Server could not read a file from the chip |
EMRTD_PASSPORT_READER_ERROR | Error in the eMRTD reader |
POST_TO_RESULT_SERVER_FAILED | Failed to post result to your server |
SERVER_ERROR | Unexpected server error |
TIMEOUT_WHILE_WAITING_FOR_START_MESSAGE | Server timeout waiting for start |
TIMEOUT_WHILE_WAITING_FOR_RESPONSE | Server timeout waiting for APDU response |
MAX_SESSION_TIME_EXCEEDED | Session exceeded maximum time |
PROTOCOL_ERROR | Protocol 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:
| Status | Description |
|---|---|
CONNECTING_TO_SERVER | Connecting to the DocVal Service |
ACCESS_CONTROL | Performing Access Control (PACE or BAC) |
READ_ATR_INFO | Reading ATR/Info |
READ_SOD | Reading the Security Object (SOD) |
READ_DG14 | Reading Data Group 14 (Chip Authentication info) |
CHIP_AUTHENTICATION | Performing Chip Authentication |
READ_DG15 | Reading Data Group 15 (Active Authentication key) |
ACTIVE_AUTHENTICATION | Performing Active Authentication |
READ_DG1 | Reading Data Group 1 (MRZ info) |
READ_DG2 | Reading Data Group 2 (face photo) |
READ_DG7 | Reading Data Group 7 (signature photos) |
READ_DG11 | Reading Data Group 11 (additional personal details) |
READ_DG12 | Reading Data Group 12 (additional document details) |
PASSIVE_AUTHENTICATION | Performing Passive Authentication |
DONE | Session finished |
Session Management
Canceling a Session
Cancel a running session at any time:
kotlin
emrtdConnector.cancel()
// ClosedListener will be called with reason CANCELLED_BY_USERChecking Session State
kotlin
if (emrtdConnector.isOpen) {
// Session is currently active
}