For the complete documentation index, see llms.txt. This page is also available as Markdown.

Holders and Tenancy

The Wallet Server runs as a single backend, but each user gets their own isolated cloud wallet. That wallet is called a holder: a per-user tenant that owns a DID and stores the Verifiable Credentials (VCs) belonging to that user. A holder is created once, identified by a holderId, and from then on every wallet operation is addressed under /holders/:holderId/*.

This page covers how holders are created, listed, and isolated from one another. For acquiring and inspecting the credentials inside a holder, see the Credential Management page.

All endpoints below require a valid OIDC JWT Bearer token (Authorization: Bearer <token>); the /agent/* management routes and the /holders/:holderId/* wallet routes additionally require the admin role. In development you can bypass auth with AUTH_DISABLED=true, which treats every request as an authenticated admin.

The holder model

A holder is a self-contained wallet tenant. When you create one, the service:

  • provisions a DID for the wallet (either did:key or did:web),

  • generates and stores the wallet's signing key,

  • maps the wallet to the calling user, and

  • returns a holderId you use to address the wallet from then on.

The holder's DID is the identity its credentials are bound to and the identity it presents under. Only did:web and did:key are created. If you choose did:web, the service hosts the DID document itself and serves it over HTTPS; see the DID Document Management page for the served-document details.

Create a holder

POST /agent/holder

Creates a per-user cloud-wallet tenant and binds it to the authenticated user. The user is identified by the sub claim of the Bearer token, so no user identifier is sent in the body.

Request Body

  • method (string, required): key or web. Selects the DID method for the holder's wallet.

  • domain (string): the hostname for the DID, without a scheme (e.g. example.com, optionally with a port such as example.com:8080). Required when method is web; ignored for key.

  • label (string, optional): a human-readable label for the wallet, useful when a user owns several holders.

Response Body

  • holderId: the internal holder identifier (UUID). This is the tenant id used in every /holders/:holderId/* path.

  • did: the holder's DID. A did:web DID is built as did:web:<domain>:<holderId>; a did:key DID looks like did:key:z6Mk....

  • cryptographicBindingMethodsSupported: the DID binding methods the wallet can use when receiving credentials.

  • credentialSigningAlgValuesSupported: the signing algorithms the wallet supports.

  • didDocument: the full DID document JSON. Returned only when method is web.

For a did:key holder, omit domain:

List your holders

GET /agent/holders

Returns the holders owned by the authenticated user, most recent first. The result is scoped to the caller's sub: you only ever see your own holders.

Response Body

An array of holder summaries:

  • holderId: the tenant id used in /holders/:holderId/* calls.

  • did: the holder's DID.

  • label: the label supplied at creation, or null if none was given.

  • createdAt: when the holder was created.

Multi-tenancy and ownership

Although every holder lives in the same backend, holders are isolated per user:

  • Ownership is bound to the token subject. When a holder is created, the service records a mapping between the user (the JWT sub claim) and the new holderId. GET /agent/holders returns only the mappings for the calling user.

  • Wallet routes are ownership-checked. Every request to a /holders/:holderId/* route is verified against this mapping. If the holderId in the path is not owned by the caller, the request is rejected with 403 Forbidden and the standard error body { statusCode, message, path, timestamp } — even though the token is otherwise valid and has the admin role.

  • The holderId addresses the wallet. It is the single handle that ties together the holder's DID, its stored credentials, and its access checks. Use it in every wallet call, for example POST /holders/:holderId/process and GET /holders/:holderId/credentials.

Because ownership keys off the token subject, a token issued to one user can never reach another user's wallet, and a holder created under AUTH_DISABLED=true is owned by the development user that mode injects.

End-to-end example

A typical sequence for standing up a wallet and putting a credential into it:

  1. Create the holder and capture its holderId:

  1. Process a credential-offer URI to claim a credential into the wallet (here, an [email protected]:sd-jwt offer):

  1. List the holder's decoded credentials to confirm it was stored:

For the full claim and presentation walkthroughs, see Processing Credential Offers and Presentation Requests.

Notes

  • A holder's DID method is fixed at creation time; there is no endpoint to change a holder's DID afterward.

  • The /agent holder routes cover creation (POST /agent/holder) and listing (GET /agent/holders); holder deletion is not currently part of the public API.

Last updated