Credentials Wallet And its Lifecycle 

This page explains how to inspect the on-device credential wallet, read credential data, react to lifecycle changes, maintain authentication keys and Mobile Security Objects (MSOs), and remove credentials safely.

What This Covers 

  • listing and identifying credentials in mobileID.wallet
  • handling pending, failed, and enrolled credential states
  • reading attributes, media, and data groups
  • issuing additional credentials from an eligible primary credential
  • registering lifecycle callbacks with mobileID.events
  • checking for remote updates and SDK status changes
  • rotating authentication keys and refreshing MSOs
  • forwarding the APNs device token and reacting to push signals
  • removing credentials and cleaning up wallet-level user data

API Surface 

Wallet And Credential APIs 

  • mobileID.wallet
  • Wallet.credentials() -> [Credential]
  • Wallet.remove(credential:) async throws
  • Credential
  • PendingCredential
  • FailedCredential.reissue() async throws
  • EnrolledCredential
  • AdditionalCredentialCandidate.issue() async throws

Credential Data APIs 

  • EnrolledCredential.attributes() async throws -> Attributes
  • EnrolledCredential.media() async throws -> Media
  • EnrolledCredential.supportsDataGroup(ofType:) -> Bool
  • EnrolledCredential.dataGroup(ofType:) throws -> DataGroup
  • EnrolledCredential.additionalCredentialCandidates() async throws
  • Credential.authenticator() throws -> CredentialKey

Lifecycle APIs 

  • mobileID.status() -> MobileIDStatus
  • mobileID.update() async -> MobileIDError?
  • mobileID.events
  • mobileID.keyRotationStatus(for:) async throws
  • mobileID.rotateKey(for:) async throws
  • mobileID.updateMSO(for:) async
  • mobileID.pushNotifications
  • mobileID.cleanup() throws

Credential Lifecycle Model 

Wallet.credentials() can return credentials at different issuance stages. Only EnrolledCredential exposes issued identity data and a CredentialStatus.

diagram

Credential type and credential status represent different things:

Type or status
Meaning
PendingCredentialIssuance started but has not finished.
FailedCredentialIssuance failed and can be retried with reissue().
EnrolledCredentialIssuance finished; issued data APIs are available.
CredentialStatus.activeThe enrolled credential is ready to use.
CredentialStatus.cancelledThe issuer revoked access without deleting local data; this can be reversed.
CredentialStatus.updateRequiredStored credential data must be brought to the current format or state.
MobileIDStatus.upgradeRequiredThe SDK cannot continue until the app is upgraded.

Read The Wallet Inventory 

Treat wallet.credentials() as the current local snapshot. Do not assume every returned item is enrolled.

Swift
1import MobileIDCore
2
3struct WalletSnapshot {
4 let enrolled: [EnrolledCredential]
5 let pending: [PendingCredential]
6 let failed: [FailedCredential]
7}
8
9func walletSnapshot(from mobileID: MobileID) -> WalletSnapshot {
10 let credentials = mobileID.wallet.credentials()
11
12 return WalletSnapshot(
13 enrolled: credentials.compactMap { $0 as? EnrolledCredential },
14 pending: credentials.compactMap { $0 as? PendingCredential },
15 failed: credentials.compactMap { $0 as? FailedCredential }
16 )
17}

All credential types expose:

  • midUid: unique identifier for this enrollment instance
  • credentialId: document identifier shared by multiple enrollments of that document
  • remoteCredentialId: issuer-side credential identifier
  • primaryCredentialId: identifier of the credential that enabled this credential's issuance, when applicable
  • friendlyNames: localized display names supplied by configuration
  • featuresConfiguration: optional capabilities for the credential
  • authenticator(): access to the credential authentication key

Use midUid when a view model needs to distinguish wallet entries. Re-read the wallet after an issuance or lifecycle callback instead of retaining an old snapshot indefinitely.

Enforce Credential And SDK Status 

Check SDK-wide status before starting a protected journey, then check each enrolled credential's status before offering it for use.

Swift
1func usableCredentials(from mobileID: MobileID) -> [EnrolledCredential] {
2 guard mobileID.status() == .active else {
3 return []
4 }
5
6 return mobileID.wallet.credentials()
7 .compactMap { $0 as? EnrolledCredential }
8 .filter { $0.status == .active }
9}

Handle .upgradeRequired as a blocking app-upgrade state. Do not delete a .cancelled credential merely because it is unavailable; the issuer can reinstate it through a later lifecycle update.

Read Credential Attributes 

Use the asynchronous API and retrieve only the attributes needed by the current feature.

