Cloud SDK - API Reference

API reference for the Cloud SDK REST service that verifies mobile IDs from Apple Wallet, Google Wallet, Samsung Wallet, and state-issued mDL apps.

01Overview

The Cloud SDK supports the following protocols:

Credential FormatTransport ChannelCompatible Production Wallets
ISO 18013-5 mdocDigital Credentials APIApple Wallet (iOS)
ISO 18013-5 mdocDigital Credentials API + OpenID4VPGoogle Wallet (Android)
ISO 18013-5 mdocOpenID4VP Annex BSamsung Wallet
W3C Verifiable CredentialOpenID4VPState Wallet

Any wallet that implements the same credential format and transport channel is compatible.

A. DC API + ISO mdoc For wallets invoked via the browser's Digital Credentials API returning an ISO mdoc credential (e.g. Apple Wallet).

B. DC API + OpenID4VP + ISO mdoc For wallets invoked via the DC API using an OpenID4VP envelope (e.g. Google Wallet).

C. OpenID4VP Annex B + ISO mdoc For wallets invoked directly via a mdoc-openid4vp:// deep link, no browser mediation (e.g. Samsung Wallet).

D. OpenID4VP + W3C Verifiable Credential For wallets that present W3C Verifiable Credentials over OpenID4VP (e.g. State Wallet).

To choose a flow and see end-to-end implementation, see the Integration Guide.


02CloudSDK Environment Configuration

Production base URL:

https://credenceid.com/cloudsdk/playground

Required headers

HeaderValue
AuthorizationBearer <accessToken> β€” required on all protected endpoints.
Content-Typeapplication/json (unless noted) β€” required on all requests with a body.
OriginYour registered HTTPS domain (e.g. https://app.example.com). Always required on POST /v1/setup. On all other endpoints: browser callers send it automatically; server-side callers may omit it.

03Authentication

The Cloud SDK uses a two-token JWT (JSON Web Token) scheme:

TokenTTLUse
Access token15 minutes (expiresIn: 900)Authorization: Bearer … on every protected request
Refresh token7 daysExchange via /v1/refresh for a new access token.
POST /v1/setup3.1.

Exchanges a license key and Profile ID for an access-token / refresh-token pair.

To generate a License Key and Profile ID, visit: Generate Cloud SDK Key.

AuthNone
Required headersOrigin, Content-Type: application/json

Request body

Both fields are required and identify which license and reader profile to activate.

{
  "licenseKey": "CS-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "profileId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
FieldTypeRequiredDescription
licenseKeystringyesLicense key issued by Verify with Credence. Must start with CS-.
profileIduuidyesUUID of the reader profile associated with the license.

Example

curl -X POST https://credenceid.com/cloudsdk/playground/v1/setup \
  -H "Origin: https://app.example.com" \
  -H "Content-Type: application/json" \
  -d '{
    "licenseKey": "CS-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "profileId":  "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }'

Response 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI…",
  "refreshToken": "rt_550e8400-e29b-41d4-a716-446655440000",
  "expiresIn": 900,
  "tokenType": "Bearer",
  "customerName": "Credence Demo",
  "profileName": "Default Profile"
}
FieldTypeDescription
accessTokenstringHS256 JWT. Use as Authorization: Bearer <token>. Embeds the activation domain as a field inside the token.
refreshTokenstringOpaque token, prefixed rt_. Rotated on every /v1/refresh. Replace your stored value with the new token each time.
expiresInintAccess-token lifetime in seconds (currently 900 = 15 minutes).
tokenTypestringAlways "Bearer".
customerNamestringDisplay name of the licensed customer. May be empty.
profileNamestringDisplay name of the selected reader profile. May be empty.

Errors

