Skip to the content.

CHAPI for Digital Wallets

CHAPI integrates easily into digital wallet software, allowing your wallet to receive and present Verifiable Credentials to/from third party sites:

for Web Wallets


First, set up your wallet to register itself with the browser as a Credential Handler.

1. Import the CHAPI Polyfill into your wallet app

If you’re developing on Node.js, add the credential-handler-polyfill library to your project. You can also install the web-credential-handler helper library to simplify your code.

npm i credential-handler-polyfill@3.0.1
npm i web-credential-handler@2.0.1 

In your code, you can import and load the polyfill library as follows:

import * as CredentialHandlerPolyfill from 'credential-handler-polyfill';
import * as WebCredentialHandler from 'web-credential-handler';

await CredentialHandlerPolyfill.loadOnce();
console.log('Ready to work with credentials!');


2. Add a credential_handler to your app’s manifest.json

In order to register a credential handler, your web app must serve a “manifest.json” file from its root path (“/manifest.json”). This file must also be CORS-enabled. At a minimum, add the following credential_handler object:

  "credential_handler": {
    "url": "/wallet-worker.html",
    "enabledTypes": ["VerifiablePresentation"]


3. Allow your users to register their wallet’s credential handler with the browser polyfill

You can register a Credential Handler by calling the CredentialManager.requestPermission() API. This call will ensure that the individual using the browser explicitly confirms that they want to use the website as a credential handler. The example below uses the installHandler() helper method to perform this action:

  await WebCredentialHandler.installHandler();
  console.log('Wallet installed.');


Then, configure your wallet to respond to Credential Handler events.

Your wallet app’s existing functionality can be configured to respond to CHAPI events.

4. Setup Listeners for CHAPI Events

The activateHandler() function is a helper that sets up listeners for CHAPI get() and store() events.

    async get(event) {
        console.log('WCH: Received get() event:', event);
        return { type: 'redirect', url: '/wallet-ui-get.html' };
    async store(event) {
        console.log('WCH: Received store() event:', event);
        return { type: 'redirect', url: '/wallet-ui-store.html' };


5. Get Credentials Events

CHAPI supports the presentation of credentials via the navigator.credentials.get() API. CHAPI is agnostic to the presentation request query language and passes the query directly through to the credential handler. If you’ve configured an event listener, you can follow the example below to call the relevant code in your wallet whenever it receives a CHAPI get() request from a third-party website.

  async function handleGetEvent() {
    const event = await WebCredentialHandler.receiveCredentialEvent();

    console.log('Wallet processing get() event:', event);

    //Your wallet's code for responding to a request for a Verifiable Credential


When presenting credentials, the user is shown what they will be sharing and must provide explicit consent before the credentials are shared with the requesting party.

6. Store Credentials Events

CHAPI supports storing credentials via the API. If you’ve configured an event listener, you can follow the example below to call the relevant code in your wallet whenever it receives a CHAPI store() request from a third-party website.

async function handleStoreEvent() {
    const event = await WebCredentialHandler.receiveCredentialEvent();
    console.log('Store Credential Event:', event.type, event);

    //Your wallet's code for storing a Verifiable Credential


Storage of credentials prompts the individual using the browser to confirm that they want to store the credential in their digital wallet.

DID Authentication with CHAPI

This section is written from the perspective of web wallets. CHAPI provides a simple method for a 3rd party website to request an individual present their Decentralized Identifier (DID) and prove their identity. The individual selects a digital wallet to respond to this DID Authentication request.


1. The 3rd Party Site sends a DID Authentication Request

The individual interacts with a 3rd party website, triggering a request for DID Authentication. The site sends a Verifiable Presentation Request (VPR) using the CHAPI get() event.

An example VPR is shown below. Like the other CHAPI examples on this site, the VPR is wrapped in a web credential object - this ensures that it is passed to a Credential Handler in the individual’s browser.

"web": {
  "VerifiablePresentation": {
      "query": {
        "type": "DIDAuthentication"
      "challenge": "IME0WNG2MIOsYsPgezxAM", //randomly-generated challenge string (e.g.,a UUID, nanoid, or bnid)
      "domain": "" //URL of your web app (where the wallet will respond to the DID Auth request
  "recommendedHandlerOrigins": [

2. The Digital Wallet responds with a Verifiable Presentation

The individual selects a digital wallet, which responds to the CHAPI get() event. The example code below shows two functions, plus a third function that your wallet will need to create

async function formDidAuthResponse({challenge, domain}) {
    const dataType = 'VerifiablePresentation';

    //Add your code for signing a verifiable presentation
    didAuthPresentation = await signDidAuthPresentation({challenge, domain});

    //wrap the DID Auth presentation in a web credential
    const credentialType = 'VerifiablePresentation';
    const didAuthResponse = new WebCredential(
      credentialType, didAuthPresentation, {
      recommendedHandlerOrigins: []

    return didAuthResponse;

async function handleGetEvent() {
    const event = await WebCredentialHandler.receiveCredentialEvent();
    console.log('Get Credential Event:', event.type, event);

    const vp = event.credentialRequestOptions.web.VerifiablePresentation;
    const query = Array.isArray(vp.query) ? vp.query[0] : vp.query;

    const {type} = query.value;
    if(type ==='DIDAuthentication') {
      event.respondWith(formDidAuthResponse({challenge: vp.challenge, domain: vp.domain}))

Your wallet’s version of signDidAuthPresentation() should create a signed Verifiable Presentation with the holder equal to the user’s DID. The example below shows what this looks like with the Ed25519Signature2020 and 2018 signature suites, respectively.

const didAuthPresentation = {
    '@context': [
    type: ['VerifiablePresentation'],
    holder: 'did:key:z6MkeprvBw4RFHJPQEmtioq4xRrN6Tk8EBSJ37eBCBQNHRjZ',
    proof: {
        type: 'Ed25519Signature2020',
        created: '2022-11-09T22:04:18Z',
        verificationMethod: 'did:key:z6MkeprvBw4RFHJPQEmtioq4xRrN6Tk8EBSJ37eBCBQNHRjZ#z6MkeprvBw4RFHJPQEmtioq4xRrN6Tk8EBSJ37eBCBQNHRjZ',
        proofPurpose: 'authentication',
        challenge: 'qd4_rg4FvyYDUIuy-DmN9',
        domain: 'https://localhost:51443',
        proofValue: 'zinUxNo4eLvMRU7QaYwSKTKRkvYud7cDeh3B8zm3G1FLZGiSKjCXFgZiQTLKJmpLuatgpcqCTpRZBj4ETAsddcfe'

const didAuthPresentation = {
    "@context": [
    "type": "VerifiablePresentation",
    "holder": "did:v1:test:nym:z6MkjsQSCqdN4CGE6R9tKhETAEoPYdXci5v4tK2USAhWptpr",
    "proof": {
        "type": "Ed25519Signature2018",
        "created": "2022-10-28T20:24:27Z",
        "verificationMethod": "did:v1:test:nym:z6MkjsQSCqdN4CGE6R9tKhETAEoPYdXci5v4tK2USAhWptpr#z6MkjsQSCqdN4CGE6R9tKhETAEoPYdXci5v4tK2USAhWptpr",
        "proofPurpose": "authentication",
        "challenge": "IME0WNG2MIOsYsPgezxAM",
        "domain": "",
        "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..n7f3DZ4yNuH2ApE0dZy1gaLBTKEuGYHGsmycgWwKptZaNeKz2FKRAjzPeat3GQnJg1n_5Q6GU9bAql602m2tCg"

for Native Mobile Apps


To enable a native wallet to receive VCs via CHAPI, you’ll need to register the wallet as a share target with the mobile OS that is capable of receiving text files. See appropriate share target documentation for Android or iOS.