Swift
1func identitySummary(from credential: EnrolledCredential) async throws -> [String: String] {
2 let attributes = try await credential.attributes()
3
4 let filtered = attributes.filter(
5 applying: AttributesFilter(attributesToKeep: [
6 "given_name",
7 "family_name",
8 "birth_date"
9 ])
10 )
11
12 return filtered.toMap()
13}

You can read one value with attributes["family_name"]. Handle MobileIDError.attributesNotFound separately from unexpected storage or keychain failures.

Attribute data is sensitive. Avoid logging Attributes.toMap(), and filter before handing values to an app layer that does not need the full credential.

Read Credential Media 

Media is grouped by media type, with one or more Asset values per type. Asset content is loaded asynchronously.

Swift
1func portraitData(from credential: EnrolledCredential) async throws -> Data? {
2 let media = try await credential.media()
3
4 guard let portrait = media["frontPortrait"]?.first else {
5 return nil
6 }
7
8 return try await portrait.content()
9}

The actual media keys depend on issuance configuration. Handle MobileIDError.mediaNotFound when no media was issued, and use each asset's contentType and mediaType before decoding its raw Data.

Read Data Groups 

Check availability before requesting a data group:

Swift
1func simpleDataGroup(from credential: EnrolledCredential) throws -> DataGroup? {
2 guard credential.supportsDataGroup(ofType: .simple) else {
3 return nil
4 }
5
6 return try credential.dataGroup(ofType: .simple)
7}

DataGroupType provides .standard, .simple, and .icao. The simple CBOR data group is used to build ISO 18013-5-compatible responses. DataGroup.content is encoded binary data; decode it only according to the contract for that data-group type.

If a supported group disappears or cannot be read, handle MobileIDError.dataGroupNotFound and refresh the wallet state rather than using cached data.

Issue Additional Credentials 

An enrolled credential with multiCredentialPrimary == true can make other credential candidates available.

Swift
1func issueFirstAdditionalCredential(from credential: EnrolledCredential) async throws {
2 guard credential.multiCredentialPrimary else {
3 return
4 }
5
6 let candidates = try await credential.additionalCredentialCandidates()
7 guard let candidate = candidates.first else {
8 return
9 }
10
11 try await candidate.issue()
12}

Issuance may be asynchronous. Observe events.onIssuingStatusChanged, then inspect whether the supplied Credential is pending, failed, or enrolled. Call FailedCredential.reissue() to retry a failed issuance.

Register Lifecycle Events 

Register callbacks soon after MobileID initialization and before calling update(). The callbacks describe changes to credentials, SDK status, requests, and maintenance operations.

Swift
1func configureLifecycleEvents(for mobileID: MobileID) {
2 mobileID.events.onMobileIDStatusChanged = { status in
3 if status == .upgradeRequired {
4 // Block SDK journeys and direct the user to update the app.
5 }
6 }
7
8 mobileID.events.onCredentialsCancellationStateChanged = { _ in
9 // Rebuild the wallet snapshot and refresh affected rows.
10 }
11
12 mobileID.events.onCredentialDisenrolledRemotely = { _ in
13 // Remove stale app state and rebuild the wallet snapshot.
14 }
15
16 mobileID.events.onUpdateFinished = { update in
17 switch update.result {
18 case .success(let summary):
19 print("Changed attributes: \(summary.diff.affectedAttributeIDs)")
20 case .failure(let error):
21 print("Credential update failed: \(error)")
22 }
23 }
24
25 mobileID.events.onIssuingStatusChanged = { _ in
26 // Rebuild the wallet snapshot and refresh the UI.
27 }
28
29 mobileID.events.onDeviceRemovedError = {
30 // SDK data was removed because the device was revoked remotely.
31 }
32}

Other callbacks include:

  • onMsoUpdateFinished
  • onKeyRotationRequired
  • onNewMessagesReceived
  • onPendingRequestsReceived

Each event property accepts one callback; assigning a new closure replaces the previous one. Centralize registration if several app features consume lifecycle events. Do not assume callbacks arrive on the main thread. Move UI work to MainActor, and use weak captures when a callback closes over a coordinator or view model.

Check For Updates 

Call update() periodically while the app is active and after receiving a relevant push notification. It requires at least one enrolled credential and returns nil on success or a MobileIDError on failure.

Swift
1func refreshMobileID(_ mobileID: MobileID) async {
2 let hasEnrolledCredential = mobileID.wallet.credentials()
3 .contains { $0 is EnrolledCredential }
4
5 guard hasEnrolledCredential else {
6 return
7 }
8
9 if let error = await mobileID.update() {
10 print("MobileID refresh failed: \(error)")
11 return
12 }
13
14 let credentials = mobileID.wallet.credentials()
15 print("Wallet now contains \(credentials.count) credentials")
16}

update() can trigger credential update, cancellation, remote removal, key-rotation, message, and pending-request callbacks. Rebuild your wallet view after it completes and after relevant callbacks.