HTTPerrorWhen
400INVALID_REQUESTOrigin header missing/blank, or body malformed.
401INVALID_LICENSE_KEYLicense key not recognized by Verify with Credence.
401LICENSE_EXPIRED_OR_REVOKEDLicense has expired or been revoked.
403DOMAIN_MISMATCHOrigin doesn't match the registered domain.
403PROFILE_NOT_FOUNDProfile ID does not belong to this license.
429PLAN_LIMIT_EXCEEDEDMonthly verification quota exhausted.
429RATE_LIMIT_EXCEEDEDMore than 30 requests/minute from this IP. Honor Retry-After.
429{ "error": "Rate limit exceeded" }Exceeded 30 requests/min per IP. Check Retry-After header.
500INTERNAL_ERRORTransient server fault. Retry once with backoff.
502UPSTREAM_ERRORVerify with Credence backend unreachable. Retry with backoff.

POST /v1/refresh3.2.

Atomically consumes the presented refresh token to issue a new access token and a new (rotated) refresh token. The original refresh token is invalidated immediately upon success; any attempt to reuse it will result in a 401 Unauthorized error.

AuthNone
Required headersContent-Type: application/json

Origin Validation

The Origin header is optional for server-side callers. When sent, it must match the domain bound to the refresh token. Browser callers send it automatically. If sent and it does not match, the request returns a DOMAIN_MISMATCH error.

Token Rotation Behavior

Upon every successful refresh:

  • Rotation: A new refreshToken is issued. You must replace your locally stored value with this new token.
  • Sliding Expiration: The refresh token's expiration is reset to 7 days from the time of the call. This allows the session to remain active indefinitely, provided it is refreshed before the current token expires.
  • Immediate Invalidation: The previously presented refresh token is invalidated immediately. Do not attempt to reuse or retry with the old value.

Request body

The refresh token returned by /v1/setup is the only required field.

{
  "refreshToken": "rt_550e8400-e29b-41d4-a716-446655440000"
}
FieldTypeRequiredDescription
refreshTokenstringyesOpaque refresh token from a prior /v1/setup. Prefixed rt_.

Example

curl -X POST https://credenceid.com/cloudsdk/playground/v1/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "rt_550e8400-e29b-41d4-a716-446655440000" }'

Response 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs…",
  "refreshToken": "rt_550e8400-e29b-41d4-a716-446655440000",
  "expiresIn": 900,
  "tokenType": "Bearer"
}
FieldTypeDescription
accessTokenstringNew HS256 JWT. Use as Authorization: Bearer <token>. Valid for 15 minutes.
refreshTokenstringNew refresh token. Replace the value you stored at /v1/setup (or your last /v1/refresh) with this one. The old token is now invalid.
expiresInintAccess-token lifetime in seconds (currently 900).
tokenTypestringAlways "Bearer".

customerName and profileName are not returned on refresh.

Errors

HTTPerrorWhen
400INVALID_REQUESTrefreshToken is blank, body malformed, or Origin header is malformed.
401REFRESH_TOKEN_EXPIREDRefresh token not found or older than 7 days. Call /v1/setup again.
403DOMAIN_MISMATCHOrigin was sent but doesn't match the domain bound to this refresh token.
429RATE_LIMIT_EXCEEDEDMore than 60 requests/minute from this IP. Honor Retry-After.
500INTERNAL_ERRORTransient server fault. Retry once with backoff.

04Digital Credentials API

Flow A: Initiates a new DC API ISO 18013 verification session. Returns a session ID and the encrypted credential request payload to pass to navigator.credentials.get() in the browser.

POST /v1/getDocRequest4.1.
AuthBearer access token (JWT-protected)
Required headersAuthorization
OriginRequired for browser callers. Must match the domain bound to the access token.

Request: body is empty.

curl -X POST https://credenceid.com/cloudsdk/playground/v1/getDocRequest \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response 201 Created

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "dcRequest": {
    "deviceRequest": "omdkb2NUeXBlcy4u…",
    "encryptionInfo": "omVub25jZVgu…"
  }
}
FieldTypeDescription
sessionIdstringOpaque session identifier. Pass to /v1/verifyDocRequest.
dcRequestobjectContains the fields needed for the navigator.credentials.get() call. Pass dcRequest.deviceRequest as the data field.
dcRequest.deviceRequeststringBase64url-encoded CBOR DeviceRequest.
dcRequest.encryptionInfostringBase64url-encoded encryption parameters (nonce + ephemeral ECDH public key) used by the SDK to decrypt the wallet's response.

