Enrollment Orchestration 

This page explains how to orchestrate enrollment with mobileID.enrollment, resumable flow.

What Enrollment Orchestration Covers 

Use the enrollment API to:

  • retrieve available enrollment candidates,
  • start a new enrollment (remote or activation-token based),
  • resume an interrupted in-progress enrollment,
  • handle enrollment-related deep links (for example phone OTP binding),
  • react to busy/idle UX signals and custom plugin view hooks,
  • handle terminal outcomes (success, cancel, retryable failure, interruption).

Core API Surface 

The orchestration entry point is mobileID.enrollment:

  • candidates() async throws -> [EnrollmentCandidate]
  • canResume() async -> EnrollmentResumptionStatus
  • start(with:in:delegate:) async -> Result<CredentialUpdateContext, EnrollmentProcessFailure>
  • resume(with:in:delegate:) async -> Result<CredentialUpdateContext, EnrollmentProcessFailure>
  • handle(deepLink:) -> Bool
  • cancel() async
  • transactionData: EnrollmentTransactionData?

Available Enrollment Plugins 

The SDK resolves enrollment steps through EnrollmentPlugin classes.
There are two groups:

  • integrator-configurable plugins (provided in MobileIDConfiguration.enrollmentPlugins),
  • internal plugins (always added by the SDK engine).

Integrator-Configurable Plugins 

Plugin Class
Module
What it does
EnrollmentPluginPhoneInfoMobileIDEnrollmentPluginPhoneBindingCollects and submits the user phone number for binding (PHONE_INFO).
EnrollmentPluginPhoneOtpMobileIDEnrollmentPluginPhoneBindingVerifies the phone ownership with OTP and completes phone binding (PHONE_OTP).
EnrollmentPluginEmailInfoMobileIDEnrollmentPluginEmailBindingCollects and submits the user email for email binding (EMAIL_INFO).
EnrollmentPluginDocumentAuthenticationMobileIDEnrollmentPluginDocumentAuthenticationRuns local document capture/authentication and submits captured document pages (DOC_AUTH / WEBBIO_CODING).
EnrollmentPluginRemoteDocumentCaptureMobileIDEnrollmentPluginDocumentAuthenticationRuns remote/live document capture session flow (DOCUMENT_LIVE_CAPTURE).
EnrollmentPluginMrzScanningMobileIDEnrollmentPluginDocumentAuthenticationCaptures and submits MRZ data used in ICAO/NFC-assisted document flows (NFC_MRZ).
EnrollmentPluginNfcScanningMobileIDEnrollmentPluginDocumentAuthenticationPerforms NFC chip read and validates document chip data (NFC_PHASE).
EnrollmentPluginFaceMatchingMobileIDEnrollmentPluginFaceMatchingCaptures selfie/liveness data and submits face matching payload (FACE_MATCHING).
EnrollmentPluginReferenceSelfieMobileIDEnrollmentPluginFaceMatchingSubmits previously stored reference selfie for matching (MID_SELFIE_MATCHING).
EnrollmentPluginLivenessChallengeSessionCreationMobileIDEnrollmentPluginFaceMatchingCreates a remote liveness challenge session (LIVENESS_CHALLENGE_SESSION_CREATION).
EnrollmentPluginLivenessChallengePhaseMobileIDEnrollmentPluginFaceMatchingExecutes remote liveness challenge capture phase (LIVENESS_CHALLENGE_PHASE).
EnrollmentPluginAttributeMatchingMobileIDEnrollmentPluginDocumentAuthenticationHandles backend-driven attribute matching expected data branch (ATTRIBUTE_MATCHING).

End-To-End Flow 

diagram

Start A Remote Enrollment 

