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 Format | Transport Channel | Compatible Production Wallets |
|---|---|---|
| ISO 18013-5 mdoc | Digital Credentials API | Apple Wallet (iOS) |
| ISO 18013-5 mdoc | Digital Credentials API + OpenID4VP | Google Wallet (Android) |
| ISO 18013-5 mdoc | OpenID4VP Annex B | Samsung Wallet |
| W3C Verifiable Credential | OpenID4VP | State 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
| Header | Value |
|---|---|
Authorization | Bearer <accessToken> β required on all protected endpoints. |
Content-Type | application/json (unless noted) β required on all requests with a body. |
Origin | Your 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:
| Token | TTL | Use |
|---|---|---|
| Access token | 15 minutes (expiresIn: 900) | Authorization: Bearer β¦ on every protected request |
| Refresh token | 7 days | Exchange 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.
| Auth | None |
| Required headers | Origin, 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
licenseKey | string | yes | License key issued by Verify with Credence. Must start with CS-. |
profileId | uuid | yes | UUID 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"
}
| Field | Type | Description |
|---|---|---|
accessToken | string | HS256 JWT. Use as Authorization: Bearer <token>. Embeds the activation domain as a field inside the token. |
refreshToken | string | Opaque token, prefixed rt_. Rotated on every /v1/refresh. Replace your stored value with the new token each time. |
expiresIn | int | Access-token lifetime in seconds (currently 900 = 15 minutes). |
tokenType | string | Always "Bearer". |
customerName | string | Display name of the licensed customer. May be empty. |
profileName | string | Display name of the selected reader profile. May be empty. |
Errors
| HTTP | error | When |
|---|---|---|
| 400 | INVALID_REQUEST | Origin header missing/blank, or body malformed. |
| 401 | INVALID_LICENSE_KEY | License key not recognized by Verify with Credence. |
| 401 | LICENSE_EXPIRED_OR_REVOKED | License has expired or been revoked. |
| 403 | DOMAIN_MISMATCH | Origin doesn't match the registered domain. |
| 403 | PROFILE_NOT_FOUND | Profile ID does not belong to this license. |
| 429 | PLAN_LIMIT_EXCEEDED | Monthly verification quota exhausted. |
| 429 | RATE_LIMIT_EXCEEDED | More 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. |
| 500 | INTERNAL_ERROR | Transient server fault. Retry once with backoff. |
| 502 | UPSTREAM_ERROR | Verify 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.
| Auth | None |
| Required headers | Content-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
refreshTokenis 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
refreshToken | string | yes | Opaque 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"
}
| Field | Type | Description |
|---|---|---|
accessToken | string | New HS256 JWT. Use as Authorization: Bearer <token>. Valid for 15 minutes. |
refreshToken | string | New refresh token. Replace the value you stored at /v1/setup (or your last /v1/refresh) with this one. The old token is now invalid. |
expiresIn | int | Access-token lifetime in seconds (currently 900). |
tokenType | string | Always "Bearer". |
customerName and profileName are not returned on refresh.
Errors
| HTTP | error | When |
|---|---|---|
| 400 | INVALID_REQUEST | refreshToken is blank, body malformed, or Origin header is malformed. |
| 401 | REFRESH_TOKEN_EXPIRED | Refresh token not found or older than 7 days. Call /v1/setup again. |
| 403 | DOMAIN_MISMATCH | Origin was sent but doesn't match the domain bound to this refresh token. |
| 429 | RATE_LIMIT_EXCEEDED | More than 60 requests/minute from this IP. Honor Retry-After. |
| 500 | INTERNAL_ERROR | Transient 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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization |
| Origin | Required 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β¦"
}
}
| Field | Type | Description |
|---|---|---|
sessionId | string | Opaque session identifier. Pass to /v1/verifyDocRequest. |
dcRequest | object | Contains the fields needed for the navigator.credentials.get() call. Pass dcRequest.deviceRequest as the data field. |
dcRequest.deviceRequest | string | Base64url-encoded CBOR DeviceRequest. |
dcRequest.encryptionInfo | string | Base64url-encoded encryption parameters (nonce + ephemeral ECDH public key) used by the SDK to decrypt the wallet's response. |
Errors
| HTTP | error | When |
|---|---|---|
| 401 | MISSING_AUTH_CONTEXT | Auth filter did not populate the request context (missing/invalid Authorization header). |
| 401 | INVALID_TOKEN | JWT malformed, signed with the wrong key, or scheme is not Bearer. |
| 401 | TOKEN_EXPIRED | Access token expired. Call /v1/refresh. |
| 401 | INVALID_LICENSE_KEY | License key embedded in the token no longer recognized. |
| 401 | LICENSE_EXPIRED_OR_REVOKED | License has expired or been revoked. |
| 403 | DOMAIN_MISMATCH | Origin doesn't match the domain bound to the access token. |
| 403 | PROFILE_NOT_FOUND | Profile ID in the token no longer belongs to this license. |
| 422 | PROFILE_NOT_SUPPORTED | Multi-document ADVANCED profiles are not yet supported. |
| 500 | INTERNAL_ERROR | Transient server fault. Retry once with backoff. |
| 502 | UPSTREAM_ERROR | Verify with Credence backend unreachable. Retry with backoff. |
| 503 | READER_AUTH_UNAVAILABLE | Reader 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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization, Content-Type: application/json |
| Origin | Required 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>"
}
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | yes | Session ID returned by /v1/getDocRequest. |
data | string | yes | Base64url-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\"}"
}
| Field | Type | Description |
|---|---|---|
res | boolean | true if all cryptographic checks passed; false otherwise. |
resDetails | string | When 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."
}
| Field | Type | Description |
|---|---|---|
cancelled | boolean | Always true in this shape. |
message | string | Reason surfaced to the verifier. |
Cancellation and verification failure are two different response shapes, both returned as HTTP 200. Check for the
cancelledfield first; otherwise readres/resDetails.
Errors
| HTTP | error | When |
|---|---|---|
| 401 | MISSING_AUTH_CONTEXT | Auth filter did not populate the request context. |
| 401 | INVALID_TOKEN | JWT malformed, signed with the wrong key, or scheme is not Bearer. |
| 401 | TOKEN_EXPIRED | Access token expired. Call /v1/refresh. |
| 401 | INVALID_LICENSE_KEY | License key embedded in the token no longer recognized. |
| 401 | LICENSE_EXPIRED_OR_REVOKED | License has expired or been revoked. |
| 403 | DOMAIN_MISMATCH | Origin doesn't match the domain bound to the access token. |
| 403 | PROFILE_NOT_FOUND | Profile ID in the token no longer belongs to this license. |
| 422 | PROFILE_NOT_SUPPORTED | Multi-document ADVANCED profiles are not yet supported. |
| 500 | INTERNAL_ERROR | Cert-load failure, decrypt/parse failure, or unhandled fault. Retry once with backoff. |
| 502 | UPSTREAM_ERROR | Verify with Credence backend unreachable. Retry with backoff. |
| 503 | READER_AUTH_UNAVAILABLE | Reader 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.
| Requirement | Description |
|---|---|
| Auth | Bearer access token (JWT-protected). |
| Required Headers | Authorization |
| Origin | Mandatory 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
| Field | Type | Description |
|---|---|---|
sessionId | string | Opaque session ID. Equal to the nonce. Pass to /dcapi/openid4vp/v1/validate. |
payload | object | Complete 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)
| Field | Description |
|---|---|
response_type | Always "vp_token". |
response_mode | "dc_api" (plain) or "dc_api.jwt" (JWE-encrypted response). Server-configured. |
nonce | Anti-replay nonce; equal to sessionId. |
dcql_query.credentials | Credential queries (currently one entry, the mDL). |
dcql_query.credentials[].format | Always "mso_mdoc". |
dcql_query.credentials[].meta.doctype_value | "org.iso.18013.5.1.mDL". |
dcql_query.credentials[].claims | Requested claims (JSON-pointer paths + intent-to-retain flags). |
client_metadata.jwks | Verifier's ephemeral ECDH-ES P-256 public key. Wallet uses this to encrypt the response. |
client_metadata.vp_formats_supported | Accepted COSE algorithm IDs (e.g. -7 = ES256) for device and issuer authentication. |
Errors
Errors raised by this controller
| HTTP | Body |
|---|---|
| 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:
| HTTP | error |
|---|---|
| 401 | MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED |
| 403 | DOMAIN_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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization, Content-Type: application/json |
| Origin | Required 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>"
}
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | yes | Session ID returned by /dcapi/openid4vp/v1/initiate. |
data | string | yes | JWE 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\":{...}}"
}
| Field | Type | Description |
|---|---|---|
res | boolean | Always true on this success response. |
resDetails | string | Stringified 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."
}
| Field | Type | Description |
|---|---|---|
cancelled | boolean | Always true in this shape. |
message | string | Reason surfaced to the verifier. |
Cancellation and result use different shapes. Check for
cancelledfirst; otherwise readres/resDetails.
Errors
Errors raised by this controller
| HTTP | Body | When |
|---|---|---|
| 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:
| HTTP | error |
|---|---|
| 401 | MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED |
| 403 | DOMAIN_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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization |
| Origin | Required for browser callers. Must match the domain bound to the access token. |
Request body (optional, application/json)
| Field | Type | Required | Description |
|---|---|---|---|
returnUrl | string | no | Absolute 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-β¦"
}
| Field | Type | Description |
|---|---|---|
sessionId | UUID | Pass to /openid4vp/v1/mdoc/result to poll. |
walletUri | URI | Render as a QR code (cross-device) or open as a deep link (same-device). |
requestUri | URL | Where the wallet fetches the signed authorization request. Opaque to your code. |
Errors
Errors raised by this controller
| HTTP | Body | When |
|---|---|---|
| 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:
| HTTP | error |
|---|---|
| 401 | MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED |
| 403 | DOMAIN_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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization, Content-Type: application/json |
| Origin | Required 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" }
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | yes | Session 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."
}
| Field | Type | Description |
|---|---|---|
success | boolean | true if all cryptographic checks passed; false if the wallet returned an error or the credential failed verification. |
identity | string or null | Stringified JSON of verified identity claims when success=true. null when success=false. Caller must JSON.parse() it. |
error | string or null | Human-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 thesuccessfield to know the verification outcome.
Response 202 Accepted: result not yet available, keep polling
{ "status": "pending" }
| Field | Type | Description |
|---|---|---|
status | string | Always "pending" in this shape. |
Errors
Errors raised by this controller
| HTTP | Body | When |
|---|---|---|
| 404 | { "error": "Session not found: <details>" } | Session ID does not exist (NoSuchElementException). |
| 500 | { "error": "Internal error: <details>" } | Unexpected server fault. |
Generic JWT-filter errors:
| HTTP | error |
|---|---|
| 401 | MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED |
| 403 | DOMAIN_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
returnUrlinPOST /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 theresponse_codefrom the URL query string and calls this endpoint to retrieve the result.
| Auth | None. The response_code in the query string authorizes the call. |
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
response_code | string | yes | Opaque 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."
}
| Field | Type | Description |
|---|---|---|
success | boolean | true if all cryptographic checks passed; false otherwise. |
identity | string or null | Stringified JSON of verified identity claims when success=true; null when success=false. Caller must JSON.parse() it. |
error | string or null | Human-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
| HTTP | Body | When |
|---|---|---|
| 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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization |
| Origin | Required 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-β¦"
}
| Field | Type | Description |
|---|---|---|
sessionId | UUID | Pass to /w3c/openid4vp/v1/validate to poll. |
authorizationRequest | object | The full OpenID4VP authorization request, mirrored into the signed JWT served at requestUri. |
callbackUrl | URL | The wallet's direct_post callback URL where the VP Token will be received. |
requestUri | URL | What 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)
| Field | Description |
|---|---|
response_type | Always "vp_token". |
response_mode | Always "direct_post" (wallet POSTs back to the server, not the browser). |
response_uri | Server endpoint where wallet posts the VP Token. Equal to callbackUrl. |
client_id | Verifier identity (did:web:<domain>). |
client_id_scheme | Always "did". Wallet resolves client_id via did:web to verify the JAR signature. |
nonce, state | Anti-replay nonce and session correlator. |
presentation_definition | W3C Presentation Exchange query. Describes the credential and claims to request. |
client_metadata | Verifier 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
| HTTP | Body | When |
|---|---|---|
| 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:
| HTTP | error |
|---|---|
| 401 | MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED |
| 403 | DOMAIN_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.
| Auth | Bearer access token (JWT-protected) |
| Required headers | Authorization, Content-Type: application/json |
| Origin | Required 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" }
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | yes | Session 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
| Field | Type | Description |
|---|---|---|
success | boolean | true if all seven cryptographic / trust checks passed; false otherwise. |
claims | object or null | Parsed (not stringified) credential subject claims when success=true. null when success=false. |
issuer | string or null | DID or URL of the credential issuer. null when success=false. |
issuanceDate | string or null | ISO-8601 issuance date of the VC. null when success=false. |
expirationDate | string or null | ISO-8601 expiration date of the VC. null if no expiry set or success=false. |
credentialType | array or null | Strings from the VC type array. null when success=false. |
verificationDetails | object or null | Granular result of each individual check (see below). null when success=false. |
error | string or null | Human-readable failure reason. null when success=true. |
verificationDetails fields (only present when success=true)
| Field | Type | Meaning when true |
|---|---|---|
vpSignatureValid | boolean | Outer VP JWT signature is valid. |
vcSignatureValid | boolean | Inner VC JWT signature is valid. |
nonceMatches | boolean | VP nonce matches the session nonce (anti-replay). |
audienceMatches | boolean | VP audience matches the verifier's expected client_id URI. |
issuerTrusted | boolean | Issuer DID/URL is in the configured trusted-issuers list. |
notExpired | boolean | VC has not passed its expiration time. |
credentialTypeMatches | boolean | VC 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
| HTTP | Body | When |
|---|---|---|
| 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:
| HTTP | error |
|---|---|
| 401 | MISSING_TOKEN, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_LICENSE_KEY, LICENSE_EXPIRED_OR_REVOKED |
| 403 | DOMAIN_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.
| Endpoint | Default limit | Notes |
|---|---|---|
POST /v1/setup | 30/min/IP | Activation. |
POST /v1/refresh | 60/min/IP | Token refresh. |
GET /openid4vp/v1/mdoc/request/{id} | 120/min/IP | Wallet fetches signed JAR. |
GET /w3c/openid4vp/v1/request/{id} | 120/min/IP | Wallet fetches signed JAR. |
GET /openid4vp/v1/mdoc/resume | 120/min/IP | Same-device redirect resume. |
POST /openid4vp/v1/mdoc/response | 60/min/IP | Wallet posts authorization response. |
POST /w3c/openid4vp/v1/callback | 60/min/IP | Wallet 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.