Errors

HTTPerrorWhen
401MISSING_AUTH_CONTEXTAuth filter did not populate the request context (missing/invalid Authorization header).
401INVALID_TOKENJWT malformed, signed with the wrong key, or scheme is not Bearer.
401TOKEN_EXPIREDAccess token expired. Call /v1/refresh.
401INVALID_LICENSE_KEYLicense key embedded in the token no longer recognized.
401LICENSE_EXPIRED_OR_REVOKEDLicense has expired or been revoked.
403DOMAIN_MISMATCHOrigin doesn't match the domain bound to the access token.
403PROFILE_NOT_FOUNDProfile ID in the token no longer belongs to this license.
422PROFILE_NOT_SUPPORTEDMulti-document ADVANCED profiles are not yet supported.
500INTERNAL_ERRORTransient server fault. Retry once with backoff.
502UPSTREAM_ERRORVerify with Credence backend unreachable. Retry with backoff.
503READER_AUTH_UNAVAILABLEReader signing key is not configured. Contact support.

POST /v1/verifyDocRequest4.2.

Decodes and verifies the encrypted wallet response returned by navigator.credentials.get(), runs cryptographic checks (issuer signature, IACA trust chain, mdoc binding), and returns the extracted identity claims.

AuthBearer access token (JWT-protected)
Required headersAuthorization, Content-Type: application/json
OriginRequired for browser callers. Must match the domain bound to the access token.

Request body

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "data": "<base64url CBOR from credential.data>"
}
FieldTypeRequiredDescription
sessionIdstringyesSession ID returned by /v1/getDocRequest.
datastringyesBase64url-encoded encrypted CBOR wallet response. The data field from the credential object returned by navigator.credentials.get() in the browser.

Example

curl -X POST https://credenceid.com/cloudsdk/playground/v1/verifyDocRequest \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "data": "eyJhbGciOiJFQ0RILUVTK..."
  }'

Response 200 OK: verification result

{
  "res": true,
  "resDetails": "{\"family_name\":\"Smith\",\"given_name\":\"Jane\",\"birth_date\":\"1990-01-15\"}"
}
FieldTypeDescription
resbooleantrue if all cryptographic checks passed; false otherwise.
resDetailsstringWhen res=true: stringified JSON of the verified identity claims. When res=false: a human-readable error description. Always a string, never a parsed object. Caller must JSON.parse() if true.

Response 200 OK: user cancelled in wallet

{
  "cancelled": true,
  "message":   "Verification was cancelled by the user."
}
FieldTypeDescription
cancelledbooleanAlways true in this shape.
messagestringReason surfaced to the verifier.

Cancellation and verification failure are two different response shapes, both returned as HTTP 200. Check for the cancelled field first; otherwise read res / resDetails.

Errors

HTTPerrorWhen
401MISSING_AUTH_CONTEXTAuth filter did not populate the request context.
401INVALID_TOKENJWT malformed, signed with the wrong key, or scheme is not Bearer.
401TOKEN_EXPIREDAccess token expired. Call /v1/refresh.
401INVALID_LICENSE_KEYLicense key embedded in the token no longer recognized.
401LICENSE_EXPIRED_OR_REVOKEDLicense has expired or been revoked.
403DOMAIN_MISMATCHOrigin doesn't match the domain bound to the access token.
403PROFILE_NOT_FOUNDProfile ID in the token no longer belongs to this license.
422PROFILE_NOT_SUPPORTEDMulti-document ADVANCED profiles are not yet supported.
500INTERNAL_ERRORCert-load failure, decrypt/parse failure, or unhandled fault. Retry once with backoff.
502UPSTREAM_ERRORVerify with Credence backend unreachable. Retry with backoff.
503READER_AUTH_UNAVAILABLEReader signing key is not configured. Contact support.