Swift
1import MobileIDCore
2import UIKit
3
4@MainActor
5final class EnrollmentCoordinator: NSObject, EnrollmentInteractionDelegate {
6 private let mobileID: MobileID
7 private let navigationController: UINavigationController
8
9 init(mobileID: MobileID, navigationController: UINavigationController) {
10 self.mobileID = mobileID
11 self.navigationController = navigationController
12 }
13
14 func startRemoteEnrollment() async {
15 do {
16 let candidates = try await mobileID.enrollment.candidates()
17 guard let candidate = candidates.first(where: { $0.isRemoteEnrollmentAvailable }) else {
18 // No remotely enrollable credential available for this user.
19 return
20 }
21
22 let params = EnrollmentParameters(
23 enrollmentType: .remote(candidate: candidate),
24 customSteps: [],
25 authenticatorBiometricProtection: .strong
26 )
27
28 let result = await mobileID.enrollment.start(
29 with: params,
30 in: navigationController,
31 delegate: self
32 )
33
34 await handle(result)
35 } catch {
36 // candidates() failed (network/config/runtime).
37 }
38 }
39
40 private func handle(_ result: Result<CredentialUpdateContext, EnrollmentProcessFailure>) async {
41 switch result {
42 case .success(let context):
43 let credential = context.credential
44 print("Enrollment succeeded: \(credential)")
45
46 case .failure(.cancelled):
47 print("User cancelled enrollment")
48
49 case .failure(.interrupted):
50 print("Enrollment interrupted by another enrollment start")
51
52 case .failure(.failed(let error)):
53 print("Non-retryable enrollment error: \(error)")
54
55 case .failure(.failedWithRetryPossibility(let error, let onRetry)):
56 print("Retryable error: \(error)")
57 let retryResult = await onRetry()
58 await handle(retryResult)
59 }
60 }
61
62 func enrollmentDidStart(timestamp: Date) {}
63
64 func enrollmentDidBecomeBusy(expectedInterval: NavigatorBusyStateInterval) {
65 // Show loader/progress UI.
66 }
67
68 func enrollmentDidBecomeIdle() {
69 // Hide loader/progress UI.
70 }
71
72 func enrollmentView(for context: EnrollmentViewContext) -> UIViewController? {
73 // Optional: return a custom view controller for plugin-defined interaction points.
74 // Call context.onContinue(nil) when user confirms, or context.onContinue(error) to abort.
75 nil
76 }
77}

Resume An In-Progress Enrollment 

Swift
1@MainActor
2func resumeIfNeeded(mobileID: MobileID, navigationController: UINavigationController, delegate: EnrollmentInteractionDelegate) async {
3 let status = await mobileID.enrollment.canResume()
4
5 guard case .canResume = status else {
6 return
7 }
8
9 let result = await mobileID.enrollment.resume(
10 with: [],
11 in: navigationController,
12 delegate: delegate
13 )
14
15 // Handle result with the same success/failure logic as start().
16 _ = result
17}

DeepLink(url:) can parse SDK links. For enrollment orchestration, the two relevant branches are:

  • .enrollment(payload:): use this to build ActivationToken and start token-based enrollment,
  • .phoneBinding(otp:): forward to mobileID.enrollment.handle(deepLink:) so phone OTP can be consumed by the active step.
Swift
1func application(
2 _ app: UIApplication,
3 open url: URL,
4 options: [UIApplication.OpenURLOptionsKey: Any] = [:]
5) -> Bool {
6 guard let deepLink = DeepLink(url: url) else { return false }
7
8 switch deepLink {
9 case .phoneBinding:
10 return mobileID.enrollment.handle(deepLink: deepLink)
11
12 case .enrollment:
13 do {
14 let token = try ActivationToken(deepLink: deepLink)
15 let params = EnrollmentParameters(
16 enrollmentType: .activationToken(token),
17 authenticatorBiometricProtection: .strong
18 )
19
20 Task { @MainActor in
21 _ = await mobileID.enrollment.start(with: params, in: rootNavigationController, delegate: enrollmentDelegate)
22 }
23 return true
24 } catch {
25 return false
26 }
27
28 default:
29 // qrCode/mdoc deep links are handled by other SDK domains.
30 return false
31 }
32}

Custom Step Integration (Advanced) 

Use CustomStep when your backend process includes an expected data key that must be fulfilled by app-owned logic.

Swift
1import MobileIDCommonUtils
2import MobileIDCore
3
4struct TermsAcceptanceSubmission: EnrollmentSubmission {
5 let path = "transactions/{transactionId}/terms-acceptance"
6 let body: SubmissionBody
7
8 init(version: String, accepted: Bool) {
9 body = SubmissionBody([
10 "version": version,
11 "accepted": accepted
12 ])
13 }
14}
15
16final class TermsAcceptanceStep: CustomStep {
17 let key = "TERMS_ACCEPTANCE"
18
19 func onStep(navigator: Navigator) async throws -> EnrollmentSubmission {
20 // Present your own UI, collect consent, then submit payload.
21 return TermsAcceptanceSubmission(version: "2026-01", accepted: true)
22 }
23
24 func cancelRequested() {
25 // Optional: cancel internal tasks and make onStep throw CancellationError.
26 }
27}

Operational Guidance 

  • Always call canResume() on app launch or when returning to your enrollment entry screen.
  • Keep a single enrollment at a time; starting while one is active returns an active-enrollment failure.
  • Route deep links early (App/Scene delegate) and pass phone binding links to handle(deepLink:) immediately.
  • Treat .failedWithRetryPossibility as a user-recovery path and preserve context for retry.
  • Use transactionData only as transient runtime data for an active process.
  • Keep enrollmentPlugins aligned with backend-enabled workflow steps.
  • EnrollmentParameters
  • EnrollmentType (.remote, .activationToken)
  • EnrollmentProcessFailure
  • EnrollmentInteractionDelegate
  • CustomStep / EnrollmentSubmission