Standards / Intermediate / 10 min

Verifiable Presentations (OID4VP)

How credentials are shared and verified—OpenID for Verifiable Presentations

The Problem Verifiable Presentations Solve

You have a Verifiable Credential (VC)—a diploma, employment record, agent mandate—how do you share it with a verifier?

Challenges:

  • Proving you control the credential — Anyone can copy a VC; how does the verifier know you are the holder?
  • Selective disclosure — What if you only want to share part of the credential?
  • Request/response flow — How does the verifier request specific credentials?
  • Privacy — How do you share credentials without revealing unnecessary data?

Verifiable Presentations (VPs) solve this: a standardized way to package VCs with proof that you (the holder) are presenting them.

OpenID for Verifiable Presentations (OID4VP) adds the protocol: how verifiers request VPs and how holders respond.


What Is a Verifiable Presentation?

A Verifiable Presentation is a container that:

  1. Holds one or more Verifiable Credentials
  2. Proves the holder controls the credentials
  3. Binds the presentation to a specific verifier (prevents replay attacks)

Structure:

{
  "@context": ["https://www.w3.org/2018/credentials/v1"],
  "type": ["VerifiablePresentation"],
  "holder": "did:webvh:alice.com",
  "verifiableCredential": [
    { /* VC: University Diploma */ },
    { /* VC: Employment Record */ }
  ],
  "proof": {
    "type": "Ed25519Signature2020",
    "created": "2026-07-21T19:00:00Z",
    "verificationMethod": "did:webvh:alice.com#key-1",
    "proofPurpose": "authentication",
    "challenge": "nonce-from-verifier-xyz",
    "proofValue": "z3FXQi...signature..."
  }
}

Key properties:

  • Holder — Who is presenting (Alice)
  • VCs — The credentials being shared
  • Proof — Cryptographic signature proving Alice controls her DID
  • Challenge — Nonce from verifier (prevents replay)

How Verifiable Presentations Work

1. Verifier Requests Credentials

Verifier sends a presentation request:

GET /request-presentation?
  response_type=vp_token&
  client_id=did:webvh:employer.com&
  presentation_definition={...}

Presentation Definition (what the verifier wants):

{
  "id": "employment-check",
  "input_descriptors": [{
    "id": "diploma",
    "constraints": {
      "fields": [{
        "path": ["$.type"],
        "filter": { "type": "string", "const": "UniversityDegreeCredential" }
      }]
    }
  }]
}

Translation: “I want a UniversityDegreeCredential”

2. Holder Creates Presentation

Holder (Alice) selects matching VCs and creates a VP:

// Alice has a diploma VC
const diplomaVC = await wallet.getCredential('diploma')

// Create presentation
const presentation = {
  type: ['VerifiablePresentation'],
  holder: 'did:webvh:alice.com',
  verifiableCredential: [diplomaVC],
  proof: await sign({
    challenge: verifierChallenge,
    holder: 'did:webvh:alice.com',
    privateKey: alicePrivateKey
  })
}

3. Holder Sends Presentation

Alice sends the VP to the verifier:

POST /submit-presentation
Content-Type: application/json

{
  "vp_token": "<signed-presentation>",
  "presentation_submission": {
    "id": "...",
    "definition_id": "employment-check",
    "descriptor_map": [...]
  }
}

4. Verifier Validates

Verifier checks:

// 1. Verify the VP signature (is this really from Alice?)
const holderDID = presentation.holder
const holderKey = await resolve(holderDID)
const isValidPresentation = await verify(presentation.proof, holderKey)

// 2. Verify each VC signature (is the diploma really from Stanford?)
for (const vc of presentation.verifiableCredential) {
  const issuerKey = await resolve(vc.issuer)
  const isValidVC = await verify(vc.proof, issuerKey)
}

// 3. Check challenge (is this response for MY request?)
const challengeMatches = presentation.proof.challenge === myChallenge

// 4. Check revocation (has Stanford revoked the diploma?)
const isRevoked = await checkRevocation(diplomaVC)

// ✅ All checks pass — presentation is valid

OID4VP: The Protocol Layer

OpenID for Verifiable Presentations (OID4VP) standardizes the request/response flow using OAuth 2.0 patterns.

Authorization Request (Verifier → Holder)

GET /authorize?
  response_type=vp_token&
  client_id=did:webvh:employer.com&
  redirect_uri=https://employer.com/callback&
  presentation_definition={...}&
  nonce=challenge-xyz

Presentation Submission (Holder → Verifier)

POST /callback
Content-Type: application/x-www-form-urlencoded

vp_token=<base64-encoded-presentation>&
presentation_submission=<metadata>

Verification (Verifier)

Verifier validates the VP and returns success/failure to the application flow.


Selective Disclosure with BBS+ Signatures

Problem: You want to prove you’re over 21 without revealing your exact birthdate.

Solution: BBS+ signatures allow deriving a new credential that proves properties without revealing the full claim.

Original VC:

