Skip to main content

Getting started

What is a wallet?​

A Wallet is the single object your app talks to. It owns the credential store and exposes every capability of the SDK through a small set of services:

wallet.credentials // list / get / delete / DCQL match / status / changes
wallet.issuance // OpenID4VCI issuance sessions
wallet.presentation // OpenID4VP remote + Digital Credentials API
wallet.proximity // ISO 18013-5 in-person
wallet.transactions // audit history

You assemble it once — passing a WalletConfig (trust anchors and issuance defaults) and a WalletPorts bundle (the thin platform adapters the pure core needs) — and then hold onto it for the lifetime of your app. The core is fully implemented in Kotlin and mirrored in Swift; both share the same API contract, so the shapes below are identical across platforms.

Assemble a wallet​

val wallet = Wallet.create(
config = WalletConfig(
trust = TrustConfig(
issuerAnchorsDer = issuerCaDerList, // verifies issuer certs & Token Status Lists
readerAnchorsDer = readerCaDerList, // verifies signed VP requests / mdoc reader auth
),
issuance = IssuanceConfig(
clientId = "wallet-dev",
redirectUri = "eudi-wallet://authorize",
),
),
ports = WalletPorts(
secureAreas = listOf(androidKeystoreSecureArea), // hardware-backed keys
storage = encryptedStorageDriver, // your persistence adapter
http = okHttpTransport, // your HTTP adapter
// walletAttestation, clock, rng, logger, transactionLogStore all have defaults
),
)

WalletConfig​

Every field has a default, so WalletConfig() is valid for a first run against an issuer that does not require trust anchors.

FieldTypePurpose
trustTrustConfigX.509 trust anchors used across the SDK, all DER-encoded (List<ByteArray> / [[UInt8]]). issuerAnchorsDer validates issuer signing certificates and status-list signers; readerAnchorsDer validates signed presentation requests (OpenID4VP JAR) and mdoc reader authentication; registrarAnchorsDer validates an RP registration certificate (WRPRC) and its status list. See Trust & audit.
issuanceIssuanceConfigDefaults applied to issuance flows: clientId (default "wallet-dev") and redirectUri (default "eudi-wallet://authorize"). Set these to the values your issuer registered for your app.
presentationPresentationConfigmdoc presentation policy: mdocDeviceAuth (Signature default, or Mac — see Proximity), proximitySessionCurve (P-256 default), mdocTransactionDataBinder (required to bind transaction_data to an mdoc), and verifyRegistrationViaRegistrarApi (opt-in online registrar lookup).
transactionLogTransactionLogConfigrecordFailures (default false) — when true, presentations that fail at submission are also written to the audit log with an ERROR status.
readerAuthReaderAuthSigner?The wallet's ISO 18013-5 reader identity (for wallet.reader, the Read-mDL role): a signer + certificate chain so the holder you read from can authenticate you. Null (default) → your read requests carry no reader auth.

WalletPorts​

Ports are the boundary between the pure, platform-agnostic core and the host. Three are required; the rest have sensible defaults.

PortRequired?Notes
secureAreasYesOne or more SecureArea key stores. In production this must be Android Keystore on Android and the Secure Enclave on iOS — keys are hardware-backed and never leave the device.
storageYesA StorageDriver for encrypted persistence of credentials and SDK state.
httpYesAn HttpTransport (e.g. OkHttp / URLSession) used for all network calls. It must honour request.followRedirects.
walletAttestationNoWalletAttestationProvider for wallet key attestation; defaults to none.
clockNoWalletClock; defaults to the system clock.
rngNoRng; defaults to the platform secure RNG.
loggerNoWalletLogger; defaults to none.
transactionLogStoreNoBacking store for the audit log; defaults to an in-memory store. Provide a persistent one to keep history across restarts.

:::warning Production keys SoftwareSecureArea and InMemoryStorageDriver exist only for tests (they ship in the test kit). Never use them in a released build — they hold key material and credentials in process memory. :::

For unit tests you can wire the software adapters and skip trust anchors entirely:

// Tests only — never ship SoftwareSecureArea / InMemoryStorageDriver
val wallet = Wallet.create(
config = WalletConfig(), // all defaults
ports = WalletPorts(
secureAreas = listOf(SoftwareSecureArea()),
storage = InMemoryStorageDriver(),
http = fakeHttpTransport,
),
)

Your own adapters​

