Skip to main content
Version: v0.10

Engagement and Retrieval

Overview​

The Identity Development Kit implements ISO/IEC 18013-5 device engagement and data retrieval protocols for mobile credentials (mDL and other mdoc documents). The SDK provides two primary integration approaches:

  1. Session UI Projection Layer (Recommended) - Simplified, opinionated state management for quick UI integration
  2. Direct EventHub Access - Full control over raw events for custom state management

Both approaches work with the same underlying MdocEngagementManager, which orchestrates the complete mdoc interaction lifecycle across both ISO 18013-5 phases:

  • Phase 1: Engagement - Establishing connection (QR, NFC, TO_APP)
  • Phase 2: Retrieval/Transfer - Exchanging device requests and responses (BLE, NFC, HTTP/WebSocket)

Engagement Manager​

The Engagement Manager is the primary entry point for creating and managing mdoc engagement sessions. Access it through the presentation service:

val engagementManager = presentationService.engagementManager

Key Features​

Typed Engagement Access: Direct property access without UUID tracking

val qrEngagement = manager.qrEngagement.value
val nfcEngagement = manager.nfcEngagement.value
val toAppEngagement = manager.toAppEngagement.value
val activeEngagement = manager.activeEngagement.value

EventHub Architecture: Centralized event management with filtering and projection

// Access all events
manager.eventHub.allEvents.collect { event -> /* ... */ }

// Or filter by type
manager.eventHub.engagementEvents.collect { event -> /* ... */ }
manager.eventHub.transferEvents.collect { event -> /* ... */ }

Session UI Projection: Single state flow for simple UI integration (see Session UI Projection below)

manager.eventHub.sessionState.collect { state ->
when (state.phase) {
UiPhase.ENGAGEMENT -> handleEngagement(state)
UiPhase.TRANSFER -> handleTransfer(state)
UiPhase.TERMINAL -> handleTerminal(state)
}
}

SharedParameters: Automatic BLE UUID and ephemeral key management

val bleCentralUuid = manager.sharedParameters.bleCentralClientUuid.value
val blePeripheralUuid = manager.sharedParameters.blePeripheralServerUuid.value
manager.sharedParameters.regenerate() // Generate new UUIDs

Engagement Suspension: Automatic suspension/resumption when switching between engagements

// NFC engagement automatically suspends when QR becomes active
manager.nfcEngagement.value?.isActive?.collect { isActive ->
if (!isActive) showNfcInBackground()
}

Creating Engagement Instances​

Create a new EngagementInstance using the DSL configuration. The instance manages the engagement phase and transitions to the transfer phase:

val engagementInstance = engagementManager.createEngagement {
// Engagement phase: How reader discovers holder
engagement {
// Option 1: Device-initiated (Holder creates engagement)
device {
qr {
scheme = "mdoc://" // Default, can be customized
}
nfc { } // Enable NFC engagement
}

// Option 2: Reader-initiated (Reverse engagement via app link)
// Note: Cannot be mixed with device engagement
reader {
uri = "mdoc://example-base64-url" // From deep link
}
}

// Retrieval/Transfer phase: How data is transferred
retrieval {
ble {
centralClientMode = true // Act as BLE central (scan for peripherals)
peripheralServerMode = false
}
nfc { } // Enable NFC transfer
// oid4vp { } // OpenID4VP support
}
}
Engagement Methods

Device Engagement (QR, NFC): Holder creates engagement and displays/broadcasts it for reader to connect Reverse Engagement (TO_APP via URI): Reader creates engagement and shares via app link; holder connects

These cannot be mixed in a single engagement configuration.

Starting the Engagement​

Get QR code data before calling start() if needed:

val qrCodeData = engagementInstance.getEngagementUri()
displayQrCode(qrCodeData) // Show to user

// Start the engagement and get transfer manager
val transferManager = engagementInstance.start()
Important