{
  "credentialSubject": {
    "id": "did:webvh:alice.com",
    "name": "Alice",
    "birthdate": "1995-03-15",
    "address": "123 Main St"
  },
  "proof": { "type": "BbsBlsSignature2020", "proofValue": "..." }
}

Derived VC (selective disclosure):

{
  "credentialSubject": {
    "id": "did:webvh:alice.com",
    "over21": true  // Proven cryptographically, no birthdate revealed
  },
  "proof": { "type": "BbsBlsSignatureProof2020", "proofValue": "..." }
}

How it works:

  • BBS+ allows proving predicates (age > 21) without revealing the underlying data (birthdate)
  • The proof is still verifiable against the original issuer’s signature
  • Verifier sees only what you choose to disclose

Real-World Use Cases

1. Agent Mandate Presentation

Scenario: Agent presents its authorization mandate to a partner system.

Flow:

// Verifier requests mandate
const request = {
  presentation_definition: {
    input_descriptors: [{
      id: 'trading-mandate',
      constraints: {
        fields: [{
          path: ['$.type'],
          filter: { const: 'TradingMandate' }
        }]
      }
    }]
  }
}

// Agent creates presentation
const presentation = {
  holder: 'did:webvh:bankA.com:agent-42',
  verifiableCredential: [tradingMandateVC],
  proof: await signPresentation({
    challenge: request.nonce,
    holderDID: 'did:webvh:bankA.com:agent-42',
    privateKey: agentPrivateKey
  })
}

// Verifier validates and allows trade

2. PHI Access Authorization

Scenario: Agent proves it’s authorized to access patient health information.

Flow:

// System requests authorization
const request = {
  presentation_definition: {
    input_descriptors: [{
      id: 'phi-access',
      constraints: {
        fields: [
          { path: ['$.type'], filter: { const: 'PHIAccessAuthorization' } },
          { path: ['$.credentialSubject.patient'], filter: { const: 'patient-12345' } }
        ]
      }
    }]
  }
}

// Agent presents authorization VC
const presentation = {
  holder: 'did:webvh:hospital.com:agent-99',
  verifiableCredential: [phiAccessVC],
  proof: { /* Agent's signature */ }
}

// System validates and grants access

3. Cross-Org Identity Verification

Scenario: Partner needs to verify your agent is who it claims to be.

Flow:

// Partner requests identity proof
const request = {
  presentation_definition: {
    input_descriptors: [{
      id: 'agent-identity',
      constraints: {
        fields: [{
          path: ['$.issuer'],
          filter: { const: 'did:webvh:yourcompany.com' } // Must be from your org
        }]
      }
    }]
  }
}

// Agent presents identity credential
const presentation = {
  holder: 'did:webvh:yourcompany.com:agent-42',
  verifiableCredential: [identityVC],
  proof: { /* Agent's signature */ }
}

// Partner verifies and establishes trust

Verifiable Presentations in the Affinidi Stack

Elements Services issues VCs that can be presented:

// Issue a credential
const vc = await elements.issueCredential({
  holder: 'did:webvh:agent-42.com',
  type: 'TradingMandate',
  claims: { authorizedBy: 'trader-alice', expiry: '2026-12-31' }
})

// Store in wallet
await wallet.store(vc)

Affinidi Vault holds VCs and creates presentations:

// Receive presentation request
const request = await vault.receivePresentationRequest(requestUrl)

// Select matching credentials
const matchingVCs = await vault.findCredentials(request.presentation_definition)

// Create presentation
const presentation = await vault.createPresentation({
  verifiableCredential: matchingVCs,
  challenge: request.nonce,
  holder: userDID
})

// Submit to verifier
await submitPresentation(presentation, request.callback_url)

Agent Gateway verifies presentations:

// Agent presents credentials with request
const request = {
  action: 'execute-trade',
  presentation: vpToken
}

// Gateway verifies presentation
const isValid = await agentGateway.verifyPresentation(request.presentation)
if (!isValid) throw new Error('Invalid credentials')

// Proceeds if valid

Getting Started

For Developers

Create a Verifiable Presentation:

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

const presentation = await VP.create({
  holder: 'did:webvh:alice.com',
  verifiableCredential: [diplomaVC, employmentVC],
  challenge: verifierChallenge,
  privateKey: alicePrivateKey
})

console.log(presentation) // Ready to send to verifier

Verify a Presentation:

const isValid = await VP.verify(presentation, {
  challenge: myChallenge
})

console.log(isValid) // true/false

For Architects

When to use Verifiable Presentations:

  • ✅ Credential sharing with proof of holder control
  • ✅ Privacy-preserving data exchange (selective disclosure)
  • ✅ Cross-org authentication without passwords
  • ✅ Agent authorization proofs

When NOT to use:

  • ❌ Bearer tokens where holder proof isn’t needed
  • ❌ High-frequency auth (cache the VP result instead)
  • ❌ Internal systems where simpler auth suffices

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