Core Concepts / Beginner / 10 min

Verifiable Credentials

Cryptographic claims anyone can verify—no callback to the issuer required

The Problem Verifiable Credentials Solve

Today’s credentials require the verifier to call back to the issuer:

  • Diploma — Employer calls university: “Did Alice graduate?”
  • Employment verification — Landlord calls company: “Does Bob work there?”
  • Agent authorization — Partner calls your system: “Is Agent-42 allowed to do this?”

Problems:

  • Privacy leak — Issuer learns who’s verifying and when
  • Dependency — Verifier can’t verify if issuer’s system is down
  • Latency — Real-time verification requires API calls
  • Revocation checking — Must check status with issuer every time

Verifiable Credentials (VCs) solve this: cryptographically signed claims that anyone can verify without calling the issuer.


What Is a Verifiable Credential?

A VC is a digitally signed claim issued by one party (issuer) about another party (holder).

Example: University issues diploma to Alice

{
  "@context": ["https://www.w3.org/2018/credentials/v1"],
  "type": ["VerifiableCredential", "UniversityDegreeCredential"],
  "issuer": "did:webvh:stanford.edu",
  "issuanceDate": "2024-06-15T00:00:00Z",
  "credentialSubject": {
    "id": "did:webvh:alice.com",
    "degree": {
      "type": "BachelorDegree",
      "name": "Bachelor of Science in Computer Science"
    }
  },
  "proof": {
    "type": "Ed25519Signature2020",
    "created": "2024-06-15T00:00:00Z",
    "proofPurpose": "assertionMethod",
    "verificationMethod": "did:webvh:stanford.edu#key-1",
    "proofValue": "z3FXQi...signature..."
  }
}

Key properties:

  1. Issuer — Who made the claim (Stanford)
  2. Holder — Who the claim is about (Alice)
  3. Claims — What’s being asserted (degree earned)
  4. Proof — Cryptographic signature from issuer
  5. Verifiable — Anyone can check the signature without calling Stanford

How Verifiable Credentials Work

1. Issuance

Issuer signs the credential:

// Issuer (Stanford) creates credential for Alice
const credential = {
  issuer: 'did:webvh:stanford.edu',
  credentialSubject: {
    id: 'did:webvh:alice.com',
    degree: { type: 'BachelorDegree', name: 'Computer Science' }
  },
  issuanceDate: new Date().toISOString()
}

// Sign with issuer's private key
const signedVC = await sign(credential, issuerPrivateKey)

// Alice stores it (in wallet, database, etc.)

2. Presentation

Holder shares the credential:

// Alice presents the VC to an employer
const presentation = {
  type: 'VerifiablePresentation',
  holder: 'did:webvh:alice.com',
  verifiableCredential: [signedVC],
  proof: { /* Alice's signature proving she controls the DID */ }
}

3. Verification

Verifier checks the credential:

// Employer verifies:
// 1. Issuer's signature is valid
const issuerDID = presentation.verifiableCredential[0].issuer
const issuerKey = await resolve(issuerDID) // Get Stanford's public key
const isValidSignature = await verify(signedVC, issuerKey)

// 2. Holder controls the DID (Alice signed the presentation)
const holderKey = await resolve(presentation.holder)
const isValidPresentation = await verify(presentation.proof, holderKey)

// 3. Credential hasn't been revoked (optional, check status list)
const isRevoked = await checkRevocation(signedVC)

// ✅ All checks pass — credential is valid

No callback to Stanford required!


Verifiable Credentials vs Traditional Credentials

AspectTraditional (e.g., API call)Verifiable Credential
VerificationCall issuer’s APIVerify signature locally
PrivacyIssuer sees every verificationIssuer sees nothing after issuance
AvailabilityFails if issuer is downWorks offline
LatencyNetwork round-tripInstant (local verification)
Selective disclosureAll-or-nothingShare only what’s needed
RevocationCheck with issuer every timeCheck status list (cacheable)

Real-World Use Cases

1. Agent Authorization Mandate

Problem: Bank A’s agent needs to prove it’s authorized to trade. Bank B must verify in real-time.

Solution: Bank A issues a VC (mandate) to the agent. Bank B verifies the signature without calling Bank A.

{
  "type": ["VerifiableCredential", "TradingMandate"],
  "issuer": "did:webvh:bankA.com",
  "credentialSubject": {
    "id": "did:webvh:bankA.com:agent-42",
    "mandate": {
      "authorizedBy": "did:webvh:bankA.com:trader-alice",
      "permissions": ["execute-trades"],
      "expiry": "2026-12-31"
    }
  },
  "proof": { "proofValue": "z3FXQi..." }
}

Bank B’s verification:

// 1. Check Bank A's signature (is this really from Bank A?)
const isValid = await verifyCredential(mandate)

// 2. Check expiry (is it still valid?)
const isExpired = new Date() > new Date(mandate.credentialSubject.mandate.expiry)

// 3. Check revocation status (did Bank A revoke it?)
const isRevoked = await checkRevocation(mandate)

// ✅ Verified—no API call to Bank A

2. PHI Access Authorization

Problem: AI agent needs to prove a case worker authorized PHI access—HIPAA requires proof.

Solution: Issue a VC linking agent to case worker. Audit log captures the VC—proof is cryptographic, not just a log entry.

