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+
  • MobileIDCore
  • MobileIDInPersonSharing
  • 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 throws
  • DigitalCredentialContext.init?(appGroup:) throws
  • DigitalCredentialContext.credentials: [DigitalCredential]
  • DigitalCredential
  • DigitalCredential.accept(rawRequest:origin:) async throws -> Data
  • DigitalCredentialContextError.invalidRequest

Platform Registration 

  • EnrolledCredential.digitalCredentialRegistrationStatus
  • EnrolledCredential.registerDigitalCredential(allowedAuthorities:)
  • EnrolledCredential.unregisterDigitalCredential()
  • DigitalCredentialRegistrationStatus
  • DigitalCredentialRegistrationError
  • SupportedAuthorityKeyIdentifiers.any
  • SupportedAuthorityKeyIdentifiers.only(_:)

End-To-End Flow 

diagram

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.

Swift
1import MobileIDCore
2
3enum DigitalCredentialConfiguration {
4 static let appGroup = "group.com.example.mobileid"
5}
6
7let mobileID = try await MobileIDFactory.create(
8 environmentConfiguration: .app,
9 configuration: .app,
10 tracker: nil,
11 analyticsOptOut: true,
12 appGroup: DigitalCredentialConfiguration.appGroup
13)

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:

Swift
1import MobileIDInPersonSharing
2
3@available(iOS 26.0, *)
4func refreshDigitalCredentialContext(using mobileID: MobileID) async throws {
5 let provider = try DigitalCredentialContextProvider(
6 appGroup: DigitalCredentialConfiguration.appGroup,
7 mobileID: mobileID
8 )
9
10 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:

Swift
1import MobileIDCore
2import MobileIDInPersonSharing
3
4@available(iOS 26.0, *)
5func registerEligibleCredentials(from mobileID: MobileID) async {
6 let credentials = mobileID.wallet.credentials()
7 .compactMap { $0 as? EnrolledCredential }
8
9 for credential in credentials {
10 do {
11 let isPresentable = credential.status == .active
12
13 guard isPresentable else {
14 try await credential.unregisterDigitalCredential()
15 continue
16 }
17
18 try await credential.registerDigitalCredential(
19 allowedAuthorities: .any
20 )
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,
  • credentialId as the platform document identifier,
  • the MSO validUntilDate as 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:

Swift
1// The provider may appear without enforcing a requester-signing requirement.
2try await credential.registerDigitalCredential(allowedAuthorities: .any)
3
4// Only requests chained to one of these subject key identifiers are eligible.
5try await credential.registerDigitalCredential(
6 allowedAuthorities: .only([
7 trustedAuthoritySubjectKeyIdentifier
8 ])
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 

Swift
1@available(iOS 26.0, *)
2func isRegistered(_ credential: EnrolledCredential) async throws -> Bool {
3 switch try await credential.digitalCredentialRegistrationStatus {
4 case .registered:
5 return true
6 case .notRegistered:
7 return false
8 }
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.

Swift
1import MobileIDInPersonSharing
2
3@available(iOS 26.0, *)
4func loadDigitalCredentials() throws -> [DigitalCredential] {
5 guard let context = try DigitalCredentialContext(
6 appGroup: DigitalCredentialConfiguration.appGroup
7 ) else {
8 return []
9 }
10
11 return context.credentials
12}

Each DigitalCredential exposes:

Property
Purpose
idPlatform document identifier; derived from credentialId.
typeMobile ID CredentialType.
datagroupEncoded .simple data group used to build the response.
attributesDisplay data for app-owned authorization UI.
validUntilMSO validity end date.
friendlyNamesLocalized credential display names.
isCancelledWhether the source credential was not active when the snapshot was built.
docTypesDocument types contained in the ISO data group.

Filter out cancelled and expired entries, then select a credential whose docTypes cover the parsed request:

Swift
1@available(iOS 26.0, *)
2func selectCredential(
3 from credentials: [DigitalCredential],
4 requestedDocumentTypes: Set<String>
5) -> DigitalCredential? {
6 credentials.first { credential in
7 !credential.isCancelled
8 && 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:

Swift
1import IdentityDocumentServicesUI
2import SwiftUI
3
4@main
5struct DocumentProviderExtension: IdentityDocumentProvider {
6 var body: some IdentityDocumentRequestScene {
7 ISO18013MobileDocumentRequestScene { context in
8 AuthorizationView(requestContext: context)
9 }
10 }
11
12 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:

Swift
1import IdentityDocumentServices
2import IdentityDocumentServicesUI
3import MobileIDInPersonSharing
4
5@available(iOS 26.0, *)
6func sendResponse(
7 for requestContext: ISO18013MobileDocumentRequestContext,
8 using credential: DigitalCredential
9) async throws {
10 try await requestContext.sendResponse { rawRequest in
11 let responseData = try await credential.accept(
12 rawRequest: rawRequest.requestData,
13 origin: requestContext.requestingWebsiteOrigin
14 )
15
16 return ISO18013MobileDocumentResponse(
17 responseData: responseData
18 )
19 }
20}

DigitalCredential.accept(rawRequest:origin:):

  1. validates and parses the Digital Credentials request,
  2. obtains the ISO 18013 digital-credential session information,
  3. builds a response from the stored issuer data group,
  4. creates device authentication with the shared credential key,
  5. 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:

Swift
1requestContext.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 issuedUpdate contextRegister if active, valid, and eligible.
Credential data or MSO updatedUpdate contextRegister again to refresh type/validity metadata.
Key rotatedUpdate contextRegistration can remain if metadata is unchanged.
Credential cancelledUpdate contextUnregister until reinstated.
Credential reinstatedUpdate contextRegister again.
Credential removed locallyUnregister first, remove from wallet, then update context.
Credential removed remotelyUpdate context and reconcile stale platform registrations by identifier.

Example local removal order:

Swift
1@available(iOS 26.0, *)
2func removeDigitalCredential(
3 _ credential: EnrolledCredential,
4 from mobileID: MobileID
5) 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 requestConfirm entitlement approval, document type declaration, successful registration, and current invalidation date.
Extension loads no credentialsConfirm the host app called DigitalCredentialContextProvider.update() and both targets use the same App Group.
Context update throwsConfirm every enrolled wallet entry has attributes, a parsable .simple data group, and an accessible authentication key.
Response signing cannot find the keyConfirm MobileIDFactory.create received the App Group before enrollment and the extension belongs to it.
Cancelled credential is still offered by iOSContext refresh does not remove platform registration; call unregisterDigitalCredential() or reconcile the registration store.
Registration reports unknown document typeConfirm the .simple data group contains a document and its type is listed in the entitlement.
Registration reports unknown validityConfirm the data group contains readable MSO validity information.
Request validation failsPass the exact rawRequest.requestData and unmodified requestingWebsiteOrigin.
  • Getting Started
  • Credential Wallet And Lifecycle
  • PIN And Liveness Verification
  • Theming, Branding, And Localization