Wallet SDK
A Kotlin Multiplatform library for building mobile identity wallets - provisioning, storage, presentation, and document capture through one stable API.
The Wallet SDK gives you a complete, standards-compliant credential pipeline for Android and iOS. You integrate against clean engine interfaces; the SDK handles OpenID4VCI issuance, hardware-backed storage, ISO 18013-5 / OpenID4VP presentation, and on-device document and biometric capture underneath.
implementation("com.credenceid:wallet-sdk:1.0.0-SNAPSHOT")At a glance
- Platforms
- Android (API 26+) · iOS 14+ (XCFramework)
- Language
- Kotlin Multiplatform 2.2.0 · JVM 11
- iOS interop
- Coroutine StateFlow bridged to Swift AsyncSequence via SKIE - no manual bridging
- Architecture
- Clean / Hexagonal, strict inward dependency flow
- Distribution
- Android via CredenceID Maven (Nexus); iOS as a pre-built static WalletSDK.xcframework
Four engines, one singleton
You interact with the SDK through a single process-scoped WalletSdk instance. Engines are lazily initialized on first access - you never instantiate implementation classes directly.
WalletSdk (singleton)
├─ ProvisioningEngine → OpenID4VCI credential issuance
├─ StorageEngine → hardware-backed vault (StateFlow, delete)
├─ PresentationEngine → ISO 18013-5 QR/BLE, OpenID4VP, DC API
└─ DocumentCaptureEngine → OCR, MRZ, NFC MRTD, livenessval sdk = WalletSdk.getInstance(
issuerIdentifier = IssuerIdentifier.ClientId("your_client_id"),
issuerServiceUrl = "https://your-issuer.credenceid.com/issuer-service",
context = applicationContext, // Android only - null on iOS
storageMode = StorageMode.PERSISTENT,
reviewCenterUrl = "https://your-reviewcenter.credenceid.com/reviewcenter",
)getInstance() is thread-safe via double-checked locking. Same issuerIdentifier + issuerServiceUrl returns the cached instance with no network call; change either and the SDK discards the old instance and re-runs POST /api/public/wallet/init automatically.
Provisioning - OpenID4VCI, two phases
Issuance follows the OpenID4VCI pre-authorized code flow, split into two clear phases:
Phase 1 - Submit
submitProvisioningRequest() sends the applicant's identity to the issuer and persists a PENDING credential locally.
Phase 2 - Fetch
The issuer pushes an openid-credential-offer:// URI out-of-band (deep link or QR). fetchReadyCredential() exchanges it for the signed mDoc and flips the credential to VALID.
sdk.provisioningEngine.submitProvisioningRequest(params)
.onSuccess { /* PENDING credential now observable in the vault */ }
.onFailure { error -> showError(error.message) }
// later, when the offer URI arrives via deep link or QR scan:
sdk.provisioningEngine.fetchReadyCredential(credentialOfferUri)
.onSuccess { credential -> /* now VALID */ }
.onFailure { error -> /* handle TIMEOUT, NETWORK_ERROR, INVALID_OFFER */ }Deep-link and QR delivery converge on the same call - decode the QR with any standard scanner (ZXing / ML Kit) and pass the raw string; the pre-authorized code is extracted from the offer payload for you.
Storage - reactive, hardware-backed
The StorageEngine exposes the vault as a StateFlow<List<DigitalCredential>>. Collect it once; it emits the current list immediately and again on every change. No polling, no manual refresh.
sdk.storageEngine.storedCredentials.collectLatest { credentials ->
_credentialList.value = credentials
}Credentials persist to on-device SQLite (PERSISTENT) or stay in memory for tests (EPHEMERAL). Keys are held in the platform SecureArea - Android KeyStore or iOS Secure Enclave. deleteCredential() erases the mDoc payload and destroys the hardware key irreversibly, so gate it behind explicit user confirmation.
Presentation - three modes, one state machine
All three modes run through PresentationEngine and share a single StateFlow<PresentationState>.
| Mode | Trigger | Protocol |
|---|---|---|
| In-person | Holder taps "Show credential" | ISO 18013-5 (QR + BLE) |
| Online | Deep link from a verifier site | OpenID4VP / ISO 18013-7 |
| Browser (DC API) | Web page calls navigator.credentials.get() | OpenID4VP, mdoc Annex C |
- Idle
- EngagementReady
- Connecting
- RequestReceived
- ConsentGiven
- Success | Error
Your UI observes the flow and reacts at each transition - render the engagement QR at EngagementReady, show the consent screen with verifierIdentity and requestedClaims at RequestReceived, and handle recoverable vs. unrecoverable failures via error.isRecoverable. The Digital Credentials API browser flow requires Android API 29+, Chrome 128+, and current Google Play Services, and only registers VALID credentials with Credential Manager.
Wallet Toolkit - capture & biometrics
Document capture, NFC, and liveness ship with the core SDK (liveness is an optional module).
- Document capture
- OCR and barcode extraction for national ID cards, passports, and driving licences, driven by an explicit capture-state lifecycle.
- MRTD / NFC
- Read the chip embedded in ePassports and travel documents.
- Liveness
- Passive on-device face capture via the optional wallet-sdk-liveness module (Innovatrics provider).
// optional liveness module
implementation("com.credenceid:wallet-sdk-liveness:1.0.0-SNAPSHOT")Security - RASP built in
Runtime Application Self-Protection is configured through RaspConfig and observable as a violation stream. Detects root/jailbreak, emulators, hooking frameworks, and attached debuggers. Use enforceBlock = false in CI and development so emulator and debugger checks are observe-only rather than blocking.
val config = RaspConfig.Builder()
.enableRootDetection()
.enableEmulatorDetection(enforceBlock = false)
.enableHookingDetection()
.enableDebuggerDetection(enforceBlock = false)
.build()Logging supports pluggable audit sinks and a Redacted<T> wrapper to keep PII out of log output. technicalMessage on error types is for internal logs only - map code enums to your own user-facing strings.
Get started
- Add the CredenceID Maven repo and the wallet-sdk dependency (root coordinate only - never @aar or -android-release, both strip transitive dependencies).
- Request an API key from your CredenceID account representative.
- Initialize WalletSdk in Application.onCreate().
- Run the sample app: ./gradlew :walletsdksample:installDebug