05Digital Credentials API with OpenID4VP

Flow B: Initiates a new OpenID4VP v1 verification session for Google Wallet on Android via the Digital Credentials (DC) API. Returns a unique session ID and the complete OpenID4VP credential request payload required for the navigator.credentials.get() call.

POST /dcapi/openid4vp/v1/initiate5.1.
RequirementDescription
AuthBearer access token (JWT-protected).
Required HeadersAuthorization
OriginMandatory for browser callers. The value is captured in the OpenID4VP session transcript at initiation.

Always send the Origin header explicitly.

Request: body is empty.

curl -X POST https://credenceid.com/cloudsdk/playground/dcapi/openid4vp/v1/initiate \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response 200 OK

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "payload": {
    "response_type": "vp_token",
    "response_mode": "dc_api.jwt",
    "nonce": "550e8400-…",
    "dcql_query": { "credentials": [ { "id": "cred1", "format": "mso_mdoc", "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, "claims": [ ... ] } ] },
    "client_metadata": {
      "jwks": { "keys": [ { "kty": "EC", "crv": "P-256", "x": "…", "y": "…", "use": "enc", "kid": "…", "alg": "ECDH-ES" } ] },
      "vp_formats_supported": { "mso_mdoc": { "deviceauth_alg_values": [-7], "issuerauth_alg_values": [-7] } }
    }
  }
}

Top-level fields

FieldTypeDescription
sessionIdstringOpaque session ID. Equal to the nonce. Pass to /dcapi/openid4vp/v1/validate.
payloadobjectComplete OpenID4VP authorization request in DCQL form. Pass to navigator.credentials.get() unchanged.

payload fields (verifier integrators don't usually inspect these, pass the object through)

FieldDescription
response_typeAlways "vp_token".
response_mode"dc_api" (plain) or "dc_api.jwt" (JWE-encrypted response). Server-configured.
nonceAnti-replay nonce; equal to sessionId.
dcql_query.credentialsCredential queries (currently one entry, the mDL).
dcql_query.credentials[].formatAlways "mso_mdoc".
dcql_query.credentials[].meta.doctype_value"org.iso.18013.5.1.mDL".
dcql_query.credentials[].claimsRequested claims (JSON-pointer paths + intent-to-retain flags).
client_metadata.jwksVerifier's ephemeral ECDH-ES P-256 public key. Wallet uses this to encrypt the response.
client_metadata.vp_formats_supportedAccepted COSE algorithm IDs (e.g. -7 = ES256) for device and issuer authentication.

Errors

Errors raised by this controller

HTTPBody
401{ "error": "Authentication context not available." }
400{ "error": "Unsupported profile configuration. Contact your administrator." }
400{ "error": "Unknown document type in profile configuration." }
500{ "error": "Failed to initiate verification: <details>" }

Generic JWT-filter errors:

HTTPerror
401MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED
403DOMAIN_MISMATCH

POST /dcapi/openid4vp/v1/validate5.2.

Decrypts the wallet's JWE response (Flow B), performs cryptographic validation on the embedded mdoc (issuer signature, IACA trust chain, and mdoc binding), and returns the extracted identity claims.

AuthBearer access token (JWT-protected)
Required headersAuthorization, Content-Type: application/json
OriginRequired for browser callers. Must match the domain bound to the access token.

Request body

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "data": "<encrypted token (JWE compact serialization) from credential.data>"
}
FieldTypeRequiredDescription
sessionIdstringyesSession ID returned by /dcapi/openid4vp/v1/initiate.
datastringyesJWE compact serialization of the wallet's VP Token response. The data field from the credential object returned by navigator.credentials.get().
curl -X POST https://credenceid.com/cloudsdk/playground/dcapi/openid4vp/v1/validate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "550e8400-e29b-41d4-a716-446655440000",
    "data": "eyJhbGciOiJFQ0RILUVTK..."
  }'

Response 200 OK

{
  "res": true,
  "resDetails": "{\"identity\":{...},\"authentication\":{...}}"
}
FieldTypeDescription
resbooleanAlways true on this success response.
resDetailsstringStringified JSON of the verified identity claims. Always a string, caller must JSON.parse() it.

Response 200 OK: user cancelled in wallet

{
  "cancelled": true,
  "message":   "Verification was cancelled by the user."
}
FieldTypeDescription
cancelledbooleanAlways true in this shape.
messagestringReason surfaced to the verifier.

Cancellation and result use different shapes. Check for cancelled first; otherwise read res / resDetails.

Errors

Errors raised by this controller

HTTPBodyWhen
401{ "error": "Authentication context not available." }Auth filter passed but context unavailable.
400{ "error": "Validation failed: <exception message>" }Any non-cancellation failure or verification failure.

Generic JWT-filter errors:

HTTPerror
401MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED
403DOMAIN_MISMATCH

06OpenID4VP Annex B

Flow C: Initiates an ISO 18013-7 Annex B OpenID4VP mdoc verification session for QR-based or same-device verification. Returns a session ID, the wallet URI to render as a QR code or deep link, and the protocol-internal request URI.

POST /openid4vp/v1/mdoc/initiate6.1.
AuthBearer access token (JWT-protected)
Required headersAuthorization
OriginRequired for browser callers. Must match the domain bound to the access token.

Request body (optional, application/json)

FieldTypeRequiredDescription
returnUrlstringnoAbsolute HTTPS URL on your registered domain where Samsung Wallet will redirect the user's browser after consent (e.g. https://app.example.com/callback). The backend appends ?response_code=<code> to it. Required for Samsung same-device flows. When omitted, the backend falls back to its own result endpoint (suitable for cross-device / polling-only callers).
curl -X POST https://credenceid.com/cloudsdk/playground/openid4vp/v1/mdoc/initiate \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response 200 OK

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "walletUri": "mdoc-openid4vp://?client_id=…&request_uri=https%3A%2F%2Fcredenceid.com%2Fcloudsdk/playground%2Fopenid4vp%2Fv1%2Fmdoc%2Frequest%2F550e8400-…",
  "requestUri": "https://credenceid.com/cloudsdk/playground/openid4vp/v1/mdoc/request/550e8400-…"
}
FieldTypeDescription
sessionIdUUIDPass to /openid4vp/v1/mdoc/result to poll.
walletUriURIRender as a QR code (cross-device) or open as a deep link (same-device).
requestUriURLWhere the wallet fetches the signed authorization request. Opaque to your code.

