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:
- Holds one or more Verifiable Credentials
- Proves the holder controls the credentials
- 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:
- Verifiable Presentations Data Model
- OpenID for Verifiable Presentations (OID4VP)
- Presentation Exchange
Affinidi Docs:
Related Deep Dives:
- Verifiable Credentials — VCs are what you present in a VP
- Decentralized Identifiers — DIDs identify the holder and issuer
Related Solutions:
- Per-Agent Attribution — VPs prove agent authorization
- Cross-Org Agent Trust — VPs enable trust across boundaries