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:

ItemFormatPurposeHow to get it
License keyCS-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxAuthorizes your account to call the API. Long-lived. Treat as a secret.Generate your license key β†’
Profile IDUUID (e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890)Sets the document types, attributes, and trust rules for the verification.Find your Profile ID β†’
Registered domainHTTPS 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.

RequirementDetail
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 flagChrome 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 & DAny 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

Try it first

Explore a live working implementation before you build. Covers all four wallet flows β€” Apple, Google, Samsung, and state-issued mDL.

03Token management

POST /v1/setup returns three values:

FieldMeaning
accessTokenBearer token sent on every API call. Short-lived.
refreshTokenMints a new accessToken without re-authenticating. Valid for 7 days.
expiresInLifetime 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:

StateAction
No session, or refreshToken expired (>7 days)POST /v1/setup
accessToken has >60s remainingReuse it
accessToken expired or <60s remaining, refreshToken validPOST /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 call POST /v1/setup as 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 initiate and result (and optionally resume).

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: the mdoc-openid4vp://… URI to render
  • requestUri: 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 initiate and validate.

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:

ConceptFlow A & BFlow CFlow D
EndpointPOST /v1/verifyDocRequest, POST /dcapi/openid4vp/v1/validatePOST /openid4vp/v1/mdoc/resultPOST /w3c/openid4vp/v1/validate
Success flagres (boolean)success (boolean)success (boolean)
IdentityresDetails: JSON stringidentity: JSON stringclaims: JSON object
Crypto check breakdownresDetails.authentication.* (parse JSON first)n/a (collapsed into success)verificationDetails (object)
CancellationHTTP 200 + { cancelled: true, message }HTTP 200 + { success: false, error: "User denied consent." }HTTP 200 + { success: false, error: … }
Pendingn/a (synchronous)HTTP 202 + { status: "pending" }HTTP 202 + { error: "…not yet completed" }