Call start() ideally before or immediately after showing the QR code. The actual hardware calls (BLE scanning, NFC listening) begin when start() is called.

Session UI Projection​

The Session UI Projection layer provides a simplified, single state flow for quick UI integration. Perfect for standard wallet applications with typical UI patterns.

Quick Example​

// Collect single state flow
manager.eventHub.sessionState.collect { state ->
when (state.phase) {
UiPhase.ENGAGEMENT -> {
// Handle QR display, NFC prompts based on state.qrMode and state.nfcMode
if (state.qrMode == QrMode.DISPLAY) showQrCode()
if (state.nfcMode == NfcMode.FOREGROUND) showNfcPrompt()
}
UiPhase.TRANSFER -> {
// Handle user consent, progress
if (state.userInteractionRequired) showConsentDialog(state.deviceRequest!!)
}
UiPhase.TERMINAL -> {
// Show result based on state.terminalOutcome
when (state.terminalOutcome) {
TerminalOutcome.SUCCESS -> showSuccess()
TerminalOutcome.ERROR -> showError(state.terminalMessage)
else -> handleOtherOutcomes()
}
}
}
}
Complete UI Integration Guide

For comprehensive documentation including SessionUiState properties, NFC/QR mode management, state transitions, code examples, and best practices, see the Session UI Events Guide.

EngagementInstance Interface
/**
* Represents an instance of an engagement process with associated data, events, and operations.
*/
interface EngagementInstance : MdocEngagementEvent.Handlers {
/**
* Unique identifier for this engagement instance
*/
val id: Uuid

/**
* The engagement data containing connection details, ephemeral keys, and retrieval methods
*/
val data: MdocEngagementData

/**
* Whether this engagement is currently active (not suspended by another engagement)
*/
val isActive: StateFlow<Boolean>

/**
* Transfer instance associated with this engagement (available after start() is called)
*/
val transferInstance: TransferInstance?

/**
* Current state of the engagement
*/
fun getCurrentState(): MdocEngagementState

/**
* Starts the engagement process and initializes the transfer
*
* @return The transfer manager for handling device request/response exchange
*/
suspend fun start(): TransferManager

/**
* Retrieves the QR engagement data as a string (for generating QR code)
*
* @return QR code data encoded as mdoc:// URI
*/
suspend fun getEngagementUri(): String

/**
* Stream of engagement events for this instance
*/
val events: Flow<MdocEngagementEvent>
}

Direct EventHub Access​

For applications requiring fine-grained control, bypass the UI projection and access raw events directly:

class CustomStateManager(private val manager: MdocEngagementManager) {
private val eventHub = manager.eventHub

fun observeEvents() {
lifecycleScope.launch {
// Option 1: All events (engagement + transfer)
eventHub.allEvents.collect { event ->
when (event) {
is MdocEngagementEvent.QrShow -> handleQrShow(event)
is MdocEngagementEvent.Connected -> handleConnected(event)
is MdocRetrievalEvent.DocumentsSelectionProcessStart -> handleUserConsent(event)
is MdocRetrievalEvent.SessionDataSend -> handleTransferComplete(event)
}
}
}

lifecycleScope.launch {
// Option 2: Engagement events only
eventHub.engagementEvents.collect { event -> /* ... */ }
}

lifecycleScope.launch {
// Option 3: Transfer events only
eventHub.transferEvents.collect { event -> /* ... */ }
}
}

fun useBuiltInFilters() {
lifecycleScope.launch {
// Filter: Only final state events (order >= 200)
eventHub.finalStateEvents().collect { event ->
handleSessionCompletion(event)
}
}

lifecycleScope.launch {
// Filter: Only active transfer events (order 100-199)
eventHub.activeTransferEvents().collect { event ->
updateTransferProgress(event)
}
}

lifecycleScope.launch {
// Filter: Custom state order range
eventHub.eventsInStateRange(minOrder = 100, maxOrder = 150).collect { event ->
log("Mid-transfer event: ${event::class.simpleName}")
}
}
}
}

