Digital Credential API (ISO 18013-7 Annex C)
This page explains how to make Mobile ID credentials available to the iOS Digital Credentials API for web presentment. It covers host-app registration, App Group context sharing, Identity Document Provider extension integration, user authorization, and ISO/IEC 18013-7 Annex C response creation.
This flow is different from QR-code and proximity sharing. The browser and iOS initiate the request, the user selects your document provider, and your extension returns an encrypted mobile document response.
What This Covers
- configuring the app and extension for iOS Digital Credentials
- creating Mobile ID with a shared App Group
- registering and unregistering eligible credentials with iOS
- writing a portable credential snapshot for the extension
- loading and filtering credentials inside the extension
- showing app-owned authorization UI
- processing the raw Annex C request with
DigitalCredential.accept(...) - keeping registrations and shared context synchronized with wallet lifecycle changes
Platform Requirements
- iOS
26.0+ - Xcode
26.0+ MobileIDCoreMobileIDInPersonSharing- an Identity Document Provider app extension
- the same App Group capability on the host app and extension targets
- Apple's Digital Credentials API - Mobile Document Provider entitlement, configured with every supported mobile document type
Both targets must belong to the same App Group so the extension can read the shared context and use the credential signing key. Apple documents that App Group names can be used as keychain access groups.
See Apple's identity document provider integration guide for entitlement approval, extension creation, and platform authorization behavior.
SDK API Surface
Shared Context
DigitalCredentialContextProvider.init(appGroup:mobileID:)DigitalCredentialContextProvider.update() async throwsDigitalCredentialContext.init?(appGroup:) throwsDigitalCredentialContext.credentials: [DigitalCredential]DigitalCredentialDigitalCredential.accept(rawRequest:origin:) async throws -> DataDigitalCredentialContextError.invalidRequest
Platform Registration
EnrolledCredential.digitalCredentialRegistrationStatusEnrolledCredential.registerDigitalCredential(allowedAuthorities:)EnrolledCredential.unregisterDigitalCredential()DigitalCredentialRegistrationStatusDigitalCredentialRegistrationErrorSupportedAuthorityKeyIdentifiers.anySupportedAuthorityKeyIdentifiers.only(_:)
End-To-End Flow
The host app owns wallet and registration maintenance. The extension owns authorization UI and request completion. DigitalCredentialContext is the bridge between them.
1. Configure A Shared App Group
Choose one App Group identifier and add it to both targets. Pass that exact value to MobileIDFactory.create(...):
Make sure the host app and the Identity Document Provider extension both have the App Group entitlement enabled for the same identifier. The static appGroup value in your app should match the entitlement in both targets.
Swift1import MobileIDCore23enum DigitalCredentialConfiguration {4 static let appGroup = "group.com.example.mobileid"5}67let mobileID = try await MobileIDFactory.create(8 environmentConfiguration: .app,9 configuration: .app,10 tracker: nil,11 analyticsOptOut: true,12 appGroup: DigitalCredentialConfiguration.appGroup13)
Place the identifier constant in source included by both the host app and extension targets.
Configure the App Group before enrolling credentials so the host app and extension can share credential state consistently.
Use the same identifier when creating DigitalCredentialContextProvider in the app and DigitalCredentialContext in the extension. A mismatch can make the context appear empty or prevent device-authentication signing.
2. Build The Shared Credential Context
The host app converts enrolled wallet entries into portable DigitalCredential values and writes them to App Group storage:
Swift1import MobileIDInPersonSharing23@available(iOS 26.0, *)4func refreshDigitalCredentialContext(using mobileID: MobileID) async throws {5 let provider = try DigitalCredentialContextProvider(6 appGroup: DigitalCredentialConfiguration.appGroup,7 mobileID: mobileID8 )910 try await provider.update()11}
Call update() after:
- initial enrollment or additional credential issuance
mobileID.update()- cancellation, reinstatement, or remote removal
- credential data or MSO updates
- authentication-key rotation
- local credential removal
DigitalCredentialContextProvider rebuilds its snapshot from enrolled credentials in the wallet. Each enrolled credential should provide readable attributes, an authentication key, and the .simple ISO data group. If the data group or MSO cannot be read, the update throws instead of writing a partial snapshot.
The shared context includes identity attributes and the encoded data group. Treat the App Group as sensitive storage and never log or export the context.
3. Register Eligible Credentials With iOS
Register only active, unexpired credentials that can answer ISO requests:
Swift1import MobileIDCore2import MobileIDInPersonSharing34@available(iOS 26.0, *)5func registerEligibleCredentials(from mobileID: MobileID) async {6 let credentials = mobileID.wallet.credentials()7 .compactMap { $0 as? EnrolledCredential }89 for credential in credentials {10 do {11 let isPresentable = credential.status == .active1213 guard isPresentable else {14 try await credential.unregisterDigitalCredential()15 continue16 }1718 try await credential.registerDigitalCredential(19 allowedAuthorities: .any20 )21 } catch {22 // Record a redacted operational error and continue with other credentials.23 }24 }25}
Registration uses:
- the first document type in the credential's ISO data group,
credentialIdas the platform document identifier,- the MSO
validUntilDateas the platform invalidation date.
The registration wrapper uses the first document type found in the ISO data group. If a credential contains several document types, verify that this matches your registration and entitlement model.
The first registration can cause iOS to ask the user to authorize your app as a document provider. If the user declines, registration throws; the user can change the decision later in Settings.
Restrict Requesting Authorities
Choose the authority policy deliberately:
Swift1// The provider may appear without enforcing a requester-signing requirement.2try await credential.registerDigitalCredential(allowedAuthorities: .any)34// Only requests chained to one of these subject key identifiers are eligible.5try await credential.registerDigitalCredential(6 allowedAuthorities: .only([7 trustedAuthoritySubjectKeyIdentifier8 ])9)
.only([Data]) expects X.509 subject key identifier bytes, not a complete certificate. Use a nonempty list from an approved trust policy. .only([]) has the same empty-list effect as .any, which does not enforce requester signing at registration time.
Inspect Registration Status
Swift1@available(iOS 26.0, *)2func isRegistered(_ credential: EnrolledCredential) async throws -> Bool {3 switch try await credential.digitalCredentialRegistrationStatus {4 case .registered:5 return true6 case .notRegistered:7 return false8 }9}
Registration status checks whether the platform store contains an entry whose document identifier equals credential.credentialId.
4. Load The Context In The Extension
The initializer is both throwing and failable. nil means the host app has not written a context yet.
Swift1import MobileIDInPersonSharing23@available(iOS 26.0, *)4func loadDigitalCredentials() throws -> [DigitalCredential] {5 guard let context = try DigitalCredentialContext(6 appGroup: DigitalCredentialConfiguration.appGroup7 ) else {8 return []9 }1011 return context.credentials12}
Each DigitalCredential exposes:
Property | Purpose |
|---|---|
id | Platform document identifier; derived from credentialId. |
type | Mobile ID CredentialType. |
datagroup | Encoded .simple data group used to build the response. |
attributes | Display data for app-owned authorization UI. |
validUntil | MSO validity end date. |
friendlyNames | Localized credential display names. |
isCancelled | Whether the source credential was not active when the snapshot was built. |
docTypes | Document types contained in the ISO data group. |
Filter out cancelled and expired entries, then select a credential whose docTypes cover the parsed request:
Swift1@available(iOS 26.0, *)2func selectCredential(3 from credentials: [DigitalCredential],4 requestedDocumentTypes: Set<String>5) -> DigitalCredential? {6 credentials.first { credential in7 !credential.isCancelled8 && credential.validUntil > Date()9 && Set(credential.docTypes).isSuperset(of: requestedDocumentTypes)10 }11}
Derive requestedDocumentTypes from the parsed ISO18013MobileDocumentRequest supplied by the system. If a request set contains several document requests, the response must satisfy the whole set.
5. Create The Identity Document Provider Extension
Use Xcode's Identity Document Provider extension template and create an ISO18013MobileDocumentRequestScene:
Swift1import IdentityDocumentServicesUI2import SwiftUI34@main5struct DocumentProviderExtension: IdentityDocumentProvider {6 var body: some IdentityDocumentRequestScene {7 ISO18013MobileDocumentRequestScene { context in8 AuthorizationView(requestContext: context)9 }10 }1112 func performRegistrationUpdates() async {13 // Reconcile platform registrations with the app's shared wallet snapshot.14 }15}
This is an extension skeleton. Do not ship an empty performRegistrationUpdates(); its production implementation must reconcile the platform store with the app's shared wallet snapshot and authority policy.
The app owns AuthorizationView. It should show:
requestingWebsiteOrigin, when present- the requested document types, namespaces, and data elements from
request - the credential the app proposes to use
- explicit Share and Cancel actions
Do not display every value in DigitalCredential.attributes by default. Show only data needed to identify the credential and explain the requested disclosure.
6. Accept The Raw Annex C Request
Call the system's sendResponse only after the user explicitly approves. The closure then receives the raw web-presentment request. Pass its data and the unmodified requesting origin to the selected DigitalCredential:
Swift1import IdentityDocumentServices2import IdentityDocumentServicesUI3import MobileIDInPersonSharing45@available(iOS 26.0, *)6func sendResponse(7 for requestContext: ISO18013MobileDocumentRequestContext,8 using credential: DigitalCredential9) async throws {10 try await requestContext.sendResponse { rawRequest in11 let responseData = try await credential.accept(12 rawRequest: rawRequest.requestData,13 origin: requestContext.requestingWebsiteOrigin14 )1516 return ISO18013MobileDocumentResponse(17 responseData: responseData18 )19 }20}
DigitalCredential.accept(rawRequest:origin:):
- validates and parses the Digital Credentials request,
- obtains the ISO 18013 digital-credential session information,
- builds a response from the stored issuer data group,
- creates device authentication with the shared credential key,
- returns the encoded response data expected by
ISO18013MobileDocumentResponse.
If validation does not produce both a device request and session information, it throws DigitalCredentialContextError.invalidRequest.
Keep the parsed requestContext.request available inside the response closure. The SDK wrapper validates the raw request but does not receive the system-parsed request, so it cannot perform an app-specific parsed-versus-raw consistency comparison. Run any additional comparison required by your trust policy before calling accept(...).
The current accept(...) API does not expose a custom selected-items parameter. Present the disclosure as approval or cancellation for the selected credential; do not add per-field toggles unless your integration uses a response builder that enforces those choices.
Pass requestingWebsiteOrigin through unchanged. The origin participates in request validation and session binding; do not replace a missing value with a guessed URL.
Cancel A Request
If the user declines, no credential matches, the context is unavailable, or policy validation fails, cancel the system request:
Swift1requestContext.cancel()
Do not call DigitalCredential.accept(...) before explicit user authorization. The raw request is intentionally released inside sendResponse after that interaction.
7. Reconcile Registrations Over Time
The shared context and the platform registration store are independent:
DigitalCredentialContextProvider.update()refreshes extension-readable credential data.registerDigitalCredential(...)makes a credential discoverable to iOS.unregisterDigitalCredential()removes platform discoverability.
Keep both representations synchronized.
Wallet change | Context action | Registration action |
|---|---|---|
| Credential enrolled or issued | Update context | Register if active, valid, and eligible. |
| Credential data or MSO updated | Update context | Register again to refresh type/validity metadata. |
| Key rotated | Update context | Registration can remain if metadata is unchanged. |
| Credential cancelled | Update context | Unregister until reinstated. |
| Credential reinstated | Update context | Register again. |
| Credential removed locally | Unregister first, remove from wallet, then update context. | |
| Credential removed remotely | Update context and reconcile stale platform registrations by identifier. |
Example local removal order:
Swift1@available(iOS 26.0, *)2func removeDigitalCredential(3 _ credential: EnrolledCredential,4 from mobileID: MobileID5) async throws {6 try await credential.unregisterDigitalCredential()7 try await mobileID.wallet.remove(credential: credential)8 try await refreshDigitalCredentialContext(using: mobileID)9}
If another eligible wallet entry has the same credentialId, re-register that remaining entry after removal. Unregistering is document-level and can otherwise make the remaining enrollment unavailable to the platform.
Apple can call IdentityDocumentProvider.performRegistrationUpdates() after authorization changes. Reconcile the platform store from your shared snapshot and the same authority policy used by the host app. If you use .only(...), persist that authority policy in shared app-owned storage because DigitalCredentialContext does not include it.
Error Handling
Context Errors
DigitalCredentialContext(appGroup:) == nil: no snapshot exists yet.DigitalCredentialContextError.invalidRequest: the raw request did not produce required request/session data.- storage, data-group, attribute, MSO, or key errors: context creation or response signing could not complete.
Registration Errors
DigitalCredentialRegistrationError includes:
.documentTypeUnknown.validityUnknown.underlying(Error)
Platform authorization, entitlement, and registration-store failures arrive as underlying errors. Present a safe recovery path rather than exposing raw error details to the user.
Recovery Rules
- Missing context: cancel the request and ask the user to open the host app so it can refresh.
- Cancelled or expired credential: do not present it; update or reinstate it in the host app.
- Invalid raw request: cancel without returning credential data.
- Signing-key lookup failure: verify the identical App Group is configured and was supplied before enrollment.
- Registration failure: verify entitlement approval, declared document types, platform authorization, and MSO validity.
Security And Privacy Guardrails
-
Register only active credentials that the extension can successfully present.
-
Require explicit user consent for every request.
-
Show the requesting origin and requested fields before sharing.
-
Treat the parsed request as display input; validate the raw request before building a response.
-
Pass the raw request only to the selected credential's
accept(...)method. -
Use
SupportedAuthorityKeyIdentifiers.only(...)when relying-party signing is required by policy. -
Do not log raw requests, responses, attributes, data groups, origins, key identifiers, or credential identifiers.
-
Keep App Group access limited to the host app and its provider extension.
-
Refresh context and registration metadata immediately after lifecycle changes.
-
Cancel on ambiguity; never choose an expired or cancelled credential as a fallback.
Troubleshooting
Symptom | Check |
|---|---|
| App is not offered during a web request | Confirm entitlement approval, document type declaration, successful registration, and current invalidation date. |
| Extension loads no credentials | Confirm the host app called DigitalCredentialContextProvider.update() and both targets use the same App Group. |
| Context update throws | Confirm every enrolled wallet entry has attributes, a parsable .simple data group, and an accessible authentication key. |
| Response signing cannot find the key | Confirm MobileIDFactory.create received the App Group before enrollment and the extension belongs to it. |
| Cancelled credential is still offered by iOS | Context refresh does not remove platform registration; call unregisterDigitalCredential() or reconcile the registration store. |
| Registration reports unknown document type | Confirm the .simple data group contains a document and its type is listed in the entitlement. |
| Registration reports unknown validity | Confirm the data group contains readable MSO validity information. |
| Request validation fails | Pass the exact rawRequest.requestData and unmodified requestingWebsiteOrigin. |
Related Pages
Getting StartedCredential Wallet And LifecyclePIN And Liveness VerificationTheming, Branding, And Localization