Register For Push Notifications 

Forward the APNs device token to the SDK:

Swift
1import UIKit
2
3func application(
4 _ application: UIApplication,
5 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
6) {
7 mobileID.pushNotifications.didReceive(deviceToken: deviceToken)
8}

Use mobileID.pushNotifications.isTokenStored to inspect registration state and clearToken() when push forwarding must be disabled. When the app receives a lifecycle push signal, call await mobileID.update(); there is no separate public API for forwarding the notification payload.

Authentication Key And Biometric Protection 

credential.authenticator() returns the credential's Secure Enclave-backed CredentialKey. It exposes its identifier, expiration date, biometric protection type, and sign(data:) operation.

Swift
1func enableOptionalBiometricProtection(for credential: EnrolledCredential) throws {
2 let key = try credential.authenticator()
3
4 guard key.protectionType == .off else {
5 return
6 }
7
8 try key.enableBiometricProtection()
9}

Protection can move from .off to .weak, or from .weak back to .off. .strong protection is permanent. Unsupported transitions throw CredentialDataError.illegalOperation; a credential removed during the operation can produce .credentialNotFound.

Do not cache a CredentialKey. Consume it immediately for the intended operation because rotation or lifecycle changes can invalidate a retained value.

Rotate Authentication Keys 

Check each enrolled credential's authentication key and rotate it when recommended or required.

Swift
1func maintainAuthenticationKeys(for mobileID: MobileID) async throws {
2 let credentials = mobileID.wallet.credentials()
3 .compactMap { $0 as? EnrolledCredential }
4
5 for credential in credentials {
6 switch try await mobileID.keyRotationStatus(for: credential) {
7 case .notSupported, .notNeeded(_):
8 break
9
10 case .recommended(_), .required:
11 try await mobileID.rotateKey(for: credential)
12 }
13 }
14}

.notNeeded(days) means more than 30 days remain; .recommended(days) means 30 days or less remain; .required means the key has expired. A regular update() also reports recommended and required items through onKeyRotationRequired.

Refresh Mobile Security Objects 

MSOs have a limited validity period. The MSO update flow is provided by MobileIDInPersonSharing; when that module is not linked, updateMSO(for:) returns without starting work or emitting an MSO result. With the module linked, ask the SDK to update the relevant enrolled credentials and observe onMsoUpdateFinished for each result.

Swift
1func refreshMSOs(for mobileID: MobileID) async {
2 let credentials = mobileID.wallet.credentials()
3 .compactMap { $0 as? EnrolledCredential }
4
5 mobileID.events.onMsoUpdateFinished = { update in
6 switch update.result {
7 case .success:
8 // Refresh any UI that depends on MSO validity.
9 case .failure(let error):
10 print("MSO update failed: \(error)")
11 }
12 }
13
14 await mobileID.updateMSO(for: credentials)
15}

EnrolledCredential.msoStatus reports .valid, .inProgress, or .expired for credentials with the required ISO data group. A regular mobileID.update() also runs the available MSO maintenance flow.

Remove Credentials And Clean Up 

Use the wallet API to remove any selected credential, regardless of whether it is pending, failed, or enrolled:

Swift
1func remove(_ credential: Credential, from mobileID: MobileID) async throws {
2 try await mobileID.wallet.remove(credential: credential)
3}

Removal disenrolls the credential from MobileID services when required and deletes its local credential data and keys. mobileID.unenroll(credential:) remains public for enrolled credentials, but Wallet.remove(credential:) is the preferred multi-credential API.

mobileID.cleanup() removes remaining wallet-level user data such as the user identifier and stored PIN. It requires an empty wallet and throws MobileIDError.walletNotEmpty otherwise:

Swift
1for credential in mobileID.wallet.credentials() {
2 try await mobileID.wallet.remove(credential: credential)
3}
4
5try mobileID.cleanup()

Treat this as a destructive sign-out or account-removal operation and require explicit user intent before running it.

Common Pitfalls 

  • Assuming every Credential is an EnrolledCredential.
  • Using credentialId as the unique identifier for a wallet entry instead of midUid.
  • Offering cancelled or update-required credentials as active choices.
  • Caching attributes, media, data groups, or authentication keys across lifecycle updates.
  • Logging full identity attributes, binary data groups, or credential identifiers.
  • Calling cleanup() before removing every credential.
  • Registering lifecycle callbacks after the first update() call.
  • Updating UIKit directly from a lifecycle callback without moving to the main actor.
  • Ignoring .upgradeRequired, required key rotation, or an expired MSO.
  • Getting Started
  • Enrollment Orchestration
  • PIN And Liveness Verification
  • Theming, Branding, And Localization