EventHub Features:

  • Filtered event streams by state order ranges
  • State tracking helpers (latestEventByEngagement(), getCurrentStateByEngagement())
  • Event history and debugging (getRecentEvents(), getEventTimeline())
  • Standard Flow operators for custom filtering

Engagement and Transfer Events​

Engagement Events​

MdocEngagementEvent Types
sealed interface MdocEngagementEvent {
val role: EngagementRole
val engagementId: Uuid
val state: MdocEngagementState
val time: LocalDateTimeKMP

data class Initializing(override val role: EngagementRole, override val engagementId: Uuid) : MdocEngagementEvent

data class QrShow(
override val role: EngagementRole,
val qrCodeData: String,
val engagement: DeviceEngagementCbor,
override val engagementId: Uuid
) : MdocEngagementEvent

data class NfcEngagement(
override val role: EngagementRole,
val engagement: DeviceEngagementCbor,
override val engagementId: Uuid
) : MdocEngagementEvent

data class Connecting(
override val role: EngagementRole,
val identifier: String,
override val engagementId: Uuid,
val dataRetrievalMethod: DataRetrievalTransmissionType
) : MdocEngagementEvent

data class Connected(
override val role: EngagementRole,
override val engagementId: Uuid,
val dataRetrievalMethod: DataRetrievalTransmissionType
) : MdocEngagementEvent

data class Canceled(
override val role: EngagementRole,
val reason: String? = null,
override val engagementId: Uuid
) : MdocEngagementEvent

data class Disconnected(
override val role: EngagementRole,
val reason: String,
override val engagementId: Uuid
) : MdocEngagementEvent

data class Error(
override val role: EngagementRole,
val reason: String,
val error: Throwable? = null,
override val engagementId: Uuid
) : MdocEngagementEvent

data class Data(
override val role: EngagementRole,
override val engagementId: Uuid,
val data: ByteArray,
val direction: Direction
) : MdocEngagementEvent {
enum class Direction { INCOMING, OUTGOING }
}
}

Engagement States​

MdocEngagementState Values
enum class MdocEngagementState(override val order: Int) : MdocEngagementStateEnum {
INIT(10), // Initial state
BLE_SCANNING(30), // BLE scanning in progress
BLE_ADVERTISING(30), // BLE advertising active
NFC_ENABLED(30), // NFC enabled and ready
CONNECTING(50), // Connection process starting
CONNECTED(70), // Connection established, data transfer can begin
CANCELED(80), // Engagement canceled
ERROR(90), // Error occurred
DISCONNECTED(100) // Connection terminated
}

States progress from lower to higher order values. Not every state is guaranteed to occur.

Transfer/Retrieval Events​

MdocRetrievalEvent Types
sealed interface MdocRetrievalEvent {
val data: ByteArray
val time: LocalDateTimeKMP
val state: MdocRetrievalStateType
val engagementId: Uuid
fun toCbor(): CborBaseItem

class Initializing : MdocRetrievalEvent

data class SessionEstablishmentReceived(override val data: ByteArray) : MdocRetrievalEvent
// Contains encrypted session establishment from reader
// Holder extracts DeviceRequest and prepares for document selection

data class DeviceRequestReady(override val data: ByteArray) : MdocRetrievalEvent
// DeviceRequest has been decrypted and is ready for processing

data class DocumentsSelectionProcessStart(override val data: ByteArray) : MdocRetrievalEvent
// User interaction required - show consent dialog

data class DocumentsSelectionProcessAccepted(override val data: ByteArray) : MdocRetrievalEvent
// User accepted - contains DeviceResponse

data class DocumentsSelectionProcessDeclined(override val data: ByteArray) : MdocRetrievalEvent
// User declined - will return error to reader

data class SessionDataSend(override val data: ByteArray) : MdocRetrievalEvent
// DeviceResponse is being sent to reader

data class SessionTerminationSend(override val data: ByteArray) : MdocRetrievalEvent
// Session termination message sent

data class SessionTerminationReceived(override val data: ByteArray) : MdocRetrievalEvent
// Session termination received from reader

data class Error(override val data: ByteArray, val error: Throwable) : MdocRetrievalEvent
}

