Skip to content

Verifier · Verify Credential

This page is for the Verifier role: the service that asks a holder to prove something. From your backend’s perspective, verification is two API calls — create an authorization request, then read the result. Everything between (fetching the signed request object, prompting the user, building the VP token, submitting it) happens on the wallet side.


Rendering diagram...

What you implement:

  1. Create the request. Post your DCQL query and verifier identity to Verifier Service.
  2. Deliver the request URI.
  3. Poll the result. Watch GET /v1/verifier/result/{session_id} until the session is completed.

You never construct or verify the VP token, the SD-JWT signature, or the key-binding JWT yourself. Verifier Service does all of that and exposes the outcome on the result endpoint as status (overall pass/fail) plus check_results[] (per-check breakdown).


Terminal window
curl -X POST https://verifier.turingspace.co/v1/verifier/authorization-request \
-H "X-API-Key: tcs_production_7f3a9b2c1d4e5f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e" \
-H "Content-Type: application/json" \
-d '{
"dcql_query": {
"credentials": [
{
"id": "identity_credential",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["https://schema-registry.turingspace.co/schemas/TuringCerts_Standard_Credential/v2"]
},
"claims": [
{ "path": ["credentialName"] },
{ "path": ["issuedTime"] }
]
}
]
},
"verifier_private_key": "${VERIFIER_PRIVATE_KEY}",
"verifier_client_id": "did:iota:0xae72f18dc7b6af5b740edae8e4e0b2d3c9fb10a4e7d6c5b3a2f1e0d9c8b7a6f5",
"response_mode": "direct_post",
"expires_in": 300
}'

Response (201 Created):

{
"session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
"request_uri": "https://verifier.turingspace.co/v1/verifier/request/b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
"authorization_request": "openid4vp://authorize?client_id=did%3Aiota%3A0xae72...&request_uri=https%3A%2F%2Fverifier.turingspace.co%2Fv1%2Fverifier%2Frequest%2Fb2c3d4e5-...",
"expires_at": "2026-03-26T00:05:00Z"
}
NameTypeRequiredDescription
dcql_queryobjectYesWhich credentials and claims you need. See DCQL Query Structure.
verifier_private_keystringYesBase64url-encoded raw Ed25519 (32 bytes). Used to sign the JAR (request object) so the wallet can verify who is asking.
verifier_client_idstringYesThe verifier’s DID (or URL). Wallets show this to the user during consent.
response_modestringNo"direct_post" (default — plain server-to-server VP delivery) or "direct_post.jwt" (encrypted VP delivery using ECDH-ES; TCS auto-populates client_metadata.jwks with an ephemeral public key).
expires_innumberNoSession TTL in seconds (default: 300).
client_metadataobjectNoclient_name, logo_uri, policy_uri, tos_uri, jwks — shown on the wallet’s consent screen.
redirect_uristringNoWhere the wallet redirects the user after submission. Omit for headless flows.
StatusCause
400Malformed body (missing fields, empty dcql_query.credentials, etc.)
401Invalid or missing X-API-Key

How you put the request in front of the holder depends on the operating mode.

Pass authorization_request to Holder Service on behalf of the user. Holder Service fetches the signed request, prompts the user, builds the VP token, and submits it. See Holder · Manage Wallets & Credentials § 4.

Terminal window
curl -X POST https://holder.turingspace.co/v1/oid4vp/presentation \
-H "Authorization: Bearer ${HOLDER_USER_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"authorization_request_uri": "openid4vp://authorize?client_id=...&request_uri=...",
"selected_credentials": [
{ "credential_id": "...", "query_id": "identity_credential",
"disclosed_claims": ["credentialName", "issuedTime"] }
],
"holder_did": "did:iota:testnet:0x7a8b...",
"private_key": "${HOLDER_PRIVATE_KEY}"
}'

Response (200 OK):

{
"success": true,
"redirect_uri": "https://verifier.example.com/callback?session_id=b2c3d4e5-..."
}

