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:keyordid:web),generates and stores the wallet's signing key,
maps the wallet to the calling user, and
returns a
holderIdyou 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):
keyorweb. 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 asexample.com:8080). Required whenmethodisweb; ignored forkey.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:webDID is built asdid:web:<domain>:<holderId>; adid:keyDID looks likedid: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
methodisweb.
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
nullif 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
subclaim) and the newholderId.GET /agent/holdersreturns 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 theholderIdin the path is not owned by the caller, the request is rejected with403 Forbiddenand the standard error body{ statusCode, message, path, timestamp }— even though the token is otherwise valid and has the admin role.The
holderIdaddresses 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 examplePOST /holders/:holderId/processandGET /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:
Create the holder and capture its
holderId:
Process a credential-offer URI to claim a credential into the wallet (here, an
[email protected]:sd-jwtoffer):
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
/agentholder routes cover creation (POST /agent/holder) and listing (GET /agent/holders); holder deletion is not currently part of the public API.
Last updated