light-mode-image
Learn
DC API

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:

  1. Configure your XCode project to support the new app extension.
  2. Create and configure an Identity Document Provider extension.
  3. Initialize the SDK with support for the DC API enabled.
  4. 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

  1. In Xcode, select your app target → General.
  2. Set Minimum Deployments to iOS 26.0 (or later).

Step 2: Enable Keychain Sharing (required for SDK key access)

  1. Select your app target → Signing & Capabilities.
  2. Click + Capability → add Keychain Sharing.
  3. 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)

  1. In Signing & Capabilities, click + Capability → add App Groups.
  2. Create an App Group identifier (for example: group.com.yourcompany.yourapp).
  3. Select the same App Group for both the app and the extension.
  4. 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

  1. 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).
  2. 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

  1. Open your project and select the project (blue icon) in the Project Navigator.
  2. Under Targets, click the + button (or use File → New → Target…).
  3. Select iOS in the template chooser.
  4. Search for Identity Document Provider, choose it and click Next.
  5. 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:

DocumentProviderExtension.swift
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.

  1. Select the project (blue icon) in the Project Navigator.
  2. Under Targets, select your new Identity Document Provider target (the extension).
  3. Open the Signing & Capabilities tab.
  4. Click + Capability → Keychain Sharing.
  5. Add the same Keychain Group you configured on the main app target.
  6. Click + Capability → App Groups.
  7. 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:

Constants.swift
  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:

ContentView.swift
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:

  1. Build and run your app on the testing iOS device.
  2. On first initialization with dcConfiguration, iOS will prompt you to: Allow this app to be used for identity verification on websites.
  3. Approve the prompt.
  4. Navigate to the MATTR Labs remote presentation testing tool using a supported browser.
  5. Select Digital Credentials API from the Select Experience list.
  6. 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:

  1. Select your project (blue icon) in the Project Navigator.
  2. Under Targets, select your Identity Document Provider extension target.
  3. Open General → scroll to Frameworks, Libraries, and Embedded Content.
  4. Click + and add MobileCredentialHolderSDK.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.

  1. If you haven’t completed the Remote Presentation or Proximity Presentation tutorials, add the following file named PresentCredentialsView.swift to your project and make sure it’s included in the extension target membership:
PresentCredentialsView.swift
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) -> Void is used to display claim values.
  • sendCredentialAction: (String) -> Void is used to send a credential response to the verifier once the user selected a credential and provided consent by selecting the Send Response button.
  1. Select the Constants.swift file.
  2. Use the File Inspector to enable Target Membership for your app extension.
  3. Do the same for DocumentView.swift and PresentCredentialsViewModel.swift (and any related view models/types it depends on).

Step 3: Implement the extension handler (DocumentProviderExtension.swift)

  1. Replace any existing code in DocumentProviderExtension.swift with the following code, which will be the basis for handling DC API presentation requests:
DocumentProviderExtension.swift
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) {

    }
}
  1. Under // DC API - Step 3.2: Import MobileCredentialHolderSDK, import the SDK:
Importing the SDK in the extension
import MobileCredentialHolderSDK

This gives the extension access to MobileCredentialHolder, presentation sessions, and credential models.

  1. Under // DC API - Step 3.3: Store a reference to the SDK, add a holder property referencing the MobileCredentialHolder singleton:
Adding holder reference in the extension view model
let holder = MobileCredentialHolder.shared

You’ll use this reference to initialize the SDK, create a presentation session, and fetch credentials.

  1. Under // DC API - Step 3.4: Initialize the SDK for app extension, initialize the SDK using the shared App Group:
Initializing the SDK in the extension
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.

  1. Under // DC API - Step 3.5: Add properties to manage presentation state, add the properties used by the consent UI and the response flow:
Adding presentation state properties to the extension view model
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:

  • presentationSession holds the active DC presentation session created from the system request.
  • matchedCredentials are the credentials retrieved from storage that match the verifier’s request (used to show claim values in the UI).
  • matchedMetadata is the metadata about the matched credentials (used to show the credential in the UI before revealing claim values).
  • credentialRequest is the original request from the verifier (used to show what was requested and to create the presentation response).
  1. Add a new function under // DC API - Step 3.6: Create DC presentation session from context to convert the system request context into a DCPresentationSession and extract the request details:
Creating a DC presentation session from the system request context
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.

  1. Add a new function under // DC API - Step 3.7: Retrieve credential from storage to load a specific credential by ID when the user wants to reveal claim values:
Retrieving a credential from storage in the extension
@MainActor
func getCredential(id: String) {
    Task {
        do {
            let credential = try await holder.getCredential(credentialId: id)
            matchedCredentials.append(credential)
        } catch {
            print(error)
        }
    }
}
  1. Under // DC API - Step 3.8: Display the credential selection UI replace EmptyView() with the following code to show the PresentCredentialsView and pass the necessary data and actions:
Displaying the credential selection UI in the extension
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:)
    )
)
  • sendCredentialAction sends the selected credential back to the relying party using DCPresentationSession.sendResponse(...).
  • getCredentialAction loads a specific credential to display claim values before consent.
  1. Under // DC API - Step 3.9: Initialize the session when view appears, call createDCSession() when the view appears to set up the presentation session and populate the UI:
Initializing the DC presentation session when the view appears
.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

  1. Run the app and then close it (this updates the app on your testing device).
  2. Use a browser on your testing mobile device to navigate to the MATTR Labs remote presentation testing tool.
  3. Select Digital Credentials API from the Select Experience list.
  4. Select Request credentials.
  5. Select the credential you wish to send to the verifier from the list of available apps suggested by the operating system.
  6. Send the response.
  7. The application extension will close, and you should see a successful verification indication in the browser where you initiated the request.

Cross-device workflow

  1. Run the app and then close it (this updates the app on your testing device).
  2. Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
  3. Select Digital Credentials API from the Select Experience list.
  4. Select Request credentials.
  5. 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).
  6. Select the credential you wish to send to the verifier from the list of available apps suggested by the operating system.
  7. Send the response.
  8. The application extension will close.
  9. 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:

  1. Create the app extension to handle the DC API presentation requests.
  2. Initialize the SDK with support for the DC API enabled.
  3. Handle the presentation request in your app extension.

What's next?

How would you rate this page?

Last updated on

On this page