Transfer States​

MdocRetrievalStateType Values
enum class MdocRetrievalStateType(override val order: Int) : IMdocRetrievalState {
INIT(10), // Initial state
SESSION_ESTABLISHMENT_RECEIVED(30), // Session establishment received from reader
DOCUMENTS_SELECTION_PROCESS_START(50), // User consent required
DOCUMENTS_SELECTION_PROCESS_ACCEPTED(60), // User accepted request
SESSION_DATA_SEND(70), // Sending DeviceResponse
DOCUMENTS_SELECTION_PROCESS_DECLINED(80), // User declined request
ERROR(90), // Error occurred
TERMINATED(100) // Session terminated successfully
}

Transfer Manager​

The TransferManager handles the device request/response exchange after engagement is established.

Typical Flow​

  1. Receive device request from reader
  2. Show consent dialog to user
  3. Create response with selected documents
  4. Send response back to reader
  5. Handle termination
// 1. Start transfer (automatically done by engagement manager)
val transferManager = engagementInstance.start()

// 2. Receive device request
val deviceRequest = transferManager.receiveDeviceRequest()

// 3. Show to user and get consent
showConsentDialog(deviceRequest) { accepted ->
if (accepted) {
lifecycleScope.launch {
// 4. Create response with document provider
val response = transferManager.createResponse(
deviceRequest = deviceRequest,
documentProvider = myDocumentProvider
)

// 5. Send response
transferManager.sendDeviceResponse(response)
}
} else {
// User declined
engagementManager.closeAll()
}
}
TransferManager Interface
interface TransferManager : MdocRetrievalEvent.Handlers, RequestResponseProcessor {
/**
* The engagement instance associated with this transfer
*/
val engagement: EngagementInstance

/**
* The transfer session managing transmission state
*/
val session: TransferSession

/**
* The data channel for raw data transmission
*/
val datachannel: MdocDataRetrievalChannelService

/**
* Receives device request from reader
*
* @return DeviceRequest object containing requested documents and elements
*/
suspend fun receiveDeviceRequest(): DeviceRequest

/**
* Sends device response back to reader
*
* @param deviceResponse Response containing documents or error status
*/
suspend fun sendDeviceResponse(deviceResponse: DeviceResponse)

/**
* Creates response based on device request and document provider
*
* @param deviceRequest The request from the reader
* @param documentProvider Provider that supplies requested documents
* @return Result containing DeviceResponse or error
*/
override fun createResponse(
deviceRequest: DeviceRequest,
documentProvider: DocumentProvider
): IdkResult<DeviceResponse, IdkError>
}

Document Selection and Response Creation​

When a device request is received, you need to select appropriate documents and create a response. The SDK provides functional interfaces for customization:

Default Implementation​

// Use default document selection
val deviceResponse = transferManager.createResponse(
deviceRequest = deviceRequest,
documentProvider = myDocumentProvider
)

The default implementation:

  • Automatically performs selective disclosure (only returns requested elements)
  • Returns the first matching document if multiple are available
  • Handles namespace filtering and element intentToRetain flags

Custom Document Selection​

Register custom selectors for fine-grained control:

presentationService.registerCustomResponseSelectors(
requestResponseProcessor = null, // Use default
requestDocumentsSelector = null, // Use default
docRequestSingleDocumentSelector = MyCustomSelector() // Custom single doc selector
)
Document Selection Interfaces
/**
* Selects a single document from available documents based on a request
*/
fun interface DocumentRequestSingleDocumentSelector {
fun select(
docRequest: DocRequestCbor,
availableDocuments: (customSelectorData: Any?) -> Set<DocumentCbor>
): IdkResult<DocumentCbor, IdkError>
}