Errors

Errors raised by this controller

HTTPBodyWhen
400{ "error": "Unsupported profile configuration. Contact your administrator." }Profile type not supported (UnsupportedProfileTypeException).
400{ "error": "Unknown document type in profile configuration." }Profile references a doctype the use case can't handle (UnknownDocTypeException).
400{"error": "returnUrl must be a valid HTTPS URL: <url>"}returnUrl is not HTTPS or is not a valid URL.
403{"error": "DOMAIN_MISMATCH", "message": "returnUrl origin does not match the registered domain."}returnUrl origin doesn't match the license-bound registered domain.
500{ "error": "Failed to initiate session: <details>" }Any other failure: null auth context (NPE downstream), profile fetch, key generation, session save, etc.

Generic JWT-filter errors:

HTTPerror
401MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED
403DOMAIN_MISMATCH

POST /openid4vp/v1/mdoc/result6.2.

Polls for the verification result of an Annex B mdoc session. Returns 202 Accepted (with a pending status) while the wallet is completing the flow, and 200 OK (including the verification result) once the wallet has successfully posted its response.

AuthBearer access token (JWT-protected)
Required headersAuthorization, Content-Type: application/json
OriginRequired for browser callers. Must match the domain bound to the access token.

Poll every ~2 seconds between calls.

Request body

