Docs / Wallets and browser support
Wallets and browser support
Something has to answer the request. By default that's the browser's Digital Credentials API handing it to a wallet the patient has installed — but you can substitute any mediator, which is what makes this testable without a phone.
The three mediators
// 1. Platform wallet (the default) — nothing to pass.
await requestCheckin(myRequest);
// 2. A wallet web app in a tab: a real consent screen, any browser.
import { createWebWalletCredentialGetter } from "@smart-health-checkin/checkin-client";
await requestCheckin(myRequest, {
getCredential: createWebWalletCredentialGetter({ walletUrl: "/wallet.html" }),
});
// 3. Non-interactive mock: instant, fabricated data, for scripted tests.
import { createMockWalletCredentialGetter } from "@smart-health-checkin/checkin-client";
await requestCheckin(myRequest, {
getCredential: createMockWalletCredentialGetter({ origin: location.origin }),
});All three produce byte-identical wire traffic: real CBOR, real COSE signatures, real HPKE encryption, verified the same way. Only the mediator differs — so a flow proven against the web wallet is proven against the protocol.
The demo wallet's own source is worth reading if you're building a responder:
demo/wallet.html
plus demo/src/wallet.ts.
It parses the DeviceRequest, shows the requesting origin and a per-item
consent screen, and signs and seals a DeviceResponse bound to that origin.
Checking support before you offer it
import { detectDcApiSupport } from "@smart-health-checkin/checkin-client";
const support = detectDcApiSupport();
if (support.state === "unsupported") {
// support.reason explains why; show your ordinary form instead
}runCheckin does this for you and returns status: "unsupported" without
ever prompting the patient — nobody sees a button that can't work.
As of this writing the platform API is available in recent Chrome on Android
and Safari 26; elsewhere you'll get unsupported. That's precisely why the
fallback matters, and why the web-wallet mediator exists: it needs nothing but
window.open and postMessage.
The web wallet in a bit more detail
createWebWalletCredentialGetter({ walletUrl, target, timeoutMs }) opens the
wallet (a tab by default; target: "popup" for a window), waits for it to
announce readiness, posts the request, and resolves with the sealed response.
Closing the tab or declining rejects as a decline, which runCheckin reports
as status: "declined".
One wrinkle worth knowing: the wallet can't observe your page's origin from
inside its own tab, so the request message carries verifierOrigin
explicitly. Both sides bind the session transcript to it, and a mismatch makes
the response fail to open — which is the intended behavior, not a bug to work
around.
Because the opener must be a genuine user gesture, call requestCheckin from
a click handler. Automated tests need synthesized input events (a scripted
.click() won't do) or the non-interactive mock.
Key custody
The authority option decides where the verifier's private key lives:
await requestCheckin(myRequest, { authority: "browser-local" }); // default
await requestCheckin(myRequest, { authority: { server: "/checkin-api" } });browser-local keeps the ephemeral HPKE key in page memory — fine for demos
and low-stakes flows. A server-owned authority keeps it on your backend, which
also gives you a natural audit point; implement the two-call contract
(prepareCredentialRequest / completeCredentialRequest) or pass your own
VerifierAuthority. See Production.
Next: Writing FHIR · Production checklist