> For the complete documentation index, see [llms.txt](https://docs.empe.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.empe.io/develop/wallet-server/holders-and-tenancy.md).

# 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.

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"web","domain":"example.com","label":"User Wallet 1"}'
```

### Response Body

```json
{
  "holderId": "594b464b-20bd-495a-9fb6-87328c759629",
  "did": "did:web:example.com:594b464b-20bd-495a-9fb6-87328c759629",
  "cryptographicBindingMethodsSupported": ["did:key", "did:jwk", "did:web"],
  "credentialSigningAlgValuesSupported": ["EdDSA", "ES256"],
  "didDocument": { "...": "..." }
}
```

* **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`:

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"key","label":"Mobile wallet"}'
```

## 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.

```
curl -H 'Authorization: Bearer <token>' \
  http://localhost:3000/agent/holders
```

### Response Body

An array of holder summaries:

```json
[
  {
    "holderId": "594b464b-20bd-495a-9fb6-87328c759629",
    "did": "did:web:example.com:594b464b-20bd-495a-9fb6-87328c759629",
    "label": "User Wallet 1",
    "createdAt": "2026-06-22T09:14:03.000Z"
  }
]
```

* **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`:

```
curl -X POST http://localhost:3000/agent/holder \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"method":"web","domain":"example.com","label":"Engineering badges"}'
```

2. Process a credential-offer URI to claim a credential into the wallet (here, an `EmployeeBadge@1.0:sd-jwt` offer):

```
curl -X POST http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/process \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"uri": "openid-credential-offer://?credential_offer_uri=..."}'
```

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

```
curl -H 'Authorization: Bearer <token>' \
  http://localhost:3000/holders/594b464b-20bd-495a-9fb6-87328c759629/credentials/decoded
```

For the full claim and presentation walkthroughs, see [Processing Credential Offers and Presentation Requests](/develop/wallet-server/flows-claim-and-presentation.md).

## 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.empe.io/develop/wallet-server/holders-and-tenancy.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
