Verifier
Setting up OID4VP verification
Configure your tenant as a verifier, define what you want to ask for, and accept presentations.
OpenID for Verifiable Presentations (OID4VP) is how Cyfher asks a holder’s wallet for specific credentials and cryptographically verifies the response. This guide covers the tenant settings, the request shape, and what you get back.
Cyfher implements OID4VP ID3 (draft 26) with DCQL as the credential query language. The wallet also accepts Presentation Exchange 2.0 from legacy issuers, but verifiers should prefer DCQL.
Prerequisites
- An active tenant.
- At least one active signing key on your tenant. The signing key’s certificate determines the verifier
client_idadvertised to the wallet:- Key with an x509 certificate →
client_id = x509_san_dns:<hostname-from-cert>. - Key without a certificate →
client_id = redirect_uri:<your-callback-url>.
- Key with an x509 certificate →
If you have no active signing key when you call the verifier API, you’ll get error: "no_signing_key" back.
Verification settings
Portal sidebar → Verification. Three things to configure:

Trust Mode
Controls which issuers your verifier accepts presentations from:
- Open — any cryptographically valid presentation is accepted. Trust decisions happen in your application.
- Allowlist — only accept presentations signed by issuers on your trusted list. Anyone else is rejected at the verifier.
- Denylist — accept anyone except issuers on your blocked list.
- Federation — accept any issuer whose OpenID Federation trust chain resolves to a trust anchor you’ve selected. Cyfher walks the chain at verification time and verifies the presentation with the chain-resolved keys.
In Allowlist and Denylist mode, you list issuer identifiers (DIDs or HTTPS issuer URLs) one per row. Matching is exact on the credential’s issuer identifier — no trust-chain resolution happens in these modes, so a federation trust anchor entered here would only match a credential whose iss is literally that anchor.
In Federation mode, you pick one or more trust anchors from the platform registry (a platform administrator registers them under Admin → Trust Anchors). At verification time Cyfher resolves each presenting issuer’s OpenID Federation trust chain against those anchors and rejects any issuer whose chain doesn’t lead to a selected anchor. Federation mode rejects every issuer until at least one anchor is selected.
Public document verification
A toggle that enables a public verification page where third parties can drop in a signed document and see whether it verifies against your trust list — a shareable link you’ll find on the verification settings screen. Useful for offering a zero-integration verification UX to relying parties.
Creating a presentation request
You can also create a request without touching the API: Portal → Verification Requests builds the DCQL query (with required/optional claims), signs the request, and renders the openid4vp:// URI as a scannable QR.
POST /t/:slug/presentation/request
{
"credential_type": "DriversLicense"
}
Or, for full DCQL control:
POST /t/:slug/presentation/request
{
"dcql_query": {
"credentials": [
{
"id": "drivers_license",
"format": "mso_mdoc",
"meta": { "doctype_value": "org.iso.18013.5.1.mDL" },
"claims": [
{ "path": ["org.iso.18013.5.1", "given_name"] },
{ "path": ["org.iso.18013.5.1", "family_name"] },
{ "path": ["org.iso.18013.5.1", "birth_date"] }
]
}
]
}
}
Response:
{
"uri": "openid4vp://?client_id=…&request_uri=…",
"client_id": "x509_san_dns:verifier.example.com",
"request_uri": "https://verifier.example.com/t/<slug>/presentation/authorize/<id>",
"state": "<opaque-correlation-token>"
}
Render uri as a QR code for cross-device flow, or open it directly with window.location for same-device flow where the wallet handles the openid4vp:// scheme. The state is your correlation handle — save it.
What happens next
- Wallet fetches the signed authorization request from
request_uri(Cyfher serves it with content typeapplication/oauth-authz-req+jwt). - The holder reviews what’s being requested and consents in the wallet.
- The wallet posts the verified presentation token to
POST /t/:slug/presentation/responsewith yourstate. - Cyfher verifies the signature, status list, trust chain, and claim integrity, then records the result.
You learn the outcome by either:
- Watching the live result panel — when you mint the request from Portal → Verification Requests, the page subscribes to that request and swaps the QR for a result panel the moment the holder presents: it shows Verified / Verified (partial) / Verification failed, plus the credential format, issuer, credential status, and (on failure) the reason. Disclosed claim values are delivered to the holder’s wallet and are not stored here — the panel reports the outcome only.
- Watching the portal Audit Log — every verification also lands at Portal → Audit Log as a
presentation.verified(orpresentation.failed) event, including which tenant’s credential was presented. The audit log is the durable record; the live panel is the same signal shown in real time. - Reading the synchronous response — when your application drives the wallet leg and posts the VP token itself,
POST /t/:slug/presentation/responsereturns the outcome inline:verified: truewith the verificationresultandstatus_checkon success, orHTTP 400with{"error": "…", "verified": false}on failure. Thestateis only the correlation handle the wallet echoes back on that same response — there is no separate endpoint to poll a verification bystate. - Subscribing via the result webhook — configure an HTTPS endpoint under Portal → Verification → Result webhook and Cyfher POSTs every presentation-verification outcome to it as it happens. See below.
Result webhook
Configure it at Portal → Verification (the Verification Settings page), in the Result
webhook panel: set the endpoint URL, toggle Enabled, and save. The first save generates a
signing secret and shows it to you once — store it, since it can only be rotated afterwards, not
retrieved again. Use Rotate secret to issue a new one (the old secret stops working
immediately) and Send test event to fire a synthetic presentation.verified delivery at your
endpoint. Recent deliveries, their status, response code, attempt count, and last error are listed
on the same page.
Payload. Cyfher POSTs a JSON body for every terminal outcome (verified, partial, or failed):
{
"event": "presentation.verified",
"verification_state": "<the state/request id you correlated the request with>",
"status": "verified",
"tenant": "<your-tenant-slug>",
"occurred_at": "2026-07-25T12:34:56.000000Z",
"data": {
"format": "dc+sd-jwt",
"issuer": "https://issuer.example",
"credential_status": "valid"
}
}
event is one of presentation.verified, presentation.verified_partial, or
presentation.failed; status mirrors it as verified, verified_partial, or failed. data
carries outcome-specific metadata: on success, format/issuer/credential_status; on failure
(including partial), a reason (and sometimes a detail) describing what went wrong.
Headers. Each delivery carries:
X-Cyfher-Event— the event name (same value as theeventfield).X-Cyfher-Delivery— a UUID unique to this delivery attempt sequence, for idempotency/dedup.X-Cyfher-Signature—t=<unix-timestamp>,v1=<hex-hmac>, see below.
Verifying the signature. The signature is an HMAC-SHA256 over the timestamp and the raw request body, keyed on your tenant’s signing secret — the same shape Stripe uses:
signed = "{t}.{raw_request_body}"
expected = hex( HMAC_SHA256(your_signing_secret, signed) )
# constant-time compare `expected` against the v1 value from the header;
# reject if |now - t| exceeds your tolerance (a few minutes is reasonable)
Retries. A 2xx response marks the delivery delivered. Anything else (non-2xx or a timeout)
is retried with exponential backoff, up to 6 attempts total, after which the delivery is marked
failed. Delivery status, response code, attempt count, and the last error are visible in the
portal delivery log described above.
Trust chain handling
For cross-tenant verification (a credential issued by another Cyfher tenant), Cyfher resolves the issuer’s metadata via the cross-tenant JWKS endpoint automatically — you don’t need to allow-list cross-tenant issuers individually, just the tenant or the platform federation root.
For external issuers without discoverable JWKS, Cyfher fails closed: with no way to fetch the issuer’s keys it cannot verify the signature, so the presentation is rejected. The response is HTTP 400 {"error": "jwks_fetch_failed", "verified": false} and a presentation.failed event lands in the Audit Log. (A degraded HTTP-200 “partial verification” path exists only for the OID4VP conformance suite — it is opt-in under CONFORMANCE_SEED and is never enabled in production.)
Conformance
Cyfher passes the OID4VP ID3 verifier conformance suite (x509_san_dns profile, 45 SUCCESS test cases). Two items are deferred against the latest drafts:
- HAIP profile — High-Assurance Interoperability Profile (mandatory
x5cheader in JWT credentials, mandatoryexpclaim). Cyfher accepts presentations with or without these today; once HAIP stabilises, enforcement will be enabled per-tenant. - Format identifier transition — Cyfher issues and advertises
dc+sd-jwt(RFC 9901). Presentations from verifiers that still use the oldervc+sd-jwtidentifier are accepted as an alias.
Full request/response schemas are in the API reference.