Skip to main content
This guide demonstrates how to issue verifiable credentials using both DIDComm and OpenID4VC protocols in Credo.

DIDComm Credential Issuance

The DIDComm approach uses the Issue Credential protocol to exchange credentials between connected agents.
1
Initialize the Issuer Agent
2
First, set up an agent with the necessary modules for credential issuance:
3
import {
  Agent,
  DidsModule,
} from '@credo-ts/core'
import {
  AnonCredsModule,
  AnonCredsDidCommCredentialFormatService,
  DidCommCredentialV2Protocol,
} from '@credo-ts/anoncreds'
import {
  DidCommModule,
  DidCommAutoAcceptCredential,
  DidCommHttpOutboundTransport,
} from '@credo-ts/didcomm'
import { AskarModule } from '@credo-ts/askar'
import { IndyVdrModule, IndyVdrAnonCredsRegistry } from '@credo-ts/indy-vdr'
import { agentDependencies, DidCommHttpInboundTransport } from '@credo-ts/node'
import { anoncreds } from '@hyperledger/anoncreds-nodejs'
import { indyVdr } from '@hyperledger/indy-vdr-nodejs'
import { askar } from '@openwallet-foundation/askar-nodejs'

const issuerAgent = new Agent({
  config: {
    label: 'Issuer Agent',
    walletConfig: {
      id: 'issuer-wallet',
      key: 'issuer-wallet-key',
    },
  },
  dependencies: agentDependencies,
  modules: {
    didcomm: new DidCommModule({
      endpoints: ['http://localhost:3001'],
      transports: {
        inbound: [new DidCommHttpInboundTransport({ port: 3001 })],
        outbound: [new DidCommHttpOutboundTransport()],
      },
      credentials: {
        autoAcceptCredentials: DidCommAutoAcceptCredential.ContentApproved,
        credentialProtocols: [
          new DidCommCredentialV2Protocol({
            credentialFormats: [new AnonCredsDidCommCredentialFormatService()],
          }),
        ],
      },
    }),
    anoncreds: new AnonCredsModule({
      registries: [new IndyVdrAnonCredsRegistry()],
      anoncreds,
    }),
    indyVdr: new IndyVdrModule({
      indyVdr,
      networks: [
        {
          genesisTransactions: '<genesis-transactions>',
          indyNamespace: 'bcovrin:test',
          isProduction: false,
          connectOnStartup: true,
        },
      ],
    }),
    dids: new DidsModule({
      resolvers: [],
      registrars: [],
    }),
    askar: new AskarModule({
      askar,
    }),
  },
})

await issuerAgent.initialize()
4
Register Schema and Credential Definition
5
Before issuing credentials, register a schema and credential definition on the ledger:
6
import { TypedArrayEncoder } from '@credo-ts/core'
import { transformPrivateKeyToPrivateJwk } from '@credo-ts/askar'

// Import or create a DID for the issuer
const { privateJwk } = transformPrivateKeyToPrivateJwk({
  type: {
    crv: 'Ed25519',
    kty: 'OKP',
  },
  privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'),
})

const { keyId } = await issuerAgent.kms.importKey({
  privateJwk,
})

const did = 'did:indy:bcovrin:test:2jEvRuKmfBJTRa7QowDpNN'

await issuerAgent.dids.import({
  did,
  overwrite: true,
  keys: [
    {
      didDocumentRelativeKeyId: '#verkey',
      kmsKeyId: keyId,
    },
  ],
})

// Register schema
const schemaResult = await issuerAgent.modules.anoncreds.registerSchema({
  schema: {
    name: 'UniversityDegree',
    version: '1.0.0',
    attrNames: ['name', 'degree', 'date'],
    issuerId: did,
  },
  options: {
    endorserMode: 'internal',
    endorserDid: did,
  },
})

if (schemaResult.schemaState.state !== 'finished') {
  throw new Error('Schema registration failed')
}

const schemaId = schemaResult.schemaState.schemaId

// Register credential definition
const credDefResult = await issuerAgent.modules.anoncreds.registerCredentialDefinition({
  credentialDefinition: {
    schemaId,
    issuerId: did,
    tag: 'latest',
  },
  options: {
    supportRevocation: false,
    endorserMode: 'internal',
    endorserDid: did,
  },
})

if (credDefResult.credentialDefinitionState.state !== 'finished') {
  throw new Error('Credential definition registration failed')
}

const credentialDefinitionId = credDefResult.credentialDefinitionState.credentialDefinitionId
7
Establish Connection with Holder
8
Create a connection with the credential holder:
9
// Create out-of-band invitation
const outOfBand = await issuerAgent.didcomm.oob.createInvitation()

const invitationUrl = outOfBand.outOfBandInvitation.toUrl({
  domain: 'http://localhost:3001',
})

console.log('Share this invitation URL with the holder:', invitationUrl)

// Wait for connection to be established
const [connectionRecord] = await issuerAgent.didcomm.connections.findAllByOutOfBandId(outOfBand.id)
await issuerAgent.didcomm.connections.returnWhenIsConnected(connectionRecord.id)
10
Issue the Credential
11
Send a credential offer to the connected holder:
12
const credentialExchangeRecord = await issuerAgent.didcomm.credentials.offerCredential({
  connectionId: connectionRecord.id,
  protocolVersion: 'v2',
  credentialFormats: {
    anoncreds: {
      attributes: [
        {
          name: 'name',
          value: 'Alice Smith',
        },
        {
          name: 'degree',
          value: 'Computer Science',
        },
        {
          name: 'date',
          value: '2024-01-15',
        },
      ],
      credentialDefinitionId,
    },
  },
})