{
  "type": ["VerifiableCredential", "PHIAccessAuthorization"],
  "issuer": "did:webvh:hospital.com",
  "credentialSubject": {
    "id": "did:webvh:hospital.com:agent-99",
    "authorization": {
      "authorizedBy": "did:webvh:hospital.com:caseworker-bob",
      "patient": "patient-12345",
      "purpose": "treatment-review",
      "expiry": "2026-07-22T00:00:00Z"
    }
  },
  "proof": { "proofValue": "..." }
}

Audit trail:

// Log entry
{
  timestamp: '2026-07-21T19:30:00Z',
  action: 'access-PHI',
  agent: 'did:webvh:hospital.com:agent-99',
  authorization: vcAsJson, // The full VC
  verifiedBy: 'gateway-node-5'
}

// Auditor verifies the VC signature months later—still valid

3. Selective Disclosure (Age Verification)

Problem: Prove you’re over 21 without revealing your birthdate.

Solution: VC with BBS+ signatures allows proving properties without revealing the full claim.

// Issued VC (full claims)
const vc = {
  credentialSubject: {
    name: 'Alice',
    birthdate: '1995-03-15',
    address: '123 Main St'
  }
}

// Selective disclosure—prove age > 21 without revealing birthdate
const derivedVC = await deriveCredential(vc, {
  reveal: ['over21: true'], // Only reveal this
  hide: ['name', 'birthdate', 'address'] // Hide everything else
})

// Verifier sees:
{
  credentialSubject: {
    over21: true // Proven cryptographically, no other data revealed
  }
}

Revocation: Status List 2021

Problem: How do you revoke a VC that’s already out in the world?

Solution: Status List 2021 — a bitstring hosted by the issuer. Each VC points to a bit:

{
  "credentialStatus": {
    "id": "https://issuer.com/status/42#94567",
    "type": "StatusList2021Entry",
    "statusPurpose": "revocation",
    "statusListIndex": "94567",
    "statusListCredential": "https://issuer.com/status/42"
  }
}

Verification:

// 1. Fetch the status list (cacheable)
const statusList = await fetch('https://issuer.com/status/42')

// 2. Check bit 94567
const isRevoked = statusList.bitstring[94567] === 1

// ✅ If 0 → valid, if 1 → revoked

Privacy: The verifier learns “is this credential revoked?” but the issuer doesn’t see who checked.


Verifiable Credentials in the Affinidi Stack

Elements Services issues VCs:

const vc = await elements.issueCredential({
  holder: 'did:webvh:agent-42.com',
  type: 'TradingMandate',
  claims: {
    authorizedBy: 'trader-alice',
    permissions: ['execute-trades'],
    expiry: '2026-12-31'
  }
})

Agent Gateway verifies VCs before allowing actions:

// Agent presents VC with request
const request = {
  action: 'execute-trade',
  credential: vcAsJson
}

// Gateway verifies
const isValid = await agentGateway.verifyCredential(request.credential)
if (!isValid) throw new Error('Unauthorized')

// Proceeds if valid

Trust Registry stores revocation status:

// Check if VC is revoked
const isRevoked = await trustRegistry.checkRevocation({
  credentialId: 'urn:uuid:abc-123',
  statusListUrl: 'https://issuer.com/status/42'
})

Getting Started

For Developers

Issue a Verifiable Credential:

npm install @affinidi/affinidi-tdk
import { VC } from '@affinidi/affinidi-tdk'

const credential = await VC.issue({
  issuer: 'did:webvh:yourcompany.com',
  holder: 'did:webvh:agent-42.com',
  type: 'EmploymentCredential',
  claims: {
    position: 'Trading Agent',
    authorizedBy: 'manager@yourcompany.com',
    startDate: '2026-01-01'
  }
})

console.log(credential) // Signed VC ready to share

Verify a Credential:

const isValid = await VC.verify(credential)
console.log(isValid) // true/false

For Architects

When to use VCs:

  • ✅ Authorization proofs that must survive offline
  • ✅ Cross-org trust without shared infrastructure
  • ✅ Privacy-preserving verification
  • ✅ Audit trails with cryptographic proof

When NOT to use VCs:

  • ❌ High-frequency updates (VCs are issued once, updated by reissuing)
  • ❌ Real-time streaming data
  • ❌ Internal systems where API calls are fine

Further Reading

W3C Specification:

Affinidi Docs:

Related Deep Dives:

Related Solutions:

Cookie Preferences

We use cookies to enhance your experience. You can manage your preferences below. For more information, read our Cookie Policy.

Strictly Necessary Always Active

These cookies are essential for core website functions such as security, session integrity, and cookie preference storage. They cannot be disabled.

  • _cf_bm: Distinguishes humans from bots (Cloudflare) · 30m
  • _cfuvid: Ensures secure browsing (Cloudflare) · Session
  • __hs_initial_opt_in: Prevents HubSpot's banner · 7 days
  • _gtm_debug: GTM debug mode (testing only) · Session
Analytics

These cookies help us understand how visitors interact with the site so we can improve content and performance. All data is aggregated and anonymous.

  • _ga, _gid, _gat: Google Analytics · Session – 2 years
  • __hstc, hubspotutk, __hssrc: HubSpot visitor tracking · 13 months
  • __hs_opt_out: HubSpot opt-out preference · 6 months
Marketing & Targeting

These cookies allow us and our partners to serve personalised ads and measure campaign performance.

  • _gcl_au, _gcl_dc: Google Ads conversion tracking · 90 days
  • IDE: Google Display Network personalisation · 1 year
  • _fbp: Meta / Facebook remarketing · 90 days
  • li_gc, _li_fat_id, bcookie: LinkedIn tracking · 1–24 months
  • guest_id, personalization_id: Twitter/X analytics · 2 years