Digital Credentials API issuance ships in Chrome 155

Published: October 23, 2026

In Chrome 155 and later, Digital Credentials API issuance is enabled by default, so you no longer need an origin trial token or a flag to use it. The Digital Credentials API is a transport layer that lets websites connect to wallet apps installed on the user's mobile device. With issuance, issuers can add new credentials (such as digital IDs) directly into a user's wallet app.

Background

The digital identity ecosystem has three parties: issuers, verifiers (also called relying parties), and holders (wallet apps). Users often add a digital ID to their wallet app through push provisioning, where the user starts issuance from the issuer's website or app. For example, a student on their university website (issuer) can save their digital student ID to their preferred wallet app (holder).

For this model to be successful, the issuer needs a secure and reliable way to invoke the wallet app to issue the ID. Before this API, each issuer had to build its own way to open the wallet apps it partnered with, usually with custom URI schemes. Issuers had to maintain a separate mechanism for each wallet app, and users had no consistent way to choose a wallet app.

How digital credential issuance works

To streamline this process, Chrome supports issuance with the W3C Digital Credentials API. This adds to the presentation capabilities (sharing requested, selective data from a digital credential with a verifier), which shipped in Chrome 141. When an issuer website calls navigator.credentials.create() with a digital member that contains an issuance request, Chrome passes the request to the operating system (on Android, Credential Manager). The operating system then shows its own interface where the user chooses a wallet app. The API works with industry-standard protocols such as OpenID for Verifiable Credential Issuance (OpenID4VCI). Issuance supports two flows: a same-device flow on Android, and a cross-device flow that starts on desktop and finishes on an Android device.

Same-device flow on Android

On Android, Chrome passes the issuance request to Android Credential Manager, which shows a list of compatible wallet apps on the device. The user selects a wallet and consents to adding the credential. The wallet app then gets the credential from the issuer using the issuance protocol, such as OpenID4VCI.

Cross-device flow on desktop

On desktop, Chrome displays a QR code. The user scans the code with their Android device and completes the flow on that device, as described in the previous section. The devices use Bluetooth to confirm that they are close to each other, so Chrome asks the user to turn on Bluetooth if it's off. Chrome uses the same cross-device mechanism for Digital Credentials API presentation.

Implement issuance using the API

To start the issuance flow, call navigator.credentials.create() and pass your issuance requests inside a digital member. Make this call from a click handler or another user gesture handler, because the call requires transient user activation. The following example uses an OpenID4VCI credential offer with the pre-authorized code flow:

// Call this function from a user gesture handler, for example, a button's
// click handler. The pre-authorized code is a short-lived, single-use code
// that your server generates for the signed-in user.
async function issueDigitalCredential(preAuthorizedCode) {
  // Check for Digital Credentials API and protocol support.
  if (!window.DigitalCredential ||
      !DigitalCredential.userAgentAllowsProtocol('openid4vci-v1')) {
    // Provide an alternative issuance method.
    showAlternativeIssuanceFlow();
    return;
  }

  // Construct a credential offer according to the OpenID4VCI
  // specification. This example uses the format that the
  // demo CMWallet supports.
  const credentialOffer = {
    credential_issuer: 'https://issuer.example',
    credential_configuration_ids: ['org.iso.18013.5.1.mDL'],
    grants: {
      'urn:ietf:params:oauth:grant-type:pre-authorized_code': {
        'pre-authorized_code': preAuthorizedCode
      }
    },
    // Metadata that the wallet uses to request and display the credential.
    credential_issuer_metadata: {
      credential_issuer: 'https://issuer.example',
      credential_endpoint: 'https://issuer.example/openid4vci/credential',
      nonce_endpoint: 'https://issuer.example/openid4vci/nonce',
      credential_configurations_supported: {
        'org.iso.18013.5.1.mDL': {
          format: 'mso_mdoc',
          doctype: 'org.iso.18013.5.1.mDL',
          // Bind the credential to a key that the wallet generates.
          cryptographic_binding_methods_supported: ['cose_key'],
          proof_types_supported: {
            jwt: { proof_signing_alg_values_supported: ['ES256'] }
          },
          credential_metadata: {
            display: [{ name: 'Mobile driving license', locale: 'en-US' }],
            // The claims that this credential contains. For a full list of
            // standard mDL claims, see the ISO/IEC 18013-5 specification.
            claims: [{
              path: ['org.iso.18013.5.1', 'family_name'],
              display: [{ name: 'Family name', locale: 'en-US' }]
            }
            // ... Add any other claims for your credential here.
            ]
          }
        }
      }
    },
    // Metadata of the authorization server that issues access tokens.
    authorization_server_metadata: {
      issuer: 'https://issuer.example',
      token_endpoint: 'https://issuer.example/openid4vci/token'
    }
  };

  try {
    const credential = await navigator.credentials.create({
      digital: {
        requests: [{
          // The protocol identifier for OpenID4VCI 1.0.
          protocol: 'openid4vci-v1',
          // Pass the offer as an object, not as a JSON string.
          data: credentialOffer
        }]
      }
    });

    // The wallet handled the request. credential.data is an object that
    // contains the wallet's protocol-specific response. It is not the issued
    // credential, and it does not necessarily mean that issuance is complete.
    // Your server knows when the wallet retrieves the credential from your
    // credential endpoint.
    // Implement your custom logic here as needed.
    showIssuanceStatus(credential);
  } catch (error) {
    switch (error.name) {
      case 'NotAllowedError':
        // The user canceled the flow, no wallet can handle the request, or
        // the call does not have transient user activation.
        break;
      case 'NotSupportedError':
        // The browser does not support Digital Credentials API issuance.
        // Fall back to your custom alternative flow logic.
        showAlternativeIssuanceFlow();
        break;
      case 'TypeError':
        // The request is malformed, for example, data is not an object.
        console.error('Invalid issuance request:', error);
        break;
      default:
        // For example, AbortError or NetworkError.
        console.error('Credential issuance failed:', error);
    }
  }
}

