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:
- Session UI Projection Layer (Recommended) - Simplified, opinionated state management for quick UI integration
- 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
}
}
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()
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()
}
}
}
}
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
- Receive device request from reader
- Show consent dialog to user
- Create response with selected documents
- Send response back to reader
- 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
}
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
4. Always Show Consent Dialog
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
AbstractMdocNfcServicefor 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
- ISO/IEC 18013-5:2021 - Official standard
- Identity Development Kit Documentation
For questions or support, open an issue at: https://github.com/Sphereon-Opensource/identity-development-kit/issues