redirect_uri is present only if the verifier configured one — deep-link the user there to complete the flow on the verifier side. From the verifier’s perspective, the session moves to completed and your verifier integration polls for the result in Step 3 below.

Render authorization_request as a QR code or open the deep link on the user’s device. Their wallet app handles the rest of OID4VP and posts the VP token to TCS directly.


Terminal window
curl https://verifier.turingspace.co/v1/verifier/result/b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e \
-H "X-API-Key: tcs_production_7f3a9b2c1d4e5f6a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e"

Response (200 OK) — completed:

{
"session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
"status": "completed",
"created_at": "2026-04-01T00:00:00.000Z",
"expires_at": "2026-04-01T00:05:00.000Z",
"verified_at": "2026-04-01T00:01:23.000Z",
"verification_mode": "fail_fast",
"check_results": [
{ "check": "oid4vp_compliance", "status": "success", "reason_code": "oid4vp_request_valid", "message": "OID4VP request and response are spec-compliant" },
{ "check": "credential_format", "status": "success", "reason_code": "credential_format_valid", "message": "Credential format is valid SD-JWT VC" },
{ "check": "signature", "status": "success", "reason_code": "signature_verified", "message": "Issuer signature verified against the resolved DID Document" },
{ "check": "issuer_trust", "status": "success", "reason_code": "not_implemented", "message": "Trust Registry check not yet wired into the verification pipeline" }
],
"presentation_metadata": {
"claims": {
"credentialName": "Bachelor of Science",
"issuedTime": "2026-01-15"
},
"credential_metadata": [
{
"issuer": "did:iota:testnet:0xabcdef...",
"credential_type": ["UniversityDegreeCredential"],
"issued_at": "2026-01-15T00:00:00.000Z",
"expires_at": "2027-01-15T00:00:00.000Z"
}
]
}
}

The session is completed only when every check in check_results returns status: "success". To branch on overall outcome, check status === "completed" (boolean equivalent of “did the credential verify”). To explain why something failed to a user, walk check_results for the entries with status: "fail" and surface their message.

Response (200 OK) — failed (per-check breakdown):

{
"session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
"status": "failed",
"created_at": "2026-04-01T00:00:00.000Z",
"expires_at": "2026-04-01T00:05:00.000Z",
"verified_at": "2026-04-01T00:01:23.000Z",
"verification_mode": "fail_fast",
"check_results": [
{ "check": "credential_format", "status": "success", "reason_code": "credential_format_valid", "message": "Credential format is valid SD-JWT VC" },
{ "check": "signature", "status": "fail", "reason_code": "signature_verification_failed", "message": "Issuer signature did not verify against the resolved DID Document" }
]
}

In fail_fast mode, check_results stops at the first failure. In run_all mode every applicable check is reported even after the first failure — useful when you need a full diagnostic for support tickets.

Where the mode is set. verification_mode is not a parameter on POST /v1/verifier/authorization-request. It is sent by the wallet (or the custodial Holder Service acting on the holder’s behalf) in the body of POST /v1/verifier/presentation when the VP is submitted: {"verification_mode": "run_all"} (default "fail_fast"). Verifier-side code only reads it back; it does not select the mode at request creation.

The closed set of check names and reason_code values currently surfaced by the verification pipeline. Use this to triage failures or to map error codes to user-facing copy.

checkreason_code (success)reason_code (fail)What it means
oid4vp_complianceoid4vp_request_validoid4vp_non_compliant, presentation_invalidOID4VP request + response shape conform to the spec
credential_formatcredential_format_validcredential_format_invalidThe presented payload is parseable as dc+sd-jwt
signaturesignature_verifiedsignature_verification_failed, signature_payload_mismatch, issuer_key_not_resolvableIssuer signature verifies against the resolved key
issuer_trustnot_implementedTrust Registry membership check (currently a no-op pass — surfaced as not_implemented until the pipeline wires it in)
schema_governancenot_implementedSchema-side governance check (roadmap; surfaced as not_implemented)
schema_versionnot_implementedSchema-version policy check (roadmap; surfaced as not_implemented)
issuer_governancenot_implementedIssuer-side governance check (roadmap; surfaced as not_implemented)
(any check)dcql_not_satisfied, skippedDCQL match failed (no credential satisfied the query); or check skipped by fail_fast after an earlier failure