Most apps depend on the ready-made android/ presets (AndroidKeystoreSecureArea, FileStorageDriver, OkHttpTransport, …) — see Android adapters & demo. To target another platform, or to harden a preset, implement the port yourself. Two rules make an adapter correct:

  • SecureArea is the private-key custody boundary: implement createKey, publicKey, sign (raw r||s), keyAgreement, attestation, deleteKey, and declare capabilities honestly (a wallet configured for mdoc Mac checks capabilities.keyAgreement). Private keys must never leave it.
  • HttpTransport must honour request.followRedirects — the OpenID4VCI/VP flows intercept redirects, so a transport that always follows them breaks issuance and presentation.

Use the test kit's SoftwareSecureArea and InMemoryStorageDriver as reference implementations, and qualify your adapter against the shared contract test suites — the same checks that gate the presets:

// From the testkit — throws IllegalStateException on any contract violation.
SecureAreaContract.verify(mySecureArea)
StorageDriverContract.verify(myStorageDriver)

Thread-safety and shutdown​

Wallet is thread-safe and multi-instance — you may share one instance across coroutines / tasks, or create several independent wallets in the same process. When you are done (for example on logout or teardown), call close() to cancel any in-flight issuance, presentation, or proximity sessions and release resources.

wallet.close()

List credentials​

wallet.credentials.list() returns a parsed, format-agnostic view of every stored credential. Reads are suspending (Kotlin) / async (Swift). Without a filter it returns everything (CredentialFilter.All / .all).

val credentials: List<Credential> = wallet.credentials.list() // CredentialFilter.All by default
val one: Credential? = wallet.credentials.get(credentialId)

Read parsed claims and lifecycle​

Each Credential carries format-agnostic metadata (id, format, issuer, display, configurationId) plus a lifecycle. Once a credential is issued, its lifecycle exposes the decoded claims (each a path + value, with value.display() for a human-readable form) and the number of remaining single-use instances.

val credential = wallet.credentials.get(credentialId) ?: return

// Format-agnostic type name
val typeName = when (val f = credential.format) {
is CredentialFormat.SdJwtVc -> f.vct // SD-JWT VC type
is CredentialFormat.MsoMdoc -> f.docType // mdoc docType
}
credential.issuer?.let { println("Issued by ${it.displayName} (${it.url})") }

// Parsed claims are available once the credential is issued
when (val lifecycle = credential.lifecycle) {
is Lifecycle.Issued -> {
for (claim in lifecycle.claims) {
val path = claim.path.joinToString(".") // e.g. "address.locality"
println("$path = ${claim.value.display()}")
}
println("${lifecycle.instances} unused instance(s) remaining")
}
else -> println("not issued yet")
}

Check revocation status​

status(id) resolves the credential's live revocation state against its IETF Token Status List. Unknown means the status list could not be reached — treat it as "retry", not "revoked".

when (wallet.credentials.status(credentialId)) {
CredentialStatus.Valid -> allow()
CredentialStatus.Suspended -> warnTemporarilyBlocked()
CredentialStatus.Invalid -> blockRevoked()
CredentialStatus.Unknown -> retryLater() // status list unreachable
}

Match against a DCQL query​

match(dcqlJson) asks which stored credentials satisfy a DCQL (dcql_query) — the same engine the presentation flow uses, so you can preview satisfiability before a request ever arrives. Given this small query for a PID:

{
"credentials": [
{
"id": "pid",
"format": "dc+sd-jwt",
"meta": { "vct_values": ["urn:eudi:pid:1"] },
"claims": [
{ "path": ["family_name"] },
{ "path": ["given_name"] },
{ "path": ["age_equal_or_over", "18"] }
]
}
]
}

byQuery is keyed by the DCQL credential id ("pid" above); each MatchedCredential tells you which stored credential answers it and exactly which paths would be disclosed.

val match = wallet.credentials.match(dcqlJson)
if (match.satisfiable) {
val candidates = match.byQuery.getValue("pid") // keyed by the DCQL credential id
for (m in candidates) {
println("credential ${m.credential.id} would disclose ${m.disclosedPaths}")
}
}

Observe credential changes​

The store emits a change event whenever a credential is added, updated, or removed — for example after issuance completes, after a presentation consumes a single-use instance, or after a status refresh. Kotlin exposes a Flow; Swift an AsyncStream. Use it to keep your UI in sync.

wallet.credentials.changes.collect { change ->
when (change) {
is CredentialChange.Added -> onAdded(change.id)
is CredentialChange.Updated -> onUpdated(change.id)
is CredentialChange.Removed -> onRemoved(change.id)
}
}

Next steps​

Now that the wallet is assembled, put it to work:

  • Issuance — obtain credentials over OpenID4VCI (pre-authorized & authorization-code flows).
  • Presentation — present remotely (OpenID4VP) or through the browser (Digital Credentials API).
  • Proximity — present in person over ISO/IEC 18013-5 device retrieval.