const addButton = document.querySelector('#add-to-wallet');
addButton.addEventListener('click', () => {
  // Your server adds a pre-authorized code for the signed-in user
  // to the button's data-pre-authorized-code attribute.
  issueDigitalCredential(addButton.dataset.preAuthorizedCode);
});

In this example, showAlternativeIssuanceFlow() and showIssuanceStatus() stand in for your own app logic.

To call create() from a cross-origin iframe, the embedding page must allow the digital-credentials-create permissions policy feature. Otherwise, the call rejects with a NotAllowedError:

<iframe src="https://issuer.example" allow="digital-credentials-create">
</iframe>

Changes since the origin trial

The API methods and parameters haven't changed since the origin trial. However, the following changes shipped between Chrome 143 and Chrome 155, and some of them might require you to update your code. The following table summarizes the changes, and the sections after it explain each one.

Area Origin trial (Chrome 143) Chrome 155
Availability Origin trial token or the chrome://flags/#web-identity-digital-credentials-creation flag Enabled by default
User activation Required, but not consumed Required and consumed by each create() call (Chrome 146)
Cancellation errors NetworkError NotAllowedError (Chrome 148)
Protocol identifiers Chrome passes any identifier to the operating system Unrecognized identifiers trigger a deprecation warning (Chrome 151)
Opaque origins Allowed Blocked with NotAllowedError (Chrome 151)
Automated testing No built-in support Virtual wallet in the Chrome DevTools Protocol (Chrome 152)

User activation is consumed

Starting from Chrome 146, a create() call with a digital member consumes the transient user activation, as the specification requires. During the origin trial, Chrome checked for user activation but didn't consume it.

As a result, each create() call needs its own user gesture, for example, a button click. Without user activation, the promise rejects with a NotAllowedError. After the call, window.open() and other APIs that require user activation can't use the same gesture.

Cancellation rejects with NotAllowedError

Starting in Chrome 148, the promise that create() returns rejects with a NotAllowedError when the user cancels the flow, for example, by dismissing the wallet selector on Android or closing the QR code dialog on desktop. The promise also rejects with a NotAllowedError when no wallet app can handle the request. During the origin trial, these cases were rejected with a NetworkError.

If your code checks for a NetworkError to detect cancellation, update it to check for a NotAllowedError instead.

Unrecognized protocol identifiers trigger a warning

The specification defines a single issuance protocol identifier, openid4vci-v1, for OpenID4VCI 1.0. Chrome 155 recognizes openid4vci-v1, and also openid4vci. Because openid4vci isn't part of the specification, we recommend that you use openid4vci-v1.

Chrome passes requests with any protocol identifier to the platform. However, starting from Chrome 151, DevTools shows a deprecation warning when a page calls create() with an unrecognized protocol identifier. Chrome intends to block requests with unrecognized protocol identifiers. For status, see the More specific protocol filtering in Digital Credential API entry.

To test this behavior, enable chrome://flags/#enable-experimental-web-platform-features. With this flag enabled, create() ignores requests with unrecognized protocol identifiers and rejects with a TypeError if no requests remain. Also, DigitalCredential.userAgentAllowsProtocol() returns true only for recognized protocols. Without the flag, it only checks that the identifier is well formed. So, don't rely on userAgentAllowsProtocol() alone for feature detection. Handle NotSupportedError as shown in the example.

Opaque origins are blocked

Starting from Chrome 151, create() rejects with a NotAllowedError when a document with an opaque origin calls it, such as a sandboxed iframe without the allow-same-origin token. During the origin trial, Chrome did not block these calls.

Test with a virtual wallet

Starting from Chrome 152, you can test your issuance flow without an Android device or a wallet app. The experimental DigitalCredentials.setVirtualWalletBehavior command in the Chrome DevTools Protocol (CDP) implements the virtual wallet that the specification defines for automated testing. The virtual wallet can respond with the data that you provide, decline requests as if the user canceled the flow, or leave requests pending to test timeouts and aborts.

The following Puppeteer test sets the virtual wallet to respond to openid4vci-v1 requests, then clicks the button that starts issuance:

const client = await page.createCDPSession();
await client.send('DigitalCredentials.setVirtualWalletBehavior', {
  action: 'respond',
  protocol: 'openid4vci-v1',
  response: {}
});

// Clicking the button provides the user activation that create() requires.
await page.click('#add-to-wallet');

Industry adoption

We see Digital Credentials API issuance adoption. For example, the German financial institution Sparkasse uses Digital Credentials API to let account holders save verified age credentials to Google Wallet. Account holders can then use these credentials to verify their age online and in person.

Try it out

Before you test the Digital Credentials API issuance, make sure that you have the following:

  • Update to Chrome 155 or later.
  • Install a supported wallet app on your Android device, for example, the demo CMWallet.
  • Bluetooth turned on for both devices (cross-device flow only).

To test the cross-device flow:

  1. On your computer, open the demo issuer site.
  2. Click the Add button.
  3. Scan the QR code with your Android device and follow the onscreen instructions.

To test the same-device flow, open the demo issuer site in Chrome on your Android device.

Developer resources

To start integrating Digital Credentials API issuance into your website, use the following resources:

Share your feedback

To report a bug or request a feature, file an issue in the Chromium bug tracker.