Embedded Wallet
Embedded Wallet: Verifiable Credentials
Use proxy.credentials in your Host to issue, store, present, and verify SD-JWT credentials with OID4VCI, OID4VP, selective disclosure, and passkey consent.
Overview
The 1Shot Wallet gives your app a standards-based credential wallet alongside its Ethereum provider. Hosts use proxy.credentials to send an OpenID for Verifiable Credential Issuance (OID4VCI) offer, request an OpenID for Verifiable Presentations (OID4VP) presentation, or manage the user's stored credential summaries.
Credentials stay inside the wallet's encrypted vault. The Branding Layer resolves protocol requests and shows consent; the isolated Signing Layer creates passkey-bound proofs. Your Host never receives the holder's private key. During presentation, it receives only the claims requested by the verifier and approved by the user.
The hosted wallet currently supports SD-JWT VC end to end. Selective disclosure limits what leaves the vault, while the key-binding JWT (kb+jwt) binds each presentation to the verifier's nonce and audience.
Use cases
Credentials are worth reaching for when a user should prove something once and reuse that proof many times, with the verifier learning only what it actually needs. The wallet holds the credential, so the proof travels with the user instead of living in each app's database.
Each pattern below uses the same three steps — acceptOffer() to issue, present() to disclose, and verification on your side. Only the credential type, requested claims, and verifier policy change.
- Portable KYC — Run identity verification once at onboarding and issue the result as a credential. The user can reuse it across your other products, or with partners who trust the same issuer, instead of repeating document checks and re-storing personal data.
- Age and jurisdiction gating — Disclose
ageOver18andcountrywithout revealing a birthdate or address. This is the selective-disclosure case the playground demo verifier already enforces, and it keeps regulated content and market restrictions enforceable without collecting identity documents. - Eligibility and accreditation — Accredited investor status, a professional license, or a membership tier can be issued as a claim the user presents on demand. The verifier gets the status, not the financial statements or filings behind it.
- Agent passport — Issue a credential to an autonomous agent's wallet describing who it acts for and what it is authorized to do. Pair it with an EIP-7715 delegation: the credential answers who the agent is, and the delegation bounds what it can spend on-chain.
Create the wallet connection
Install
npm install @1shotapi/ows-provider @1shotapi/ows-typesCreate one OWSProxy for your wallet container. Credential calls use the same proxy as EIP-1193 requests, but they live under the dedicated credentials namespace.
Host setup
import { OWSProxy } from "@1shotapi/ows-provider";
const container = document.getElementById("wallet-container")!;
const proxy = await OWSProxy.create(
container,
"https://wallet.1shotapi.com/",
);
// Use proxy.credentials.acceptOffer(), present(), list(), and delete().Do not call proxy.showWallet() before acceptOffer() or present(). The wallet opens itself when consent is required and releases the display when the request finishes.
Passkeys require HTTPS (or localhost) throughout the ancestor frame chain. For local issuer or verifier endpoints fetched by the hosted wallet iframe, pass { allowLocalAccess: true } to OWSProxy.create() and expect the browser's local-network permission prompt.
Issue a credential with OID4VCI
Your issuer creates a short-lived credential offer and gives the Host an offer URI. Brand the URI at the external-input boundary, then pass it to acceptOffer(). The wallet resolves the offer, checks issuer trust, shows the offered credential to the user, performs passkey-bound proof of possession, requests the credential, and stores it.
Accept an issuer offer
import { CredentialOfferUri } from "@1shotapi/ows-types";
const offerUri = CredentialOfferUri(
"https://issuer.example.com/offers/4f12a8",
);
const receipt = await proxy.credentials.acceptOffer({
credentialOfferUri: offerUri,
});
console.info(receipt.credentialId);
console.info(receipt.format); // "sd-jwt-vc"
console.info(receipt.type);User rejection is an expected outcome. Do not treat it as a failed identity check or automatically reopen the consent prompt.
acceptOffer()also accepts an inlineoffer, which is useful for controlled test fixtures. Use the URI form for normal OID4VCI deployments.- The current HTTP profile supports the OID4VCI pre-authorized code grant and JWT proof of possession. Authorization Code + PKCE is not yet supported.
- A successful
CredentialReceiptcontains the wallet-localcredentialId, credentialformat, and credentialtypevalues. It does not expose the credential payload.
Request selective disclosure with OID4VP
Your verifier should generate a fresh authorization request containing its requested credential type and claims, a one-time nonce, its audience (client_id), and a response mode. Give the resulting request_uri to the Host and retain the expected nonce and audience on the verifier.
Use acceptedIssuers as a Host-side policy boundary. The wallet intersects this allow-list with credentials matching the verifier request, rechecks issuer trust and credential status, then asks the user to approve the exact claims before creating the presentation.
Request a presentation
import {
CredentialIssuer,
PresentationRequestUri,
} from "@1shotapi/ows-types";
const trustedIssuer = CredentialIssuer("https://issuer.example.com");
const requestUri = PresentationRequestUri(
"https://verifier.example.com/requests/7cb109",
);
const result = await proxy.credentials.present({
requestUri,
acceptedIssuers: [trustedIssuer],
});
console.info(result.format); // "sd-jwt-vc"
console.info(result.disclosedClaims);
if (result.submittedToResponseUri) {
console.info("The wallet submitted the response to the verifier.");
}direct_postand encrypteddirect_post.jwtresponses can be submitted by the wallet to the request'sresponse_uri; inspectsubmittedToResponseUriandresponseMode.- The current DCQL profile supports one credential and flat claim paths. Multi-credential presentations are not yet supported.
- If more than one stored credential matches, the current wallet uses the first match. Make requests specific and enforce issuer policy.
Verify the presentation
Treat a returned presentation as untrusted until verification completes. Resolve the issuer's public key through a trusted issuer configuration or JWKS, use the nonce and audience retained when your verifier created the request, and extract the holder key declared in the issuer-signed credential.
verifySdJwtVcPresentation() verifies the issuer's SD-JWT signature, the holder's kb+jwt signature, and the nonce and audience binding. After cryptographic verification, enforce your application policy separately: allowed issuer, expected credential type, required disclosed claims, freshness, and any domain-specific values.
Cryptographic and policy checks
import { verifySdJwtVcPresentation } from "@1shotapi/ows-provider";
import {
CredentialClaimName,
PresentationUtils,
} from "@1shotapi/ows-types";
const issuerClaims = PresentationUtils.decodeIssuerClaims(
result.presentation,
);
const holderPublicKeyJwk =
PresentationUtils.extractHolderJwk(result.presentation) ??
issuerClaims.cnf?.jwk;
if (!holderPublicKeyJwk) {
throw new Error("Presentation is missing the holder cnf.jwk");
}
const verification = await verifySdJwtVcPresentation({
presentation: result.presentation,
issuerPublicKeyJwk, // Resolve from a trusted issuer JWKS.
holderPublicKeyJwk,
nonce: expectedNonce, // Retained by your verifier.
audience: expectedAudience, // The request's client_id.
});
if (!verification.valid || !verification.payload) {
throw new Error(verification.reasons.join("; "));
}
if (verification.payload.iss !== "https://issuer.example.com") {
throw new Error("Issuer is not allowed");
}
const requiredClaims = [
CredentialClaimName("ageOver18"),
CredentialClaimName("country"),
];
const missing = requiredClaims.filter(
(claim) => !(claim in verification.payload!),
);
if (missing.length > 0) {
throw new Error(`Missing claims: ${missing.join(", ")}`);
}Never take the expected nonce, audience, issuer allow-list, or issuer verification key from the presentation itself. Those values are verifier policy and must come from trusted server-side state.
Extracting cnf.jwk before verification is safe only as input to the combined verification step. Do not trust that key or any decoded claim unless the issuer signature also verifies.
List and delete stored credentials
list() returns metadata summaries, not claim values, so a Host can show credential inventory without silently reading identity data. Filter by credential type or issuer when needed. delete() removes a credential by its wallet-local ID.
Credential inventory
import {
CredentialIssuer,
CredentialTypeName,
} from "@1shotapi/ows-types";
const credentials = await proxy.credentials.list({
type: CredentialTypeName("KycCredential"),
issuer: CredentialIssuer("https://issuer.example.com"),
});
for (const credential of credentials) {
console.info(
credential.credentialId,
credential.type,
credential.issuedAt,
credential.validUntil,
);
}
const credential = credentials[0];
if (credential) {
// Ask for confirmation in your Host UI before deleting.
await proxy.credentials.delete({
credentialId: credential.credentialId,
});
}The wallet's Credentials tab already lets users inspect their stored credentials. features.disableCredentials hides that tab for a focused embed, but Host credential RPC methods remain available.
Consent, trust, and failure handling
Issuance and presentation always require wallet consent. The offer modal names the issuer and offered credential; the presentation modal names the verifier, selected credential, and requested claims. The passkey ceremony then runs inside the isolated wallet layers.
Credential status, issuer trust, and cryptographic validity are distinct checks. Production verifiers should fail closed if any required check cannot be completed.
You can override consent copy with setStyle keys under copy.credentialOffer and copy.credentialPresentation; do not remove or obscure the user's disclosure decision.
- User rejected (
4001) — the user declined issuance or disclosure. Return to a neutral application state and let the user choose whether to retry. - Invalid params (
-32602) — an input failed wire validation, commonly because neither URI nor inline request was supplied. - Method not found (
-32601) — the Branding Layer did not register credential support. This should not occur with the hosted 1Shot Wallet. - Timeout (
-32603) — the ceremony exceeded the RPC timeout. Avoid aggressive retries because a user may still be interacting with passkey UI. - Trust, status, or matching failure — the issuer is not trusted, the credential is revoked or suspended, or no stored credential satisfies the request and
acceptedIssuerspolicy.
Current protocol scope
- Credential format — SD-JWT VC with selective disclosure and holder key binding.
- OID4VCI — credential offer URI or inline offer, pre-authorized code grant, and JWT proof of possession.
- OID4VP — request URI or inline test definition; DCQL subset or legacy presentation definition;
direct_post, encrypteddirect_post.jwt, and fragment response modes. - Credential selection — one credential per presentation with flat claim paths.
- Not yet supported — OID4VCI Authorization Code + PKCE, multi-credential presentations, and end-to-end HTTP issuance or presentation for JWT VC, JSON-LD, or mdoc.
Test before production
Use the wallet playground's Credentials mode to run both halves of the flow against the hosted wallet: generate and accept an issuer offer, request a verifier presentation, inspect selective disclosure, and review the cryptographic and policy custody checks.
Credential outcomes also appear on proxy.analytics as CredentialIssued, CredentialIssueFailed, CredentialIssueCancelled, CredentialPresented, CredentialPresentFailed, and CredentialPresentCancelled.