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.walletWallet.credentials() -> [Credential]Wallet.remove(credential:) async throwsCredentialPendingCredentialFailedCredential.reissue() async throwsEnrolledCredentialAdditionalCredentialCandidate.issue() async throws
Credential Data APIs
EnrolledCredential.attributes() async throws -> AttributesEnrolledCredential.media() async throws -> MediaEnrolledCredential.supportsDataGroup(ofType:) -> BoolEnrolledCredential.dataGroup(ofType:) throws -> DataGroupEnrolledCredential.additionalCredentialCandidates() async throwsCredential.authenticator() throws -> CredentialKey
Lifecycle APIs
mobileID.status() -> MobileIDStatusmobileID.update() async -> MobileIDError?mobileID.eventsmobileID.keyRotationStatus(for:) async throwsmobileID.rotateKey(for:) async throwsmobileID.updateMSO(for:) asyncmobileID.pushNotificationsmobileID.cleanup() throws
Credential Lifecycle Model
Wallet.credentials() can return credentials at different issuance stages. Only EnrolledCredential exposes issued identity data and a CredentialStatus.
Credential type and credential status represent different things:
Type or status | Meaning |
|---|---|
PendingCredential | Issuance started but has not finished. |
FailedCredential | Issuance failed and can be retried with reissue(). |
EnrolledCredential | Issuance finished; issued data APIs are available. |
CredentialStatus.active | The enrolled credential is ready to use. |
CredentialStatus.cancelled | The issuer revoked access without deleting local data; this can be reversed. |
CredentialStatus.updateRequired | Stored credential data must be brought to the current format or state. |
MobileIDStatus.upgradeRequired | The 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.
Swift1import MobileIDCore23struct WalletSnapshot {4 let enrolled: [EnrolledCredential]5 let pending: [PendingCredential]6 let failed: [FailedCredential]7}89func walletSnapshot(from mobileID: MobileID) -> WalletSnapshot {10 let credentials = mobileID.wallet.credentials()1112 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 instancecredentialId: document identifier shared by multiple enrollments of that documentremoteCredentialId: issuer-side credential identifierprimaryCredentialId: identifier of the credential that enabled this credential's issuance, when applicablefriendlyNames: localized display names supplied by configurationfeaturesConfiguration: optional capabilities for the credentialauthenticator(): 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.
Swift1func usableCredentials(from mobileID: MobileID) -> [EnrolledCredential] {2 guard mobileID.status() == .active else {3 return []4 }56 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.
Swift1func identitySummary(from credential: EnrolledCredential) async throws -> [String: String] {2 let attributes = try await credential.attributes()34 let filtered = attributes.filter(5 applying: AttributesFilter(attributesToKeep: [6 "given_name",7 "family_name",8 "birth_date"9 ])10 )1112 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.
Swift1func portraitData(from credential: EnrolledCredential) async throws -> Data? {2 let media = try await credential.media()34 guard let portrait = media["frontPortrait"]?.first else {5 return nil6 }78 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:
Swift1func simpleDataGroup(from credential: EnrolledCredential) throws -> DataGroup? {2 guard credential.supportsDataGroup(ofType: .simple) else {3 return nil4 }56 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.
Swift1func issueFirstAdditionalCredential(from credential: EnrolledCredential) async throws {2 guard credential.multiCredentialPrimary else {3 return4 }56 let candidates = try await credential.additionalCredentialCandidates()7 guard let candidate = candidates.first else {8 return9 }1011 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.
Swift1func configureLifecycleEvents(for mobileID: MobileID) {2 mobileID.events.onMobileIDStatusChanged = { status in3 if status == .upgradeRequired {4 // Block SDK journeys and direct the user to update the app.5 }6 }78 mobileID.events.onCredentialsCancellationStateChanged = { _ in9 // Rebuild the wallet snapshot and refresh affected rows.10 }1112 mobileID.events.onCredentialDisenrolledRemotely = { _ in13 // Remove stale app state and rebuild the wallet snapshot.14 }1516 mobileID.events.onUpdateFinished = { update in17 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 }2425 mobileID.events.onIssuingStatusChanged = { _ in26 // Rebuild the wallet snapshot and refresh the UI.27 }2829 mobileID.events.onDeviceRemovedError = {30 // SDK data was removed because the device was revoked remotely.31 }32}
Other callbacks include:
onMsoUpdateFinishedonKeyRotationRequiredonNewMessagesReceivedonPendingRequestsReceived
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.
Swift1func refreshMobileID(_ mobileID: MobileID) async {2 let hasEnrolledCredential = mobileID.wallet.credentials()3 .contains { $0 is EnrolledCredential }45 guard hasEnrolledCredential else {6 return7 }89 if let error = await mobileID.update() {10 print("MobileID refresh failed: \(error)")11 return12 }1314 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:
Swift1import UIKit23func application(4 _ application: UIApplication,5 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data6) {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.
Swift1func enableOptionalBiometricProtection(for credential: EnrolledCredential) throws {2 let key = try credential.authenticator()34 guard key.protectionType == .off else {5 return6 }78 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.
Swift1func maintainAuthenticationKeys(for mobileID: MobileID) async throws {2 let credentials = mobileID.wallet.credentials()3 .compactMap { $0 as? EnrolledCredential }45 for credential in credentials {6 switch try await mobileID.keyRotationStatus(for: credential) {7 case .notSupported, .notNeeded(_):8 break910 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.
Swift1func refreshMSOs(for mobileID: MobileID) async {2 let credentials = mobileID.wallet.credentials()3 .compactMap { $0 as? EnrolledCredential }45 mobileID.events.onMsoUpdateFinished = { update in6 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 }1314 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:
Swift1func 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:
Swift1for credential in mobileID.wallet.credentials() {2 try await mobileID.wallet.remove(credential: credential)3}45try 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
Credentialis anEnrolledCredential. - Using
credentialIdas the unique identifier for a wallet entry instead ofmidUid. - 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.
Related Pages
Getting StartedEnrollment OrchestrationPIN And Liveness VerificationTheming, Branding, And Localization