Cloud SDK - Integration
Step-by-step walkthrough to add the Credence ID Cloud SDK to your web app, covering activation, the four wallet flows, polling, and response handling.
01Prerequisites
You need these three values from the Verify with Credence portal:
| Item | Format | Purpose | How to get it |
|---|---|---|---|
| License key | CS-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx | Authorizes your account to call the API. Long-lived. Treat as a secret. | Generate your license key β |
| Profile ID | UUID (e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890) | Sets the document types, attributes, and trust rules for the verification. | Find your Profile ID β |
| Registered domain | HTTPS origin (e.g. https://app.example.com) | Required in the Origin header on POST /v1/setup. On protected endpoints, browser callers send it automatically; server-side callers may omit it. | Set during onboarding. |
A single license key may have multiple profiles. To switch profiles, activate again with the new Profile ID.
02Cloud SDK Requirements
The Cloud SDK supports four verification flows:
- Flow A: Digital Credentials API with ISO mdoc (e.g. Apple Wallet on iPhone)
- Flow B: Digital Credentials API with OpenID4VP and ISO mdoc (e.g. Google Wallet on Android)
- Flow C: OpenID4VP Annex B with ISO mdoc (e.g. Samsung Wallet)
- Flow D: OpenID4VP with W3C Verifiable Credential (e.g. CA DMV mDL app)
Any wallet implementing the same protocol is compatible. The examples above are validated production wallets.
The requirements below depend on which flow you'll use. Full step-by-step details are in sections 04β07.
| Requirement | Detail |
|---|---|
| Browser: supported (DC API, Flows A & B) | Chrome 141+ on Android 9+ Β· Chrome 141+ desktop (Windows / macOS / Linux) Β· Safari 17.4+ on iOS 17.4+ |
| Browser: supported with flag | Chrome 128β140 (desktop and Android). Users must enable chrome://flags#web-identity-digital-credentials once. No flag needed on 141+. |
| Browser: not supported (DC API) | Firefox (any version); any in-app webview. Users on these must use Flow C or D (QR / deep link, any browser). |
| Browser: Flows C & D | Any browser, any device β renders a QR code or opens a deep link. |
Compatible production wallet examples
- Google Wallet β ISO mdoc holder on Android 9+
- Apple Wallet β ISO mdoc holder on iPhone (iOS 17.4+, iPhone 11 or later)
- Samsung Wallet β ISO 18013-7 Annex B compliant mdocs on Samsung devices
- CA DMV mDL app β California DMV's standalone W3C VC wallet
03Token management
POST /v1/setup returns three values:
| Field | Meaning |
|---|---|
accessToken | Bearer token sent on every API call. Short-lived. |
refreshToken | Mints a new accessToken without re-authenticating. Valid for 7 days. |
expiresIn | Lifetime of accessToken in seconds. |
Do not call /setup before every request. Cache the session and compute an absolute expiresAt = Date.now() + expiresIn * 1000. Persist accessToken, refreshToken, and expiresAt in sessionStorage (browser) or a shared store (server).
Before each API call, branch on stored state:
| State | Action |
|---|---|
No session, or refreshToken expired (>7 days) | POST /v1/setup |
accessToken has >60s remaining | Reuse it |
accessToken expired or <60s remaining, refreshToken valid | POST /v1/refresh |
The 60-second buffer protects against clock skew and in-flight requests racing expiry. Once the refreshToken expires, a full /setup is the only path forward; there's no way to extend it. Long-lived sessions (kiosks, background jobs) must handle this.
Authorization for all flows. Every flow below sends
Authorization: Bearer <accessToken>to the Cloud SDK. Reuse a cached token, refresh it, or callPOST /v1/setupas described above. The flow-specific steps assume a valid bearer token is already in hand.
04Flow A: Digital Credentials API
This flow verifies an mDL through the user's native wallet using the W3C Digital Credentials API with the org-iso-mdoc protocol.
Same-device and cross-device. Both work without any extra endpoint. The browser API picks the wallet on the same device, or pairs to a phone over Bluetooth (Chrome cross-device). Either way, the result is returned synchronously through
navigator.credentials.get(). No polling step, no resume endpoint.
sequenceDiagram
participant Server as Your Server
participant API as Cloud SDK API
participant Browser as Your Browser Page
participant Wallet
Server->>API: POST /v1/setup
API-->>Server: tokens
Server->>API: POST /v1/getDocRequest
API-->>Server: { sessionId, dcRequest }
Server-->>Browser: send dcRequest to browser
Browser->>Wallet: credentials.get()
Wallet-->>Browser: encrypted resp
Browser-->>Server: walletResponse (data)
Server->>API: POST /v1/verifyDocRequest
API-->>Server: { res, resDetails }
Step 1: Server: initiate the session
Call POST /v1/getDocRequest. Pass the resulting sessionId and dcRequest object to your web page.
Step 2: Browser: invoke the wallet
Browser-side only. This step runs in the user's browser via navigator.credentials.get(). It cannot be done from your backend. The call must originate from the page the user is on.
The browser handles both same-device and cross-device flows automatically β no extra code is required. For cross-device, Chrome shows its own Bluetooth pairing UI; your app does not need to render a QR code.
const credential = await navigator.credentials.get({
digital: {
requests: [{
protocol: "org-iso-mdoc",
data: dcRequest.deviceRequest // base64url-encoded CBOR DeviceRequest
}]
},
mediation: "required"
});
// Send credential.data back to your server
fetch("/your-backend/verify", {
method: "POST",
body: JSON.stringify({ sessionId, data: credential.data })
});
The wallet returns a base64url-encoded CBOR payload in credential.data. Forward it to your server as a string.
Step 3: Server: verify the wallet's response
Call POST /v1/verifyDocRequest with the sessionId and the wallet's data. The response contains res (boolean) and resDetails (stringified JSON of the verified claims).
05Flow B: Digital Credentials API with OpenID4VP
Same shape as Flow A, but uses OpenID4VP v1, the protocol Google Wallet currently registers for via the DC API on Android.
Requires a valid
accessToken. See Authorization for all flows.
sequenceDiagram
participant Server as Your Server
participant API as Cloud SDK API
participant Browser as Your Browser Page
participant Wallet
Server->>API: POST /v1/setup
API-->>Server: tokens
Server->>API: POST /dcapi/openid4vp/v1/initiate
API-->>Server: { sessionId, payload }
Server-->>Browser: send payload to browser
Browser->>Wallet: credentials.get() Β· protocol "openid4vp"
Wallet-->>Browser: JWE response
Browser-->>Server: credential.data
Server->>API: POST /dcapi/openid4vp/v1/validate
API-->>Server: { res, resDetails }
Step 1: Server: initiate
Call POST /dcapi/openid4vp/v1/initiate. The response contains a payload object (the full OpenID4VP authorization request in DCQL form). Send it to the browser unchanged.
Step 2: Browser: invoke the wallet
Browser-side only. This step runs in the user's browser via navigator.credentials.get(). It cannot be done from your backend. The call must originate from the page the user is on.
The browser handles both same-device and cross-device flows automatically, as with Flow A.
const credential = await navigator.credentials.get({
digital: {
requests: [{
protocol: "openid4vp",
data: payload // entire object from Step 1
}]
},
mediation: "required"
});
The wallet returns a JWE compact serialization in credential.data.
Step 3: Server: validate
Call POST /dcapi/openid4vp/v1/validate. The response shape is identical to Flow A: { res, resDetails }.
06Flow C: OpenID4VP Annex B
Fully server-side. The wallet contacts the Cloud SDK directly; you only render a QR code and poll for the result.
sequenceDiagram
participant Server as Your Server
participant API as Cloud SDK API
participant Wallet
Server->>API: POST /v1/setup
API-->>Server: tokens
Server->>API: POST /openid4vp/v1/mdoc/initiate
API-->>Server: { sessionId, walletUri, requestUri }
Note over Server: render walletUri as QR<br/>(or open as deep link)<br/>user scans / taps
rect rgb(248, 250, 252)
Note over Wallet, API: Protocol-internal<br/>(you don't call these)
Wallet->>API: GET .../mdoc/request/{id}<br/>(signed JAR)
API-->>Wallet: oauth-authz-req+jwt
Wallet->>API: POST .../mdoc/response<br/>(JWE + state)
API-->>Wallet: { redirect_uri }
end
loop poll every 2s
Server->>API: POST /openid4vp/v1/mdoc/result
API-->>Server: 202 Accepted (pending)
end
API-->>Server: 200 { success, identity, error }
Wallet β API lines are protocol-internal. Your code does not call those endpoints. They are part of the OpenID4VP protocol exchange and happen automatically once the user scans the QR or taps the deep link. You only call
initiateandresult(and optionallyresume).
Step 1: Get a valid access token
Every flow below sends Authorization: Bearer <accessToken> to the Cloud SDK. Call POST /v1/setup to get a token, then keep it valid by refreshing or reusing it as described in Token management.
Step 2: Server: initiate
Call POST /openid4vp/v1/mdoc/initiate. The response contains:
walletUri: themdoc-openid4vp://β¦URI to renderrequestUri: where the wallet fetches the signed JAR (opaque to your code)sessionId: identifier you'll poll with
# Cross-device or polling-only (no body needed)
curl -X POST https://credenceid.com/cloudsdk/playground/openid4vp/v1/mdoc/initiate \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Samsung same-device (pass returnUrl so wallet redirects browser back to your app)
curl -X POST https://credenceid.com/cloudsdk/playground/openid4vp/v1/mdoc/initiate \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"returnUrl": "https://app.example.com/callback"}'
Step 3: Deliver the wallet URI
Two delivery modes are supported. Both use the walletUri from Step 2; only the way you put it in front of the user differs.
Cross-device. The verifier runs on a desktop or kiosk; the user has the wallet on their phone. Render walletUri as a QR code (use any QR library). The user scans it with their phone wallet.
Same-device. The verifier and the wallet are on the same phone. Open walletUri as a deep link, triggered by a user click:
<a href="mdoc-openid4vp://...">Verify</a>
The deep link must be triggered by a user gesture. iOS and Chrome block programmatic navigation to non-HTTP schemes that aren't user-initiated.
Step 4: Get the result
Two ways to retrieve the verified credential.
Polling (always works). Call POST /openid4vp/v1/mdoc/result every ~2 seconds. Status 202 = still waiting. Status 200 = result ready (success or failure).
import time, requests, json
def wait_for_result(base, token, session_id, interval=2.0, timeout=180):
deadline = time.time() + timeout
headers = {"Authorization": f"Bearer {token}"}
while time.time() < deadline:
r = requests.post(
f"{base}/openid4vp/v1/mdoc/result",
json={"sessionId": session_id},
headers=headers,
)
if r.status_code == 200:
body = r.json()
return json.loads(body["identity"]) if body["success"] else None
if r.status_code == 202:
time.sleep(interval)
continue
r.raise_for_status()
raise TimeoutError(f"Verification did not complete within {timeout}s")
Same-device resume (Samsung Wallet). Pass returnUrl at initiation so Samsung Wallet knows where to redirect the user's browser after consent. The wallet sends the browser to <returnUrl>?response_code=<code>. Your page reads response_code from the URL and calls GET /openid4vp/v1/mdoc/resume?response_code=<code> to get the result β no polling needed.
returnUrl must be an HTTPS URL on your registered domain. Any path is accepted (e.g. /callback, /verify/done?source=samsung). If omitted, the flow still works via polling but Samsung Wallet has nowhere to redirect the browser.
Exchange the code via GET /openid4vp/v1/mdoc/resume. The code is single-use.
07Flow D: OpenID4VP, W3C Verifiable Credential
Same shape as Flow C, but for W3C Verifiable Credential wallets using did:web identity and direct_post response mode for jwt_vc_json credentials.
Requires a valid
accessToken. See Authorization for all flows.
sequenceDiagram
participant Server as Your Server
participant API as Cloud SDK API
participant Wallet
Server->>API: POST /v1/setup
API-->>Server: tokens
Server->>API: POST /w3c/openid4vp/v1/initiate
API-->>Server: { sessionId, requestUri, authorizationRequest }
Note over Server: build openid4vp://authorize?client_id=β¦&request_uri=β¦<br/>render as QR (or open as deep link)<br/>user scans / taps
rect rgb(248, 250, 252)
Note over Wallet, API: Protocol-internal<br/>(you don't call these)
Wallet->>API: GET .../w3c/.../request/{id}<br/>(signed JAR, did:web)
API-->>Wallet: oauth-authz-req+jwt
Wallet->>API: POST .../w3c/.../callback<br/>(vp_token + state)
API-->>Wallet: { status: "success" }
end
loop poll every 2s
Server->>API: POST /w3c/openid4vp/v1/validate
API-->>Server: 202 Accepted (pending)
end
API-->>Server: 200 { success, claims, verificationDetails }
Wallet β API lines are protocol-internal. Your code does not call those endpoints. They are part of the OpenID4VP protocol exchange and happen automatically once the user scans the QR or taps the deep link. You only call
initiateandvalidate.
Step 1: Server: initiate
Call POST /w3c/openid4vp/v1/initiate. The response contains a requestUri (where the wallet fetches the signed JWT request) and a full authorizationRequest object.
Step 2: Deliver the wallet URI
W3C wallets expect an openid4vp:// URI built from the values in Step 1:
openid4vp://authorize?client_id=<authorizationRequest.client_id>&request_uri=<requestUri>
Two delivery modes are supported, mirroring Flow C.
Cross-device. Render the URI as a QR code on the verifier device (use any QR library). The user scans it with their phone wallet.
Same-device. Open the URI as a deep link, triggered by a user click:
<a href="openid4vp://authorize?...">Verify</a>
Unlike Flow C, there is no same-device resume. W3C OpenID4VP uses direct_post response mode; the wallet POSTs the credential to the Cloud SDK and never redirects the browser back. Both modes use the same polling step below.
Step 3: Poll for the result
Call POST /w3c/openid4vp/v1/validate every ~2 seconds. Status 202 = still waiting. Status 200 = result ready.
The success response includes a parsed claims object and a verificationDetails object with seven checks:
{
"success": true,
"claims": { "given_name": "Jane", "family_name": "Smith", "birth_date": "1990-01-15" },
"verificationDetails": {
"vpSignatureValid": true,
"vcSignatureValid": true,
"nonceMatches": true,
"audienceMatches": true,
"issuerTrusted": true,
"notExpired": true,
"credentialTypeMatches": true
}
}
08Reading Verification Results
The four flows return similar but not identical result formats. Use this table to map between them:
| Concept | Flow A & B | Flow C | Flow D |
|---|---|---|---|
| Endpoint | POST /v1/verifyDocRequest, POST /dcapi/openid4vp/v1/validate | POST /openid4vp/v1/mdoc/result | POST /w3c/openid4vp/v1/validate |
| Success flag | res (boolean) | success (boolean) | success (boolean) |
| Identity | resDetails: JSON string | identity: JSON string | claims: JSON object |
| Crypto check breakdown | resDetails.authentication.* (parse JSON first) | n/a (collapsed into success) | verificationDetails (object) |
| Cancellation | HTTP 200 + { cancelled: true, message } | HTTP 200 + { success: false, error: "User denied consent." } | HTTP 200 + { success: false, error: β¦ } |
| Pending | n/a (synchronous) | HTTP 202 + { status: "pending" } | HTTP 202 + { error: "β¦not yet completed" } |