How to build an iOS application that can present a credential via the DC API
Digital Credentials (DC) API support is currently offered as a tech preview. The DC API specification itself is still under active development in the W3C Web Incubator CG, and platform implementations continue to evolve. As such, functionality may be limited, may not work in all scenarios, and could change or break without prior notice as browsers and operating systems update their implementations.
Overview
This guide demonstrates how to use the iOS Holder SDK to build an iOS application that can present a claimed mDoc to a verifier remotely via the DC API, as per Annex C of ISO/IEC 18013-7:2025.
Prerequisites
Application base
This guide builds on the knowledge gained in the Claim a credential and Remote presentation tutorials.
It is recommended to complete those tutorials first, then return here to add support for the DC API workflow.
Testing Devices
- Mobile device:
- iOS device running iOS 26.0 or later.
- The device must have a holder application with claimed credentials set up as per the Claim a credential tutorial and support for the DC API enabled as per the instructions in this guide.
- Desktop device with a web browser that supports the DC API:
- Chrome or a Chromium based browser v138 or later.
- Safari v26 or later.
Development environment
- Xcode 26.0.0 or later.
How it works
The DC API is a browser standard that enables web applications to request and verify digital credentials directly from compatible wallet applications on the user's device. This provides a seamless user experience where credential presentation happens entirely within the browser context.
From a holder application point of view, there are several changes required to support remote presentations via the DC API compared to the standard OID4VP remote presentation workflow:
- The holder application must register itself as a credential provider with the iOS operating system, allowing it to be automatically discovered when websites request credentials.
- Each claimed credential must be registered individually with the operating system to make it available when a website requests that specific credential type.
- When a user selects to present a credential from the holder application, the application isn't invoked directly. Instead, an app extension handles the interaction with the user, retrieving the presentation request and managing the credential presentation process.
Adjusting your mobile application to support remote presentations via the DC API
The Holder SDK handles most of the complexity of the DC API workflow internally. You will have to make the following adjustments to enable it in your application:
- Configure your XCode project to support the new app extension.
- Create and configure an Identity Document Provider extension.
- Initialize the SDK with support for the DC API enabled.
- Handle the presentation request.
Configure your XCode project to support the new app extension
On iOS, DC API presentation requests are handled by an app extension rather than your main application. The app extension is a separate target within your Xcode project that provides a lightweight UI for credential selection and user consent.
First, configure your Xcode project so the main app can host an Identity Document Provider extension and share secure data with it.
Step 1: Set the minimum deployment target
- In Xcode, select your app target → General.
- Set Minimum Deployments to iOS 26.0 (or later).
Step 2: Enable Keychain Sharing (required for SDK key access)
- Select your app target → Signing & Capabilities.
- Click + Capability → add Keychain Sharing.
- Add a Keychain Group and make sure you use the same Keychain Group for both the app and the extension.
This is required because the extension must be able to access keys and secrets the SDK stores in the Keychain.
Step 3: Enable an App Group entitlement (required for shared storage)
- In Signing & Capabilities, click + Capability → add App Groups.
- Create an App Group identifier (for example: group.com.yourcompany.yourapp).
- Select the same App Group for both the app and the extension.
- This allows the app and extension to share data via the App Group container (for example, configuration and supporting state).
Step 4: Register the app as a Digital Credentials provider
- In Signing & Capabilities, add the entitlement Digital Credentials API - Mobile Document Provider (This entitlement is only available to apps signed by an Apple Developer Program team. It may not appear for individual accounts).
- Select all document types your holder will support (or select all during development).
This entitlement is required before iOS will allow your app (via its extension) to provide mobile documents using Apple’s identity document services.
Create and configure an Identity Document Provider extension
Next you will create the app extension that iOS invokes to handle mobile document requests. You’ll also configure the extension’s entitlements so it can share Keychain items and App Group storage with the main app (which is essential for accessing the SDK’s keys and shared configuration).
Step 1: Add the Identity Document Provider target in Xcode
- Open your project and select the project (blue icon) in the Project Navigator.
- Under Targets, click the + button (or use File → New → Target…).
- Select iOS in the template chooser.
- Search for Identity Document Provider, choose it and click Next.
- Enter the new target details (name, bundle identifier suffix, etc.), then click Finish.
After Xcode creates the target, it typically opens (or navigates you to) DocumentProviderExtension.swift. You should see a scaffold similar to:
import ExtensionKit
import IdentityDocumentServicesUI
import SwiftUI
@main
struct DocumentProviderExtension: IdentityDocumentProvider {
var body: some IdentityDocumentRequestScene {
ISO18013MobileDocumentRequestScene { context in
// Insert your view here
Text("Hello, world!")
}
}
func performRegistrationUpdates() async {
}
}Step 2: Configure Signing & Capabilities for the extension target
Now you’ll add the same shared entitlements to the extension target so it can access the same Keychain items and App Group container as the main app.
- Select the project (blue icon) in the Project Navigator.
- Under Targets, select your new Identity Document Provider target (the extension).
- Open the Signing & Capabilities tab.
- Click + Capability → Keychain Sharing.
- Add the same Keychain Group you configured on the main app target.
- Click + Capability → App Groups.
- Select the same App Group you configured on the main app target.
These values must match exactly between the app and the extension. If they don’t, the extension won’t be able to read the SDK’s stored keys or shared data.
Initialize the SDK with support for the DC API enabled
In this step, you’ll:
- Define the shared App Group identifier in code (so it can be reused consistently).
- Initialize the SDK with a
DCConfiguration. - Register your stored credentials with the Digital Credentials API, enabling your extension to respond to mdoc presentation requests from Safari and other supported browsers.
This is the final wiring step that connects: Main App → Shared Storage → SDK → Digital Credentials API → App Extension
Step 1: Define the shared App Group constant
Open Constants.swift in your main application target and add the shared App Group identifier:
static let appGroup = "group.com.mattr.identityprovider"The value must exactly match the App Group you configured for both the main app and the extension in the previous step.
Keeping this in Constants.swift ensures you avoid mismatches across targets.
Step 2: Initialize the SDK with DC API support enabled
Open ContentView.swift in your main app target and update the SDK initialization to include dcConfiguration:
try mobileCredentialHolder.initialize(
userAuthenticationConfiguration: UserAuthenticationConfiguration(
userAuthenticationBehavior: .onDeviceKeyAccess
),
credentialIssuanceConfiguration: CredentialIssuanceConfiguration(
redirectUri: Constants.redirectUri,
autoTrustMobileCredentialIaca: true
),
dcConfiguration: DCConfiguration(
appGroup: Constants.appGroup,
supportedDocTypes: [.euav, .eudi, .jpMnc, .mDL, .photoid]
)
)When you initialize the SDK with the dcConfiguration parameter:
- The SDK registers all existing credentials with the DC API.
- Any future credentials issued to the holder will also be registered automatically.
- iOS recognizes your app extension as a valid provider for the configured document types.
- The extension becomes available to handle DC API presentation requests from supported browsers.
This ensures your holder integrates seamlessly with the system-level DC API framework.
Step 3: Verify your configuration
To confirm everything is set up correctly:
- Build and run your app on the testing iOS device.
- On first initialization with
dcConfiguration, iOS will prompt you to: Allow this app to be used for identity verification on websites. - Approve the prompt.
- Navigate to the MATTR Labs remote presentation testing tool using a supported browser.
- Select Digital Credentials API from the Select Experience list.
- Select Request credentials.
If everything is configured correctly:
- The browser initiates a Digital Credentials request.
- iOS launches your Identity Document Provider extension.
- You’ll see the extension UI open (currently showing the placeholder view).
At this point, your end-to-end integration is complete. Your holder app is now registered as a Digital Credentials provider and ready to respond to verification requests.
Next, you’ll implement the logic to handle the presentation request and allow the user to select and present a credential.
Handle the presentation request in the app extension
In this step, you’ll wire your Identity Document Provider extension into the Holder SDK so it can:
- Read credentials from the shared app container (via the App Group).
- Create a Digital Credentials presentation session from the system request context.
- Show a consent screen where the user selects which credential to share.
- Send the response back to the verifier.
Step 1: Add the SDK dependency to the extension target
To import the SDK in the extension, add MobileCredentialHolderSDK.xcframework as a dependency:
- Select your project (blue icon) in the Project Navigator.
- Under Targets, select your Identity Document Provider extension target.
- Open General → scroll to Frameworks, Libraries, and Embedded Content.
- Click
+and addMobileCredentialHolderSDK.xcframework(or add it via Package Dependencies if your project uses Swift Package Manager).
Step 2: Add required shared source files to the extension target
The extension must compile with the same shared identifiers and UI helpers used by the main app.
- If you haven’t completed the Remote Presentation or Proximity Presentation tutorials, add the following file named
PresentCredentialsView.swiftto your project and make sure it’s included in the extension target membership:
import MobileCredentialHolderSDK
import SwiftUI
import Combine
struct PresentCredentialsView: View {
@ObservedObject var viewModel: PresentCredentialsViewModel
@State var selectedID: String?
init(viewModel: PresentCredentialsViewModel) {
self.viewModel = viewModel
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text("Requested Documents")
.font(.headline)
.padding(.leading)
ForEach(viewModel.requestedDocuments, id: \.docType) { requestedDocument in
DocumentView(viewModel: DocumentViewModel(from: requestedDocument))
}
Text("Matched Credentials")
.font(.headline)
.padding(.leading)
ForEach(viewModel.matchedMetadata, id: \.id) { matchedMetadata in
VStack(alignment: .leading, spacing: 10) {
if let matchedCredential = viewModel.matchedMobileCredential(id: matchedMetadata.id) {
DocumentView(viewModel: DocumentViewModel(from: matchedCredential))
.padding(.vertical)
.background(selectedID == matchedMetadata.id ? Color.blue.opacity(0.2) : Color.clear)
.onTapGesture {
guard selectedID != matchedMetadata.id else {
selectedID = nil
return
}
selectedID = matchedMetadata.id
}
Button("Hide claim values") {
viewModel.matchedCredentials.removeAll(where: { $0.id == matchedMetadata.id })
}
.frame(maxWidth: .infinity, alignment: .center)
} else {
DocumentView(viewModel: DocumentViewModel(from: matchedMetadata))
.padding(.vertical)
.background(selectedID == matchedMetadata.id ? Color.blue.opacity(0.2) : Color.clear)
.onTapGesture {
guard selectedID != matchedMetadata.id else {
selectedID = nil
return
}
selectedID = matchedMetadata.id
}
Button("Show claim values") {
viewModel.getCredentialAction(matchedMetadata.id)
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
}
}
}
if selectedID != nil {
Button("Send Response") {
viewModel.sendCredentialAction(selectedID!)
}
.buttonStyle(.borderedProminent)
.clipShape(Capsule())
.frame(maxWidth: .infinity, alignment: .center)
}
}
}
// MARK: PresentCredentialsViewModel
class PresentCredentialsViewModel: ObservableObject {
@Binding var requestedDocuments: [MobileCredentialRequest]
@Binding var matchedCredentials: [MobileCredential]
@Binding var matchedMetadata: [MobileCredentialMetadata]
var getCredentialAction: (String) -> Void
var sendCredentialAction: (String) -> Void
init(
requestedDocuments: Binding<[MobileCredentialRequest]>,
matchedCredentials: Binding<[MobileCredential]>,
matchedMetadata: Binding<[MobileCredentialMetadata]>,
sendCredentialAction: @escaping (String) -> Void,
getCredentialAction: @escaping (String) -> Void
) {
self._requestedDocuments = requestedDocuments
self._matchedCredentials = matchedCredentials
self._matchedMetadata = matchedMetadata
self.sendCredentialAction = sendCredentialAction
self.getCredentialAction = getCredentialAction
}
func matchedMobileCredential(id: String) -> MobileCredential? {
matchedCredentials.first(where: { $0.id == id })
}
}This view:
- Shows what the verifier requested.
- Lists credentials that match the request.
- Lets the user reveal claim values (optional) and consent by tapping Send Response.
The PresentCredentialsViewModel object is used to reference values from a credential request. It takes two closures in its initializer:
getCredentialAction: (String) -> Voidis used to display claim values.sendCredentialAction: (String) -> Voidis used to send a credential response to the verifier once the user selected a credential and provided consent by selecting the Send Response button.
- Select the
Constants.swiftfile. - Use the File Inspector to enable Target Membership for your app extension.
- Do the same for
DocumentView.swiftandPresentCredentialsViewModel.swift(and any related view models/types it depends on).
Step 3: Implement the extension handler (DocumentProviderExtension.swift)
- Replace any existing code in
DocumentProviderExtension.swiftwith the following code, which will be the basis for handling DC API presentation requests:
import ExtensionKit
import IdentityDocumentServicesUI
import SwiftUI
// DC API - Step 3.2: Import MobileCredentialHolderSDK
@main
struct DocumentProviderExtension: IdentityDocumentProvider {
var body: some IdentityDocumentRequestScene {
ISO18013MobileDocumentRequestScene { context in
DocumentProviderView(context: context)
}
}
/// This method is required for conformance to IdentityDocumentProvider.
/// However, the SDK manages all credential registrations automatically,
/// so no implementation is needed here.
func performRegistrationUpdates() async { }
}
struct DocumentProviderView: View {
@State private var viewModel: ExtensionViewModel
init(context: ISO18013MobileDocumentRequestContext) {
self._viewModel = State(initialValue: ExtensionViewModel(context: context))
}
var body: some View {
presentCredentialsView
// DC API - Step 3.9: Initialize the session when view appears
}
var presentCredentialsView: some View {
// DC API - Step 3.8: Display the credential selection UI
EmptyView()
}
}
@Observable
final class ExtensionViewModel {
// DC API - Step 3.3: Store a reference to the SDK
let context: ISO18013MobileDocumentRequestContext
// DC API - Step 3.5: Add properties to manage presentation state
init(context: ISO18013MobileDocumentRequestContext) {
self.context = context
// DC API - Step 3.4: Initialize the SDK for app extension
}
// DC API - Step 3.6: Create DC presentation session from context
func createDCSession() {
}
// DC API - Step 3.7: Retrieve credential from storage
@MainActor
func getCredential(id: String) {
}
}- Under
// DC API - Step 3.2: Import MobileCredentialHolderSDK, import the SDK:
import MobileCredentialHolderSDKThis gives the extension access to MobileCredentialHolder, presentation sessions, and credential models.
- Under
// DC API - Step 3.3: Store a reference to the SDK, add aholderproperty referencing theMobileCredentialHoldersingleton:
let holder = MobileCredentialHolder.sharedYou’ll use this reference to initialize the SDK, create a presentation session, and fetch credentials.
- Under
// DC API - Step 3.4: Initialize the SDK for app extension, initialize the SDK using the shared App Group:
do {
try holder.initializeAppExtension(appGroup: Constants.appGroup)
} catch {
print(error.localizedDescription)
}Constants.appGroup must be the same App Group used in the main app’s DCConfiguration. This is what allows the extension to read the same stored credentials and supporting data.
- Under
// DC API - Step 3.5: Add properties to manage presentation state, add the properties used by the consent UI and the response flow:
var presentationSession: DCPresentationSession?
var matchedCredentials: [MobileCredential] = []
var matchedMetadata: [MobileCredentialMetadata] = []
var credentialRequest: [MobileCredentialRequest] = []These values drive what the extension displays (requested data + matching credentials) and what it can send back to the verifier:
presentationSessionholds the active DC presentation session created from the system request.matchedCredentialsare the credentials retrieved from storage that match the verifier’s request (used to show claim values in the UI).matchedMetadatais the metadata about the matched credentials (used to show the credential in the UI before revealing claim values).credentialRequestis the original request from the verifier (used to show what was requested and to create the presentation response).
- Add a new function under
// DC API - Step 3.6: Create DC presentation session from contextto convert the system request context into aDCPresentationSessionand extract the request details:
func createDCSession() {
presentationSession = try? holder.createDcPresentationSession(from: context)
matchedMetadata = presentationSession?.request
.flatMap { $0.matchedCredentialsMetadata }
.compactMap { $0 } ?? []
credentialRequest = presentationSession?.request
.compactMap { $0.credentialRequest }
.compactMap { $0 } ?? []
}This extracts what the verifier requested (credentialRequest) and which stored credentials match it (matchedMetadata), then uses these values to populate the consent screen.
- Add a new function under
// DC API - Step 3.7: Retrieve credential from storageto load a specific credential by ID when the user wants to reveal claim values:
@MainActor
func getCredential(id: String) {
Task {
do {
let credential = try await holder.getCredential(credentialId: id)
matchedCredentials.append(credential)
} catch {
print(error)
}
}
}- Under
// DC API - Step 3.8: Display the credential selection UIreplaceEmptyView()with the following code to show thePresentCredentialsViewand pass the necessary data and actions:
PresentCredentialsView(
viewModel: PresentCredentialsViewModel(
requestedDocuments: $viewModel.credentialRequest,
matchedCredentials: $viewModel.matchedCredentials,
matchedMetadata: $viewModel.matchedMetadata,
sendCredentialAction: { credentialID in
Task {
try? await viewModel.presentationSession?.sendResponse(credentialIDs: [credentialID])
}
},
getCredentialAction: viewModel.getCredential(id:)
)
)sendCredentialActionsends the selected credential back to the relying party using DCPresentationSession.sendResponse(...).getCredentialActionloads a specific credential to display claim values before consent.
- Under
// DC API - Step 3.9: Initialize the session when view appears, callcreateDCSession()when the view appears to set up the presentation session and populate the UI:
.task {
viewModel.createDCSession()
}This ensures the session and request metadata are created as soon as the extension UI is shown, so the consent screen can render immediately.
Test the application
Let's test that the application is working as expected in both workflows.
Same-device workflow
- Run the app and then close it (this updates the app on your testing device).
- Use a browser on your testing mobile device to navigate to the MATTR Labs remote presentation testing tool.
- Select Digital Credentials API from the Select Experience list.
- Select Request credentials.
- Select the credential you wish to send to the verifier from the list of available apps suggested by the operating system.
- Send the response.
- The application extension will close, and you should see a successful verification indication in the browser where you initiated the request.
Cross-device workflow
- Run the app and then close it (this updates the app on your testing device).
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select Digital Credentials API from the Select Experience list.
- Select Request credentials.
- Open the camera on your testing iOS device and scan the QR code.
(When using Safari and an iOS device with the same Apple ID, the request is automatically transferred to the mobile device). - Select the credential you wish to send to the verifier from the list of available apps suggested by the operating system.
- Send the response.
- The application extension will close.
- Back on your desktop browser, you should see a successful verification indication.
Summary
You have just used the iOS Holder SDK to build an iOS application that can present a claimed mDoc to a verifier remotely via the DC API, as per Annex C of ISO/IEC 18013-7:2025.
This was achieved by making the following adjustments to your application:
- Create the app extension to handle the DC API presentation requests.
- Initialize the SDK with support for the DC API enabled.
- Handle the presentation request in your app extension.
What's next?
- You can build a web application that will interact with your wallet application via an online presentation workflow using the DC API.
- You can build additional capabilities into your new application:
- Present a claimed mDoc for verification via a proximity presentation workflow.
- You can check out the iOS Holder SDK reference documentation for more details on the available functions and classes.
How would you rate this page?
Last updated on