console.log('Credential offer sent:', credentialExchangeRecord.id)
13
Monitor Credential Exchange
14
Listen for credential exchange events to track the issuance process:
15
import { DidCommCredentialEventTypes } from '@credo-ts/didcomm'

issuerAgent.events.on(DidCommCredentialEventTypes.DidCommCredentialStateChanged, (event) => {
  console.log('Credential state changed:', event.payload.credentialExchangeRecord.state)
  
  if (event.payload.credentialExchangeRecord.state === 'done') {
    console.log('Credential successfully issued!')
  }
})

OpenID4VC Credential Issuance

The OpenID4VCI approach uses modern OAuth 2.0 flows for credential issuance.
1
Set Up the OpenID4VC Issuer
2
import { Agent, Kms, ClaimFormat } from '@credo-ts/core'
import {
  OpenId4VcModule,
  OpenId4VciCredentialFormatProfile,
} from '@credo-ts/openid4vc'
import { AskarModule } from '@credo-ts/askar'
import { askar } from '@openwallet-foundation/askar-nodejs'
import express from 'express'

const app = express()
const ISSUER_HOST = 'http://localhost:2000'

const issuerAgent = new Agent({
  config: {},
  dependencies: agentDependencies,
  modules: {
    askar: new AskarModule({ 
      askar, 
      store: { id: 'issuer', key: 'issuer-key' } 
    }),
    kms: new Kms.KeyManagementModule({
      backends: [],
    }),
    openid4vc: new OpenId4VcModule({
      app,
      issuer: {
        baseUrl: `${ISSUER_HOST}/oid4vci`,
        credentialRequestToCredentialMapper: async ({
          holderBinding,
          credentialConfigurationId,
        }) => {
          // Map credential request to actual credential
          return {
            type: 'credentials',
            format: ClaimFormat.SdJwtDc,
            credentials: holderBinding.keys.map((binding) => ({
              payload: {
                vct: 'UniversityDegreeCredential',
                university: 'Innsbruck University',
                degree: 'Bachelor of Science',
                name: 'Alice Smith',
              },
              holder: binding,
              issuer: {
                method: 'did',
                didUrl: `${issuerDidKey.did}#${issuerDidKey.publicJwk.fingerprint}`,
              },
              disclosureFrame: { _sd: ['university', 'degree', 'name'] },
            })),
          }
        },
      },
    }),
  },
})

await issuerAgent.initialize()
app.listen(2000)
3
Define Credential Configurations
4
Specify the types of credentials the issuer can issue:
5
const credentialConfigurationsSupported = {
  'UniversityDegreeCredential-sdjwt': {
    format: OpenId4VciCredentialFormatProfile.SdJwtVc,
    vct: 'UniversityDegreeCredential',
    scope: 'openid4vc:credential:UniversityDegree',
    cryptographic_binding_methods_supported: ['jwk', 'did:key'],
    credential_signing_alg_values_supported: ['ES256', 'EdDSA'],
    proof_types_supported: {
      jwt: {
        proof_signing_alg_values_supported: ['ES256', 'EdDSA'],
      },
    },
  },
}

const issuerRecord = await issuerAgent.modules.openid4vc.issuer.createIssuer({
  issuerId: 'university-issuer',
  credentialConfigurationsSupported,
  authorizationServerConfigs: [
    {
      type: 'direct',
      issuer: ISSUER_HOST,
      clientAuthentication: {
        clientId: 'holder-client',
        clientSecret: 'holder-secret',
      },
    },
  ],
})
6
Create a Credential Offer
7
Generate a credential offer for the holder:
8
const { credentialOffer, issuanceSession } = 
  await issuerAgent.modules.openid4vc.issuer.createCredentialOffer({
    issuerId: issuerRecord.issuerId,
    credentialConfigurationIds: ['UniversityDegreeCredential-sdjwt'],
    // Pre-authorized flow (no user interaction needed)
    preAuthorizedCodeFlowConfig: {
      authorizationServerUrl: ISSUER_HOST,
      txCode: {
        input_mode: 'numeric',
        length: 4,
        description: 'Please enter the PIN provided',
      },
    },
  })

const credentialOfferUri = credentialOffer.credentialOfferRequestWithBaseUrl
console.log('Share this credential offer URI with the holder:', credentialOfferUri)
console.log('PIN code:', issuanceSession.preAuthorizedCode)
9
Alternative: Authorization Code Flow
10
For flows requiring user authorization:
11
const { credentialOffer, issuanceSession } = 
  await issuerAgent.modules.openid4vc.issuer.createCredentialOffer({
    issuerId: issuerRecord.issuerId,
    credentialConfigurationIds: ['UniversityDegreeCredential-sdjwt'],
    // Authorization code flow (requires user interaction)
    authorizationCodeFlowConfig: {
      authorizationServerUrl: 'https://oauth-provider.example.com',
      issuerState: 'random-state-value',
    },
  })

console.log('Credential offer URI:', credentialOffer.credentialOfferRequestWithBaseUrl)

Next Steps

Build docs developers (and LLMs) love