{ "sessionId": "550e8400-e29b-41d4-a716-446655440000" }
FieldTypeRequiredDescription
sessionIdstringyesSession ID returned by /openid4vp/v1/mdoc/initiate.
curl -X POST https://credenceid.com/cloudsdk/playground/openid4vp/v1/mdoc/result \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "sessionId": "550e8400-e29b-41d4-a716-446655440000" }'

Response 200 OK: verification succeeded

{
  "success":  true,
  "identity": "{\"family_name\":\"Smith\",\"given_name\":\"Jane\",\"birth_date\":\"1990-01-15\"}",
  "error":    null
}

Response 200 OK: verification failed (wallet responded but credential didn't pass checks)

{
  "success":  false,
  "identity": null,
  "error":    "User denied consent."
}
FieldTypeDescription
successbooleantrue if all cryptographic checks passed; false if the wallet returned an error or the credential failed verification.
identitystring or nullStringified JSON of verified identity claims when success=true. null when success=false. Caller must JSON.parse() it.
errorstring or nullHuman-readable failure reason when success=false. null when success=true. Examples: "User denied consent.", "Session timed out.", or a verification-failure description.

This is HTTP 200 even when success=false. Verification failure is not an HTTP error. Use HTTP status to know whether the result is ready; use the success field to know the verification outcome.

Response 202 Accepted: result not yet available, keep polling

{ "status": "pending" }
FieldTypeDescription
statusstringAlways "pending" in this shape.

Errors

Errors raised by this controller

HTTPBodyWhen
404{ "error": "Session not found: <details>" }Session ID does not exist (NoSuchElementException).
500{ "error": "Internal error: <details>" }Unexpected server fault.

Generic JWT-filter errors:

HTTPerror
401MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED
403DOMAIN_MISMATCH

GET /openid4vp/v1/mdoc/resume6.3.

When the wallet redirects the user back to the verifier device with a response_code query parameter, that code is the credential. No Bearer token is required: the code itself is the auth. Single-use.

How the redirect happens: When you pass returnUrl in POST /openid4vp/v1/mdoc/initiate, the backend returns {"redirect_uri": "<returnUrl>?response_code=<code>"} to Wallet after the user consents. Wallet then redirects the user's browser to that URL. Your frontend reads the response_code from the URL query string and calls this endpoint to retrieve the result.

AuthNone. The response_code in the query string authorizes the call.

Query parameters

ParamTypeRequiredDescription
response_codestringyesOpaque single-use token returned to the user's browser by the wallet's redirect after a successful same-device verification.

Request body

None.

curl "https://credenceid.com/cloudsdk/playground/openid4vp/v1/mdoc/resume?response_code=opaque-value"

Response 200 OK: verification succeeded

{
  "success":  true,
  "identity": "{\"family_name\":\"Smith\",\"given_name\":\"Jane\",\"birth_date\":\"1990-01-15\"}",
  "error":    null
}

Response 200 OK: verification failed (wallet responded but credential didn't pass checks)

{
  "success":  false,
  "identity": null,
  "error":    "User denied consent."
}
FieldTypeDescription
successbooleantrue if all cryptographic checks passed; false otherwise.
identitystring or nullStringified JSON of verified identity claims when success=true; null when success=false. Caller must JSON.parse() it.
errorstring or nullHuman-readable failure reason when success=false; null when success=true.

Same response shape as POST /openid4vp/v1/mdoc/result.

Response 202 Accepted: code is valid but the wallet's response is still being processed

{
  "error": "Validation not yet complete"
}

Errors

HTTPBodyWhen
404{ "error": "No session found for response_code" }The code doesn't match any session, or has already been consumed by an earlier call. Single-use is enforced.
202{ "error": "Validation not yet complete" }Code recognized but the wallet's response hasn't finished processing yet. Treat as pending; do not retry resume, fall back to /v1/mdoc/result polling.

07OpenID4VP, W3C Verifiable Credential

Flow D: Initiates a W3C OpenID4VP verification session for state-issued mDL apps using direct_post response mode and did:web identity (CA DMV). Returns a session ID, the full authorization request object, the wallet callback URL, and the request URI for QR / deep-link delivery.

POST /w3c/openid4vp/v1/initiate7.1.
AuthBearer access token (JWT-protected)
Required headersAuthorization
OriginRequired for browser callers. Must match the domain bound to the access token.

Request body

Empty.

curl -X POST https://credenceid.com/cloudsdk/playground/w3c/openid4vp/v1/initiate \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response 200 OK

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "authorizationRequest": {
    "client_id": "did:web:credenceid.com:cloudsdk/playground",
    "client_id_scheme": "did",
    "response_type": "vp_token",
    "response_mode": "direct_post",
    "response_uri": "https://credenceid.com/cloudsdk/playground/w3c/openid4vp/v1/callback",
    "nonce": "…",
    "state": "…",
    "presentation_definition": { … }
  },
  "callbackUrl": "https://credenceid.com/cloudsdk/playground/w3c/openid4vp/v1/callback",
  "requestUri": "https://credenceid.com/cloudsdk/playground/w3c/openid4vp/v1/request/550e8400-…"
}
FieldTypeDescription
sessionIdUUIDPass to /w3c/openid4vp/v1/validate to poll.
authorizationRequestobjectThe full OpenID4VP authorization request, mirrored into the signed JWT served at requestUri.
callbackUrlURLThe wallet's direct_post callback URL where the VP Token will be received.
requestUriURLWhat the wallet fetches: returns a signed ES256 JWT with kid resolving via did:web so the wallet can verify the signature.

authorizationRequest payload (verifier integrators don't usually inspect, pass through to the wallet)

FieldDescription
response_typeAlways "vp_token".
response_modeAlways "direct_post" (wallet POSTs back to the server, not the browser).
response_uriServer endpoint where wallet posts the VP Token. Equal to callbackUrl.
client_idVerifier identity (did:web:<domain>).
client_id_schemeAlways "did". Wallet resolves client_id via did:web to verify the JAR signature.
nonce, stateAnti-replay nonce and session correlator.
presentation_definitionW3C Presentation Exchange query. Describes the credential and claims to request.
client_metadataVerifier metadata (DID document hints, supported VC formats, etc.).

Build the deep link

openid4vp://authorize?client_id=<authorizationRequest.client_id>&request_uri=<requestUri>

Errors

Errors raised by this controller

HTTPBodyWhen
400{ "error": "Unsupported profile configuration. Contact your administrator." }Profile type not supported (UnsupportedProfileTypeException).
400{ "error": "Unknown document type in profile configuration." }Profile references a doctype the use case can't handle (UnknownDocTypeException).
500{ "error": "Failed to initiate verification: <details>" }Any other failure: null auth context (NPE from !!), profile fetch error, JWT signing failure, etc.

Generic JWT-filter errors:

HTTPerror
401MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED
403DOMAIN_MISMATCH

POST /w3c/openid4vp/v1/validate7.2.

Polls for the verification result of a W3C OpenID4VP session. Returns 202 Accepted (with a not yet completed status) while the wallet flow is in progress, and 200 OK (containing the full verification result) once the wallet has posted its callback.

AuthBearer access token (JWT-protected)
Required headersAuthorization, Content-Type: application/json
OriginRequired for browser callers. Must match the domain bound to the access token.

Poll every ~2 seconds between calls.

Request body

{ "sessionId": "550e8400-e29b-41d4-a716-446655440000" }
FieldTypeRequiredDescription
sessionIdstringyesSession ID returned by /w3c/openid4vp/v1/initiate.
curl -X POST https://credenceid.com/cloudsdk/playground/w3c/openid4vp/v1/validate \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "sessionId": "550e8400-e29b-41d4-a716-446655440000" }'

Response 200 OK: verification succeeded

{
  "success":  true,
  "claims": {
    "given_name":  "Jane",
    "family_name": "Smith",
    "birth_date":  "1990-01-15"
  },
  "issuer":            "did:web:issuer.example.com",
  "issuanceDate":      "2024-01-15T10:30:00Z",
  "expirationDate":    "2029-01-15T10:30:00Z",
  "credentialType":    ["VerifiableCredential", "DriversLicenseCredential"],
  "verificationDetails": {
    "vpSignatureValid":      true,
    "vcSignatureValid":      true,
    "nonceMatches":          true,
    "audienceMatches":       true,
    "issuerTrusted":         true,
    "notExpired":            true,
    "credentialTypeMatches": true
  }
}

Response 200 OK: verification failed

{
  "success": false,
  "error":   "Issuer not in trusted list."
}

Response 200 fields

FieldTypeDescription
successbooleantrue if all seven cryptographic / trust checks passed; false otherwise.
claimsobject or nullParsed (not stringified) credential subject claims when success=true. null when success=false.
issuerstring or nullDID or URL of the credential issuer. null when success=false.
issuanceDatestring or nullISO-8601 issuance date of the VC. null when success=false.
expirationDatestring or nullISO-8601 expiration date of the VC. null if no expiry set or success=false.
credentialTypearray or nullStrings from the VC type array. null when success=false.
verificationDetailsobject or nullGranular result of each individual check (see below). null when success=false.
errorstring or nullHuman-readable failure reason. null when success=true.

verificationDetails fields (only present when success=true)

FieldTypeMeaning when true
vpSignatureValidbooleanOuter VP JWT signature is valid.
vcSignatureValidbooleanInner VC JWT signature is valid.
nonceMatchesbooleanVP nonce matches the session nonce (anti-replay).
audienceMatchesbooleanVP audience matches the verifier's expected client_id URI.
issuerTrustedbooleanIssuer DID/URL is in the configured trusted-issuers list.
notExpiredbooleanVC has not passed its expiration time.
credentialTypeMatchesbooleanVC type matches what the presentation definition requested.

Response 202 Accepted: result not yet available, keep polling

{
  "error": "Session is not yet completed. Status: PENDING"
}

Errors

Errors raised by this controller

HTTPBodyWhen
202{ "error": "Session is not yet completed. Status: PENDING" }Polling. Wallet hasn't responded yet. Wait and retry.
400{ "error": "<exception message>" }Session not found, callback parse error, or any other failure. Caller must parse the message.

Generic JWT-filter errors:

HTTPerror
401MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED
403DOMAIN_MISMATCH

Polling pattern (illustrative, Python)

import time, requests

def wait_for_result(base, token, session_id, interval=2.0, timeout=180):
    deadline = time.time() + timeout
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    while time.time() < deadline:
        r = requests.post(
            f"{base}/w3c/openid4vp/v1/validate",
            json={"sessionId": session_id},
            headers=headers,
        )
        if r.status_code == 200:
            return r.json()                       # full result with claims/verificationDetails
        if r.status_code == 202:
            time.sleep(interval); continue        # still pending
        r.raise_for_status()                      # 4xx/5xx -> raise
    raise TimeoutError("Verification did not complete in time.")

08Rate Limits

Rate limiting is per-IP, using a token bucket (greedy refill). Limits apply to bootstrap endpoints and wallet-facing endpoints. Defaults are below and tunable via environment variables.

EndpointDefault limitNotes
POST /v1/setup30/min/IPActivation.
POST /v1/refresh60/min/IPToken refresh.
GET /openid4vp/v1/mdoc/request/{id}120/min/IPWallet fetches signed JAR.
GET /w3c/openid4vp/v1/request/{id}120/min/IPWallet fetches signed JAR.
GET /openid4vp/v1/mdoc/resume120/min/IPSame-device redirect resume.
POST /openid4vp/v1/mdoc/response60/min/IPWallet posts authorization response.
POST /w3c/openid4vp/v1/callback60/min/IPWallet posts VP token.

When a limit is exceeded the server returns 429 Too Many Requests with a Retry-After header indicating seconds until the limit resets.