PSDP documentation
Integrate the verifier, drive the API, and verify every claim on this site yourself, with curl.
How it works
PSDP is a privacy-preserving selective-disclosure protocol: a verifier confirms facts about a credential holder without seeing the underlying data.
- Issuer signs a credential (for example, a bank records a completed KYC check).
- Holder stores the credential in their wallet.
- Verifier creates a proof request: “prove
age >= 18” or “provekyc_level >= 2”. - Holder generates a zero-knowledge proof — it reveals only what was asked.
- Verifier calls PSDP to verify the proof.
Base URL
Substitute the host of the deployment you are integrating against. The examples below use a placeholder.
https://YOUR_PSDP_SERVER
Quick links
Request access
API keys are issued with pilot access. There is no self-serve signup yet — tell us what you are building and we send a key plus the quickstart. Engineering to engineering, no sales touch.
Verify every claim yourself
Every number on this site traces to a dated artifact on the evidence page. This section gives you the commands to check the running deployment and the published proofs yourself — step by step, copy-paste ready.
1. Check the API is live
$ curl -s https://YOUR_PSDP_SERVER/api/health | python3 -m json.tool
# Inspect the reported status, backends and standards fields.
2. Verify authentication works
# No key -> 401
$ curl -s -o /dev/null -w "%{http_code}" -X POST \
https://YOUR_PSDP_SERVER/api/v1/verify
# Expected: 401
# Wrong key -> 403
$ curl -s -o /dev/null -w "%{http_code}" -X POST \
-H "X-API-Key: wrong-key" \
https://YOUR_PSDP_SERVER/api/v1/verify
# Expected: 403
3. Verify a credential (mock crypto, testing only)
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/verify \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_KEY" \
-d '{
"proof_request": {
"protocol_version": "psdp/v0.1",
"request_id": "test-001",
"verifier_id": "my-verifier",
"audience": "my-verifier",
"nonce": "test-nonce-12345",
"expires_at": "2030-01-01T00:00:00Z",
"policy": {
"policy_id": "test-policy",
"policy_version": "1.0.0",
"policy_hash": "sha256:test",
"allowed_schema_ids": ["schema.test"],
"required_disclosures": [{"path": "/name"}],
"predicates": [
{"predicate_id": "age-check", "path": "/age",
"operator": "gte", "value_type": "number",
"comparison_value": 18}
],
"status_requirements": {
"require_active_status": false,
"freshness_max_age_seconds": 3600
}
}
},
"proof_package": {
"protocol_version": "psdp/v0.1",
"package_id": "pkg-001",
"request_id": "test-001",
"credential": {
"schema_id": "schema.test",
"issuer_id": "issuer.test",
"issuer_key_id": "key.issuer.001",
"expires_at": "2030-01-01T00:00:00Z",
"credential_root": "root:test",
"issuer_signature": "mock-signature:issuer.test:key.issuer.001:eddsa-ed25519"
},
"request_binding": {
"verifier_id": "my-verifier",
"audience": "my-verifier",
"nonce": "test-nonce-12345",
"policy_id": "test-policy",
"policy_version": "1.0.0",
"policy_hash": "sha256:test",
"request_binding_digest": "sha256:binding"
},
"disclosed_claims": [
{"path": "/name", "value": "Jane Doe", "value_type": "string"}
],
"predicate_assertions": [
{"predicate_id": "age-check", "path": "/age",
"operator": "gte", "satisfied": true}
],
"proof_material": {"proof": "mock-proof:valid", "encoding": "base64"},
"backend_profile": {
"backend": "arkworks", "proof_system": "groth16",
"circuit_id": "test-v1", "circuit_version": "1.0",
"verification_key_id": "vk-test"
},
"status_binding": {
"status_authority_id": "status.test",
"status_root": "root:active",
"status_issued_at": "2026-04-13T00:00:00Z",
"status_fresh_until": "2026-04-14T00:00:00Z",
"status_binding_signature": "mock-signature:status.test:key.status.001:eddsa-ed25519"
}
},
"allow_mock_crypto": true
}'
# Expected: "decision_status": "accepted" — all checks true.
4. Verify attacks are rejected
# Tampered proof -> rejected
# (same request as step 3, but change the proof value to "tampered")
# Expected: "decision_status": "rejected"
# Wrong schema -> SCHEMA_NOT_ALLOWED (change schema_id to "schema.fake")
# Expired request -> REQUEST_EXPIRED (set expires_at to "2020-01-01T00:00:00Z")
# Wrong nonce -> POLICY_HASH_MISMATCH (change nonce in request_binding)
5. Re-run the Tamarin proofs
# Download the theory files published with the evidence package
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_protocol.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_private.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_post_compromise.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_issuer_hiding.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_forward_secrecy.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_selective_disclosure_privacy.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_verifier_unlinkability.spthy
# Verify with Docker (no local install needed)
$ docker run --rm -v $(pwd):/work -w /work \
tamarin-prover/tamarin-prover:latest \
--prove --derivcheck-timeout=0 psdp_protocol.spthy
# Equivalence theories run in diff-mode:
$ docker run --rm -v $(pwd):/work -w /work \
tamarin-prover/tamarin-prover:latest \
--diff --prove --derivcheck-timeout=0 psdp_issuer_hiding.spthy
Expected, across the 7 theory files: 36 lemmas verified — 32 trace properties + 4 observational-equivalence proofs (≈5,500 proof steps), machine-checked in the Tamarin prover under a Dolev-Yao adversary.
6. Check the OIDF conformance bundles
$ curl -O https://YOUR_PSDP_SERVER/evidence/EVIDENCE_PACKAGE.json
# Inspect the conformance block; signed test-log .zip exports
# are archived alongside it.
Every conformance result bundle is signed by the suite’s own key and archived; runs are reproducible. We are not listed on openid.net/certification.
7. Inspect security headers
$ curl -sI https://YOUR_PSDP_SERVER/api/health | grep -iE \
"x-content-type|x-frame|strict-transport|content-security|permissions|x-request-id|cache-control"
# Read the security headers the deployment returns.
8. Drive the OID4VP flow
# Check verifier metadata
$ curl https://YOUR_PSDP_SERVER/.well-known/openid-credential-verifier \
| python3 -m json.tool
# Start an OID4VP verifier flow
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/oid4vp/initiate \
-H "X-API-Key: YOUR_KEY" | python3 -m json.tool
# Expected: authorization_url, request_uri, state, nonce
# Fetch the signed request object
$ curl https://YOUR_PSDP_SERVER/api/v1/oid4vp/request/REQUEST_ID
# Expected: a signed JWT (ES256 + x5c certificate)
9. Verify evidence integrity
$ curl -O https://YOUR_PSDP_SERVER/evidence/SHA256SUMS.txt
$ sha256sum -c SHA256SUMS.txt
# Expected: all files "OK"
10. Probe the wallet endpoint with a bad request object
$ curl "https://YOUR_PSDP_SERVER/api/v1/oid4vp/wallet/authorize?client_id=test&nonce=test&request_uri=https://example.com/bad-jwt"
# Expected: an error response — the wallet role rejects
# request objects whose signature does not verify.
Authentication
All /api/v1/ endpoints require an API key in the
X-API-Key header.
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/verify \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{ ... }'
Error responses
| Status | Meaning |
|---|---|
| 401 | No API key provided |
| 403 | Invalid API key |
| 429 | Rate limit exceeded (see the Retry-After header) |
Public endpoints (no key needed)
GET /api/health— system statusGET /docs— Swagger documentationGET /evidence/*— evidence package downloadsGET /.well-known/openid-credential-verifier— OID4VP metadata
Your first verification
Verify a credential in one API call.
Step 1 — send a verification request
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/verify \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"proof_request": {
"protocol_version": "psdp/v0.1",
"request_id": "req-001",
"verifier_id": "your-company.example",
"audience": "your-company.example",
"nonce": "unique-nonce-12345",
"expires_at": "2030-01-01T00:00:00Z",
"policy": {
"policy_id": "kyc-check",
"policy_version": "1.0.0",
"policy_hash": "sha256:abc123",
"allowed_schema_ids": ["schema.banking.kyc"],
"required_disclosures": [
{"path": "/jurisdiction"}
],
"predicates": [
{"predicate_id": "kyc-level",
"path": "/kyc_level",
"operator": "gte",
"value_type": "number",
"comparison_value": 2}
],
"status_requirements": {
"require_active_status": true,
"freshness_max_age_seconds": 3600
}
}
},
"proof_package": { ... }
}'
Step 2 — read the response
{
"decision_status": "accepted",
"reason_codes": [],
"checks": {
"issuer_signature_valid": true,
"proof_valid": true,
"policy_bound": true,
"replay_safe": true,
"credential_not_expired": true,
"status_valid": true,
"disclosures_valid": true
},
"verification_time_ms": ...
}
Create a proof request
Before verifying, create a proof request that defines what you want verified.
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/request \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"verifier_id": "bank-b.example.com",
"audience": "bank-b.example.com",
"policy": {
"policy_id": "cross-border-kyc",
"policy_version": "1.0.0",
"allowed_schema_ids": ["schema.banking.kyc"],
"required_disclosures": [
{"path": "/jurisdiction", "label": "Jurisdiction"}
],
"predicates": [
{"predicate_id": "kyc-level-check",
"path": "/kyc_level",
"operator": "gte",
"value_type": "number",
"comparison_value": 2}
],
"status_requirements": {
"require_active_status": true,
"freshness_max_age_seconds": 3600
}
}
}'
The response includes an auto-generated request_id
and nonce:
{
"request_id": "req:a1b2c3d4...",
"nonce": "xK9mP2...",
"verifier_id": "bank-b.example.com",
"expires_at": "2026-04-14T00:00:00Z",
"policy": { ... }
}
Send this to the credential holder. They use it to generate a proof package.
Zero-knowledge proofs
A zero-knowledge proof lets a holder prove a statement is true without revealing any information beyond the statement itself.
Example
A holder has a credential with age: 25. A verifier needs to know
“is this person over 18?”
| Approach | What the verifier learns | Disclosure |
|---|---|---|
| Full document | age = 25, name, date of birth, address… | Everything |
| Selective disclosure (SD-JWT) | age = 25 | The attribute value |
| ZK predicate (PSDP) | age >= 18: true | The predicate result only |
With the ZK predicate path, the verifier learns only that the statement is true — not the actual age, not the birthdate, not the name.
The proving backend
PQC-hybrid authentication (ML-DSA-65) + confidentiality (ML-KEM-768). The zero-knowledge layer is classical Groth16/BN254, with a de-risked (not yet live, not yet formally sound) transparent PQ-STARK backend on the roadmap.
Real Groth16/BN254 verification is exercised in CI: a blocking job builds the
Rust provers and runs the suite with PSDP_REQUIRE_ZK=1, so a
missing prover is a hard failure, not a silent skip.
Selective disclosure
Selective disclosure lets the holder reveal only specific fields from their credential.
How it works in PSDP
The verifier’s policy specifies required_disclosures — an
array of field paths that must be revealed:
"required_disclosures": [
{"path": "/jurisdiction"},
{"path": "/certification_body"}
]
The holder’s proof package includes only these fields:
"disclosed_claims": [
{"path": "/jurisdiction", "value": "DE"},
{"path": "/certification_body", "value": "TUV"}
]
Everything else in the credential stays hidden. The proof binds the disclosed values to the issuer-signed credential — the holder cannot substitute them.
Predicates
Predicates verify facts about credential fields without revealing the actual values.
Supported operators
| Operator | Meaning | Example |
|---|---|---|
gte | greater than or equal | age >= 18 |
gt | greater than | income > 3000 |
lte | less than or equal | claims <= 3 |
lt | less than | incidents < 2 |
eq | equal | status = 1 |
Example: age verification
"predicates": [
{
"predicate_id": "over-18",
"path": "/age",
"operator": "gte",
"value_type": "number",
"comparison_value": 18
}
]
The holder’s response includes:
"predicate_assertions": [
{"predicate_id": "over-18", "satisfied": true}
]
The verifier learns: “age >= 18 is
true.” Nothing else about the actual age.
Unlinkability
The design goal: when a holder presents to two different verifiers, the two verifiers should not be able to correlate the presentations.
How it works
PSDP uses nullifiers — one-way values derived from the holder’s secret and a per-relying-party scope:
nullifier = hash(holder_secret, relying_party_scope)
Each relying party gets a distinct scope, so the same user’s nullifier differs across relying parties — no cross-site correlation — while staying stable within one relying party for replay detection. A plain SD-JWT presentation, by contrast, re-presents the same signed payload each time, which colluding verifiers can compare; the ZK path derives a distinct per-relying-party value instead.
Verification status — stated honestly
Designed and machine-checked for unlinkability properties: identifier-hiding (T10a), audit/verifier-split (T10d) and nullifier-unlinkability / private-presentation theorems are discharged; verifier-view unlinkability, issuer hiding and selective-disclosure privacy are modeled with machine-verified observational-equivalence lemmas but remain partial overall. The full split is in the formal-verification section and on the evidence page.
API reference
The core endpoints. A running deployment serves interactive
Swagger docs at /docs.
POST /api/v1/verify
Verify a credential proof package against a proof request.
| Field | Type | Description |
|---|---|---|
proof_request | object | The verification policy and request parameters |
proof_package | object | The holder’s cryptographic response |
allow_mock_crypto | boolean | Allow mock signatures/proofs (testing only; refused in production mode) |
current_time | string | Override verification time (ISO 8601) |
| Field | Type | Description |
|---|---|---|
decision_status | string | "accepted" or "rejected" |
reason_codes | array | Specific rejection reasons (empty if accepted) |
checks | object | Individual boolean check results |
verification_time_ms | number | Time taken in milliseconds |
| Check | What it verifies |
|---|---|
issuer_signature_valid | Credential signed by a registered issuer |
proof_valid | The ZK proof verifies correctly |
policy_bound | Proof matches the requested policy |
replay_safe | Proof not used before |
credential_not_expired | Credential still within validity |
status_valid | Revocation status is fresh |
disclosures_valid | Disclosed claims match the policy |
POST /api/v1/request
Create a proof request from a verification policy.
| Field | Type | Required | Description |
|---|---|---|---|
verifier_id | string | Yes | Your identifier |
audience | string | Yes | Who the proof is for |
policy | object | Yes | Verification policy |
nonce | string | No | Custom nonce (auto-generated if omitted) |
ttl_seconds | integer | No | Request validity period (default: 3600) |
| Field | Description |
|---|---|
policy_id | Name for your policy |
allowed_schema_ids | Array of accepted credential types |
required_disclosures | Array of {"path": "/field"} |
predicates | Array of predicate objects |
status_requirements | Revocation check settings |
POST /api/v1/evidence
Generate a cryptographic audit evidence package from a verification. Call this after a verification to create a tamper-evident record for compliance and audit purposes; the package includes a hash chain linking the proof request, proof package and verification result.
OID4VP endpoints
| Endpoint | Role | Description |
|---|---|---|
POST /api/v1/oid4vp/initiate | Verifier | Start a flow: creates a signed request object (JAR / RFC 9101) and returns a wallet authorization URL |
GET /api/v1/oid4vp/request/{id} | Verifier | Serves the signed request object that wallets fetch |
POST /api/v1/oid4vp/response | Verifier | Receives the VP token; direct_post and direct_post.jwt (encrypted) modes |
GET /api/v1/oid4vp/wallet/authorize | Wallet | Receives authorization requests, verifies the request-object signature, creates an SD-JWT VC with Key Binding JWT, posts the encrypted response |
GET /.well-known/openid-credential-verifier | Verifier | Metadata: supported VP formats, signing keys, encryption keys |
GET /api/health
System health and protocol metadata. No authentication required.
| Field | Description |
|---|---|
status | "ok" when healthy |
backends | Available ZK proof backends |
standards | Supported standards |
scheduler | Background task scheduler status |
Questions about an endpoint? Talk to us →
Error & rejection codes
Every rejection carries a machine-readable reason code. The verifier tells you exactly why it said no.
HTTP errors
| Code | Meaning |
|---|---|
| 401 | No API key provided |
| 403 | Invalid API key |
| 413 | Request body too large (>1 MB) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Verification rejection codes
| Code | Meaning | How to fix |
|---|---|---|
SCHEMA_NOT_ALLOWED | Credential schema not in policy | Add the schema to allowed_schema_ids |
POLICY_HASH_MISMATCH | Proof binding does not match the request | Ensure nonce, verifier_id and policy match |
DISCLOSURE_MISMATCH | Disclosed claims do not match the policy | Disclose exactly the required fields |
REQUEST_EXPIRED | Request expires_at is past | Create a new request |
CREDENTIAL_EXPIRED | Credential validity has ended | Holder needs a fresh credential |
STATUS_STALE | Revocation status too old | Holder needs fresh status |
REPLAY_DETECTED | Proof already used | Generate a new proof with a new nonce |
INVALID_PROOF | ZK proof does not verify | Check proof generation |
PREDICATE_FAILURE | Predicate assertion mismatch | Ensure assertions match the policy predicates |
VERSION_MISMATCH | Protocol version mismatch | Use psdp/v0.1 |
Integration guides
Three worked policies. All three exercise the reference implementation — production hardening is in progress, and production deployment boundaries are noted per guide.
Cross-border KYC
Scenario: Bank A (Germany) has verified Customer X. Bank B (India) needs to confirm this before processing a transfer — without receiving the customer’s personal data.
POST /api/v1/request
{
"verifier_id": "bank-b.india.example",
"audience": "bank-b.india.example",
"policy": {
"policy_id": "cross-border-kyc",
"allowed_schema_ids": ["schema.banking.kyc"],
"required_disclosures": [
{"path": "/jurisdiction"}
],
"predicates": [
{"predicate_id": "kyc-level",
"path": "/kyc_level",
"operator": "gte",
"value_type": "number",
"comparison_value": 2}
]
}
}
Step 2: the customer’s wallet creates a
proof showing jurisdiction = DE and kyc_level >= 2
— without revealing name, passport or address.
Step 3: Bank B submits the request plus proof package to
POST /api/v1/verify. On "accepted", Bank B knows the
predicate held and the jurisdiction — and nothing else: no name, no passport,
no address, not even the exact KYC level.
Designed so that no personal data beyond the disclosed jurisdiction crosses the wire in this flow; the proof carries a cryptographic assertion, not the underlying record.
Age verification
Prove “over 18” without revealing the birthdate, age, or any personal information.
{
"policy_id": "age-check",
"allowed_schema_ids": ["schema.pid.eu"],
"required_disclosures": [],
"predicates": [
{"predicate_id": "over-18",
"path": "/age",
"operator": "gte",
"comparison_value": 18}
]
}
Note: required_disclosures is empty — the verifier
learns nothing except “age >= 18:
true”.
Production deployment boundaries for age verification — what PSDP provides and what a deployment still needs — are listed in the age-verification page’s Scope & limits.
Third-party assurance (DORA-style)
Scenario: a financial entity needs evidence about an ICT provider’s security posture without receiving the underlying audit detail.
{
"policy_id": "dora-ict-check",
"required_disclosures": [
{"path": "/jurisdiction"},
{"path": "/certification_body"}
],
"predicates": [
{"predicate_id": "pentest-recent",
"path": "/days_since_pentest",
"operator": "lte",
"comparison_value": 365},
{"predicate_id": "low-incidents",
"path": "/critical_incidents_12m",
"operator": "lte",
"comparison_value": 2}
]
}
What the verifier learns: jurisdiction, certification body, that the pentest was recent and incidents were few.
What stays hidden: pentest findings, vulnerability details, incident reports, internal risk scores.
This guide demonstrates a policy pattern on the reference implementation; it is not legal or regulatory advice.
Formal verification — the honest count
33 protocol theorems tracked: 15 discharged (machine-checked), 18 partial. We publish the split — partial means exactly that.
The protocol model is machine-checked in the Tamarin prover under a Dolev-Yao adversary: 36 lemmas verified — 32 trace properties + 4 observational-equivalence proofs (≈5,500 proof steps), across 7 theory files. A theorem map without gaps is a theorem map you should distrust; ours has 18 partials and says so.
Unlinkability-family status
| Property | How it is checked | Status |
|---|---|---|
| Identifier-hiding (T10a) | Machine-checked theorem | Discharged |
| Audit / verifier-split (T10d) | Machine-checked theorem | Discharged |
| Nullifier-unlinkability / private presentation | Machine-checked theorems | Discharged |
| Verifier-view unlinkability | Machine-verified observational-equivalence lemmas | Partial |
| Issuer hiding | Machine-verified observational-equivalence lemmas | Partial |
| Selective-disclosure privacy | Machine-verified observational-equivalence lemmas | Partial |
To re-run the proofs yourself, see step 5 above.
Circuit verification
Reference ZK circuits additionally checked with Picus (Veridise, Z3) — “properly constrained” — and Circomspect (Trail of Bits) — “no issues found” — plus 14/14 negative-witness tests (circom reference circuits, April 2026 evidence package).
Scope: these results cover the circom reference circuits in the April 2026 evidence package. The live verify path is the arkworks Groth16 backend, not these circom artifacts.
Re-run the checks yourself
# Install the tools
$ git clone https://github.com/Veridise/Picus.git
$ cargo install circomspect
# Picus — formal R1CS verification
$ racket Picus/picus.rkt --solver z3 psdp_selective_disclosure.r1cs
# Expected: "The circuit is properly constrained"
# Circomspect — static analysis
$ circomspect psdp_selective_disclosure.circom
# Expected: "No issues found"
# Negative-witness tests
$ npm install circomlib circomlibjs snarkjs
$ node negative_tests.js
# Expected: "14/14 passed, 0 failed"
The circuit sources, compiled R1CS files and tool reports ship in the April 2026 evidence package — see the evidence page for artifact paths.
OID4VP & SD-JWT VC support
Standards transport, not a proprietary token.
OpenID for Verifiable Presentations
The implementation targets OID4VP 1.0 Final, including the High
Assurance Interoperability Profile (HAIP), and is tested against the official
OpenID Foundation conformance suite (local runs, reproducible — not an OpenID
Foundation certification) — see the
results below.
response_type: vp_tokenresponse_mode: direct_postanddirect_post.jwt(encrypted)- Signed request objects (JAR / RFC 9101) with ES256
client_id_scheme: x509_hashandx509_san_dns- DCQL (Digital Credentials Query Language)
- Presentation Exchange 2.0
- Both verifier and wallet roles
Cross-stack check: Austria’s vck library (A-SIT / the engine behind the ID-Austria Valera wallet) presented an SD-JWT PID to the PSDP verifier over OID4VP 1.0 Final (direct_post + DCQL); PSDP verified the issuer signature and the Key-Binding JWT and accepted (2026-06-08, reproducible harness). Scope: this tested the vck library, not the Valera app binary, and dc+sd-jwt only — not JARM or mso_mdoc.
SD-JWT VC
SD-JWT VC (Selective Disclosure JWT Verifiable Credentials) is supported as a credential format:
- Full SD-JWT parsing with disclosure resolution
- Key Binding JWT (KB-JWT) for holder binding
- Nested disclosures and array-element disclosures
_sd_algsupport (sha-256, sha-384)- base64url-encoded disclosure digests (per spec)
- ES256 and EdDSA signing
Wallet interop coverage: 22 credential types, in 43 issuer configurations (SD-JWT VC and ISO mdoc formats), each issued and stored end-to-end against the walt.id wallet stack over live OID4VCI. Per-doctype results are on the evidence page.
OIDF conformance results
Self-run against the official OpenID Foundation conformance suite — local runs, reproducible. Not a published OIDF certification.
| Run | Scope | Result | Status |
|---|---|---|---|
| Full suite (2026-06) | OID4VCI, OID4VP and OpenID Federation test plans | 319/332 passed, 0 failed (11 warnings, 2 skipped) | Pass |
| FAPI 2.0 Security Profile (Final) | Test modules embedded in the OID4VCI issuer plans | 78/80 passed, 0 failed (2 warnings) | Pass |
| Earlier full-suite baseline (2026-05-26) | Full suite at the time | 210/218 passed, 0 failed (7 warnings, 1 skipped) | Pass |
Tested against the official OpenID Foundation conformance suite (local run, reproducible): 319 of 332 test modules passed, 0 failed (11 warnings, 2 skipped), across OID4VCI, OID4VP and OpenID Federation test plans. Self-run evidence — not an OpenID Foundation certification.
Every conformance result bundle is signed by the suite’s own key and archived; runs are reproducible. We are not listed on openid.net/certification. Source artifacts and run dates are on the evidence page.
Questions about an integration?
Bring your deployment or your roadmap — engineering to engineering, no sales touch. Or audit the demos first; we would do the same.