status: "success" with reason_code: "not_implemented" is the honest signal that a check is roadmap’d but not yet enforced — it does not mean the underlying property has been verified.

Response (200 OK) — expired:

{
"session_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
"status": "expired",
"created_at": "2026-04-01T00:00:00.000Z",
"expires_at": "2026-04-01T00:05:00.000Z",
"error": {
"code": "session_expired",
"message": "Session TTL elapsed without a wallet response"
}
}
StatusMeaning
pendingRequest created, waiting for the wallet to fetch the request object
in_progressWallet has fetched the request object, user has not yet submitted
completedVP token validated; presentation_metadata populated and every entry in check_results is success
failedAt least one entry in check_results is fail (e.g. bad signature, wrong nonce, expired credential)
expiredSession TTL elapsed without a wallet response; error populated

A practical poll loop runs every 2 seconds until status is terminal:

Terminal window
while true; do
RESULT=$(curl -s "$URL/v1/verifier/result/$SESSION_ID" -H "X-API-Key: $KEY")
STATUS=$(echo "$RESULT" | jq -r '.status')
case "$STATUS" in completed|failed|expired) echo "$RESULT" | jq .; break;; esac
sleep 2
done

A push-based webhook delivery mechanism (so verifier backends can avoid polling) is on the roadmap; until it ships, polling the result endpoint is the supported integration path. Contact us if push delivery is a hard requirement for your deployment.


DCQL (Digital Credentials Query Language) describes which credentials and claims you ask for. Every authorization request requires a dcql_query.

{
"dcql_query": {
"credentials": [ /* one or more credential descriptors */ ]
}
}
FieldTypeRequiredDescription
idstringYesIdentifier for this credential in the query — also referenced by Holder Service in selected_credentials[].query_id.
formatstringYesUse "dc+sd-jwt" for SD-JWT VC.
metaobjectYesContainer for credential-format-specific constraints. Required even if you only set one inner field.
meta.vct_valuesstring[]NoAcceptable VCT URLs. Wallet matches against the credential’s vct claim.
claimsobject[]NoSpecific claims to request as { "path": [...] }. Omit to let the wallet decide.
trusted_authoritiesobject[]NoRestrict to specific issuers, e.g. [{ "type": "did", "values": ["did:iota:0x..."] }].

Each claim path is a JSON path from the credential root: ["credentialName"], ["degreeName"]. Anything not listed remains hidden via SD-JWT selective disclosure.

Example — university degree with trusted issuer pinning

Section titled “Example — university degree with trusted issuer pinning”
{
"dcql_query": {
"credentials": [
{
"id": "degree_credential",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["https://schema-registry.turingspace.co/schemas/UniversityDegreeCredential/v1"]
},
"claims": [
{ "path": ["credentialName"] },
{ "path": ["studentId"] },
{ "path": ["degreeName"] }
],
"trusted_authorities": [
{ "type": "did", "values": ["did:iota:0xabc123def456"] }
]
}
]
}
}

The holder sees a consent screen listing only these three claims; everything else stays hidden.


  • Treating verifier_private_key like config. It is a signing key — store it in a secret manager and reference it at request time, not in .env files committed alongside source.
  • Polling forever. Always honour the expires_at from Step 1. Once the session expires (status: expired), stop polling and surface the failure to the user.
  • Asking for too many claims. Wallets will show every claim path on the consent screen. Asking for ten optional fields trains users to consent without reading. Ask for the minimum.
  • Forgetting vct_values. Without it the wallet may match credentials of unrelated types.
  • Using non-DID verifier_client_id without a known JWKS. Wallets use client_id to look up your signing keys. If you use a non-DID URL, expose JWKS at the standard location and reference it in client_metadata.jwks.