/**
* Selects documents based on the complete device request
*/
fun interface RequestDocumentsSelector {
fun selectDocuments(deviceRequest: DeviceRequest): IdkResult<List<DocumentCbor>, IdkError>
}

/**
* Processes request and creates complete response
*/
fun interface RequestResponseProcessor {
fun createResponse(deviceRequest: DeviceRequest): IdkResult<DeviceResponse, IdkError>
}

Transport Methods​

BLE (Bluetooth Low Energy)​

Holder Configuration:

retrieval {
ble {
centralClientMode = true // Holder scans for reader's peripheral
peripheralServerMode = false
}
}

Reader Configuration:

retrieval {
ble {
centralClientMode = false
peripheralServerMode = true // Reader advertises peripheral
}
}

NFC (Near Field Communication)​

Holder (Android only):

engagement {
device {
nfc { } // Enables NFC engagement
}
}
retrieval {
nfc { } // Enables NFC data transfer
}
iOS NFC Limitation

NFC engagement is not available for iOS mdoc holders due to Apple's NFC restrictions. Only mdoc readers can use NFC on iOS.

QR Code with BLE Transfer​

Common Pattern:

engagement {
device {
qr { scheme = "mdoc://" }
}
}
retrieval {
ble {
centralClientMode = true // Holder acts as BLE central
}
}

Reader scans QR code (engagement), then connects via BLE (transfer).

Best Practices​

1. Use Session UI Projection for Standard Wallets​

For typical wallet applications with standard UI patterns, use the Session UI Projection layer:

manager.eventHub.sessionState.collect { state ->
// Single state object with all UI-relevant information
// Deterministic phase transitions
// Automatic QR hiding on transfer start
}

2. Favor NFC for Engagement (Android)​

NFC provides better security and user experience:

  • Tap-to-share familiar from payments
  • Prevents QR code hijacking/shoulder surfing
  • Faster connection establishment

3. Favor BLE for Transfer​

BLE is better for data transfer than NFC:

  • Higher throughput
  • No need to keep devices touching during transfer
  • Better for larger payloads

Display what will be shared before calling sendDeviceResponse():

val parsed = state.parseDeviceRequest()
val summary = parsed.documents.joinToString("\n\n") { doc ->
val attrs = doc.nameSpaces.flatMap { (ns, attributes) ->
attributes.map { "$ns/$it" }
}
"${doc.docType}:\n${attrs.joinToString("\n")}"
}

showConsentDialog(summary) { accepted ->
if (accepted) sendResponse()
else manager.closeAll()
}

5. Handle Engagement Suspension​

When using multiple engagement types (e.g., NFC + QR), handle suspension properly:

manager.eventHub.sessionState.collect { state ->
if (state.isSuspended) {
showSuspendedOverlay()
statusText.text = "Suspended - another session active"
}
}

6. Clean Up Resources​

Always close engagement manager when done:

override fun onDestroy() {
super.onDestroy()
engagementManager.close() // Closes all engagements and transfers
}

Platform-Specific Notes​

Android​

  • Min SDK: 27 (Android 8)
  • Target SDK: 35 (Android 15)
  • Permissions: BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, BLUETOOTH_SCAN, NFC
  • NFC HCE: Extend AbstractMdocNfcService for host card emulation

iOS​

  • NFC Limitation: NFC engagement not available for holders (only readers)
  • CoreBluetooth: Required for BLE operations
  • Async/Await: Kotlin coroutines bridge to Swift's async/await
  • Memory: Always call .close() on managers to prevent leaks

Additional Resources​

For questions or support, open an issue at: https://github.com/Sphereon-Opensource/identity-development-kit/issues