Skip to main content
Version: v0.25.0 (Latest)

Rich DID manager REST API

The rich DID manager REST API is the Sphereon-defined HTTP surface for everything the Universal Registrar does not cover. It is the API you want behind a tenant admin UI, in front of automation that needs to enumerate or selectively mutate DIDs, and anywhere a single document-level PUT is too coarse a tool for the change you want to make.

The API ships in the services-did-manager-rest module of the IDK and is mounted at base path /api/dids/v1. One composite adapter (DidManagerHttpAdapter) routes requests to ten sub-adapters that each own a slice of the DID model, all delegating to the same underlying DID manager service commands the IDK uses internally. That means there is no second source of truth: a DID created through this API and one created through the Universal Registrar live in the same store, surface in the same list, and resolve the same way.

What this API gives you that the Universal Registrar does not

The headline difference is granularity. The Universal Registrar speaks in entire DID Documents; the rich API speaks in DID Documents and in each editable part of one. The headline difference behind that is inventory and queryability: the Universal Registrar has no list endpoint, no filter, no projection mechanism, no way to enumerate KMS key mappings; the rich API has all of those as first-class concerns.

The concrete differences:

  • Listing DIDs with filtering, pagination, sort, and optional field expansion.
  • CRUD on individual verification methods, services, controllers, verification relationships, also-known-as aliases, and equivalent identifiers.
  • Explicit inspection and management of the KMS key mappings that back each verification method.
  • Direct access to the resolved document cache: read the cached view, force re-resolution, or invalidate.
  • Per-method capability advertisement, including a compact summary view for UIs.
  • Importing externally-managed DIDs so the tenant can track them alongside DIDs it controls.

Surface map

Rich DID Manager REST API surface map showing lifecycle, document parts, key and cache, and capabilities

The composite adapter dispatches by path prefix to ten sub-adapters. They are described per group below; all paths are relative to /api/dids/v1.

DID lifecycle

The lifecycle adapter holds the verbs that create, find, alter, and remove a DID. Note that PATCH is partial (JSON Merge Patch over the declarative fields) and PUT is a full replace of the declarative sub-collections (controllers, alsoKnownAs, equivalentIds, services, ...), explicitly excluding verification methods and key mappings. Verification methods and key mappings are mutated through their dedicated sub-resources precisely because a wholesale replace would risk leaving orphan KMS keys or dropping live keys without the corresponding KMS lifecycle event.

MethodPathPurpose
POST/didsCreate a new DID
GET/didsList DIDs (filter, page, sort, expand)
POST/dids/externalImport an externally-managed DID
GET/dids/{did}Get a DID, with optional projections
PATCH/dids/{did}Partial update of declarative fields
PUT/dids/{did}Replace declarative collections
DELETE/dids/{did}Soft-delete the DID locally
POST/dids/{did}/actions/deactivateDeactivate the DID on its method's network
GET/dids/{did}/resolveResolve through the resolver registry

DELETE is intentionally a local soft delete: it sets the row's deletedAt timestamp so the DID disappears from the default list view but remains queryable through includeDeleted=true. It does not deactivate the DID on its network, and it does not destroy KMS keys. Deactivation on the network is the explicit actions/deactivate call. The two are distinct on purpose, because "stop tracking this here" and "publicly retire this identifier" are different decisions that usually warrant different reviews.

Create

POST /api/dids/v1/dids HTTP/1.1
Content-Type: application/json

{
"method": "key",
"keyInfo": {
"providerId": "default-kms",
"alias": "alice-signing-key",
"keyType": { "kty": "EC", "crv": "P-256" }
},
"didAlias": "alice",
"controllers": ["did:web:example.com:control"],
"alsoKnownAs": ["https://alice.example.com"]
}

The request body uses keyInfo to point at the KMS provider and key alias that should back the DID. The DID manager looks the key up, derives the DID per the method's rules, persists the resulting ManagedDid, and returns the wire-shape Did. didAlias is a human-readable handle stored alongside the DID for later lookup; controllers, alsoKnownAs, and the rest of the declarative fields can be set at creation time or added later through PATCH.

Response:

{
"did": "did:key:z6MkhaXgBZDvotDkL5257faWxcsSqt4mq6D8GwSkLrHee",
"method": "key",
"alias": "alice",
"role": "MANAGED",
"alsoKnownAs": ["https://alice.example.com"],
"equivalentIds": [],
"canonicalId": null,
"createdAt": "2026-05-18T20:14:11Z",
"updatedAt": "2026-05-18T20:14:11Z"
}

List

The list endpoint exposes the full DidFilter shape as query parameters. Defaults are tuned for "fast useful answer": newest first, 100 per page, lightweight projection (no inline document, no inline keys), and excluding both deactivated and soft-deleted entries.

GET /api/dids/v1/dids?method=web&role=MANAGED&page=0&size=25&sort=updatedAt&sortDirection=DESC&expand=keys HTTP/1.1

The supported parameters:

ParameterTypeDefaultMeaning
methodstringnoneRestrict to a single DID method
aliasstringnoneExact-match alias filter
roleMANAGED | EXTERNALnoneLocally controlled vs imported-for-tracking
searchstringnoneCase-insensitive substring match over DID and alias
includeDeactivatedbooleanfalseInclude DIDs whose method-level deactivation flag is set
includeDeletedbooleanfalseInclude locally soft-deleted DIDs
pageint0Zero-based page index
sizeint100Page size; omit to return everything matching
sortenumCREATED_ATOne of CREATED_AT, UPDATED_AT, DID, METHOD, ALIAS
sortDirectionenumDESCASC or DESC
expandstringnoneComma-separated; document, keys, or all

The response is { items, page } where items are wire-shape Did records and page carries { page, size, totalElements, totalPages }.

{
"items": [
{
"did": "did:web:example.com:users:alice",
"method": "web",
"alias": "alice",
"role": "MANAGED",
"alsoKnownAs": [],
"equivalentIds": [],
"canonicalId": null,
"createdAt": "2026-05-18T19:55:02Z",
"updatedAt": "2026-05-18T20:02:41Z",
"keys": [
{ "verificationMethodId": "#key-1", "providerId": "default-kms", "alias": "alice-signing-key" }
]
}
],
"page": { "page": 0, "size": 25, "totalElements": 1, "totalPages": 1 }
}

expand is what keeps the list fast by default. The lightweight projection skips two expensive fields: the resolved DID Document and the KMS key mappings. Both fields are populated only when the caller asks for them, so a typical list call does not trigger a resolver round-trip per row. Set expand=document for inline documents, expand=keys for inline key mappings, or expand=all for both.

Tracking external DIDs

POST /dids/external imports a DID that some other party controls so the tenant can list, query, and use it like a first-class identifier without owning its private keys. This is the typical pattern for known counterparties (an Issuer's DID you accept credentials from, or a Verifier you sometimes interact with): you store the alias and reference, the system resolves the document on demand through the resolver, and the rest of the tenant treats it as a normal Did record with role: EXTERNAL.

External DIDs cannot be updated or deactivated through this API. Their lifecycle endpoints exist but reject method-mutating operations, because the tenant does not hold the keys to authorise them.

Get, with projections

GET /api/dids/v1/dids/did:web:example.com:users:alice?expand=document,keys HTTP/1.1

The same expand model applies: a bare GET returns the summary view, while explicit projections inline the resolved document and the KMS key mappings. This is the right way to render a DID detail screen without writing two requests.

Update (PATCH) and Replace (PUT)

PATCH /dids/{did} uses JSON Merge Patch semantics over the declarative fields (alias, controllers, alsoKnownAs, equivalentIds, canonicalId, ...). It is the right verb for "change one field on this DID record".

PUT /dids/{did} replaces the entire declarative side of the record at once: every listed collection is rewritten with what you send, with omitted fields being treated as empty rather than left untouched. Verification methods and key mappings are deliberately excluded from the replace body; mutating them goes through the dedicated sub-resources below.

Deactivate (on the network)

POST /api/dids/v1/dids/did:web:example.com:users:alice/actions/deactivate HTTP/1.1
Content-Type: application/json

{ "options": { "reason": "Key compromised" } }

The endpoint runs the method-specific deactivation: for did:web it republishes the did.json with "deactivated": true; for did:webvh it appends a deactivation entry to the log; for did:key and did:jwk it returns the spec-conformant capability error because those methods are immutable. The local row's deactivated flag is set in the same transaction, so resolution and downstream consumers immediately see the deactivation.

Resolve (locally tracked)

GET /dids/{did}/resolve runs the same resolver registry the Universal Resolver exposes, but scoped to a DID that is already tracked locally. The response carries the resolution result, the resolved document, and the resolver metadata. It is convenient when a UI wants to show "what does the world see for this DID right now" alongside the locally-stored aliases and key mappings.

Document sub-resources

Each editable part of the DID Document has its own sub-resource. The verbs are deliberately uniform across them: GET to list, POST to add, GET /{id} to fetch one, PATCH /{id} where partial update makes sense, DELETE /{id} to remove.

Verification methods

MethodPath
GET/dids/{did}/verification-methods
POST/dids/{did}/verification-methods
GET/dids/{did}/verification-methods/{methodId}
PATCH/dids/{did}/verification-methods/{methodId}
DELETE/dids/{did}/verification-methods/{methodId}

Verification methods are the most consequential thing in a DID Document: every signature you produce as that DID is bound to one of them. Adding a verification method through this endpoint also creates the matching KMS key mapping in the same transaction, so there is no window where a verification method exists without its key reference. Removing a verification method removes the mapping; whether it removes the underlying KMS key depends on the key's lifecycle policy (a key that backs other verification methods or other DIDs is not deleted just because one reference is removed).

This is the principal reason to prefer per-VM endpoints over the document-level PUT: PUT cannot express "rotate verification method #key-1 to a new KMS key while keeping #key-2 untouched" without rewriting the whole document and risking accidental drift.

Services

MethodPath
GET/dids/{did}/services
POST/dids/{did}/services
GET/dids/{did}/services/{serviceId}
PATCH/dids/{did}/services/{serviceId}
DELETE/dids/{did}/services/{serviceId}

Services are the document's pointers at endpoints (an issuer endpoint, a credential repository, a hub URL). Per-service endpoints are useful because services tend to change independently of keys, and because a tenant admin UI usually wants to render and edit them in isolation.

Controllers

MethodPath
GET/dids/{did}/controllers
POST/dids/{did}/controllers
DELETE/dids/{did}/controllers/{controllerId}

The controller array on a DID Document declares which other DIDs are authorised to act on its behalf. Multi-controller setups (delegation between an organisational DID and a key-rotation DID, for instance) are common in enterprise deployments, and managing the list element by element keeps changes auditable.

Also-known-as and equivalent IDs

MethodPath
GET/dids/{did}/also-known-as
POST/dids/{did}/also-known-as
DELETE/dids/{did}/also-known-as/{akaId}
GET/dids/{did}/equivalent-ids
POST/dids/{did}/equivalent-ids
DELETE/dids/{did}/equivalent-ids/{equivalentId}

alsoKnownAs are non-DID identifiers (URLs, account references) that name the same subject; equivalentIds are other DIDs that the controller asserts to be the same identity. They serve different verifiability stories (alsoKnownAs is a hint, equivalentIds is a controller-signed assertion) and each gets its own sub-resource so the audit trail stays clean.

Verification relationships

MethodPath
GET/dids/{did}/verification-relationships
POST/dids/{did}/verification-relationships
DELETE/dids/{did}/verification-relationships/{relationshipId}

Relationships (authentication, assertionMethod, keyAgreement, capabilityInvocation, capabilityDelegation) define which verification methods can do what. Editing them as a separate sub-resource lets you assign or revoke a purpose for an existing key without touching the key itself.

Key mappings and document cache

Key mappings

MethodPath
GET/dids/{did}/key-mappings
POST/dids/{did}/key-mappings
DELETE/dids/{did}/key-mappings/{mappingId}

A key mapping ties a verification method (in the DID Document) to a KMS provider and key alias (where the private material lives). The mapping endpoints expose that link directly: useful when you need to migrate a DID from one KMS provider to another, when you are auditing which keys back which DIDs, or when you are reconciling a freshly imported tenant's DIDs with a freshly imported KMS.

Adding a key mapping does not create a verification method on its own; the typical flow is "add verification method through the verification-method endpoint, which atomically creates the mapping". The bare key-mapping endpoints exist mostly for repair, migration, and inspection.

Document cache

MethodPath
GET/dids/{did}/document
POST/dids/{did}/document/refresh
DELETE/dids/{did}/document/cache

The DID manager keeps a cached view of the resolved document so reads do not trigger a resolver round-trip every time. The cache endpoints expose that:

  • GET /document returns whatever is in the cache (or a stale-with-warning response if the cache has expired).
  • POST /document/refresh forces a fresh resolve and replaces the cached entry. This is the right operation to run after a did:web publish, to make the new document visible immediately without waiting for the natural TTL to expire.
  • DELETE /document/cache invalidates the cache entry without forcing an immediate re-resolve; the next read will trigger one.

Capabilities

The capability endpoints answer "what can the DID manager do with method X" in machine-readable form, which is what makes building a method-aware UI tractable.

MethodPathReturns
GET/methodsThe full list of methods this tenant supports
GET/methods/{method}/capabilitiesThe detailed capability descriptor for one method
GET/methods/{method}/capabilities/summaryA compact yes/no summary suitable for UI rendering

The full descriptor describes everything from supported key types and curves to whether keys can be rotated, whether services can be added after creation, and whether the method has a deactivation operation. The summary version trims it to the booleans most UIs actually render.

Wiring it into a tenant

dependencies {
implementation("com.sphereon.idk:services-did-manager-rest:0.25.0")
implementation("com.sphereon.edk:idk-spring-support:0.25.0")

// Method providers; each provider is independently optional.
implementation("com.sphereon.idk:lib-did-methods-key:0.25.0")
implementation("com.sphereon.idk:lib-did-methods-jwk:0.25.0")
implementation("com.sphereon.idk:lib-did-methods-web:0.25.0")
implementation("com.sphereon.idk:lib-did-methods-webvh-provider:0.25.0")
implementation("com.sphereon.idk:lib-did-methods-webvh-resolver:0.25.0")

// Persistence. Pick one per tenant.
implementation("com.sphereon.edk:lib-did-persistence-postgresql:0.25.0")
}
sphereon:
did:
manager:
enabled: true
base-path: /admin/did

The optional base-path setting prefixes the adapter so /api/dids/v1/dids lands on /admin/did/api/dids/v1/dids on the wire. The adapter is contributed into the SessionScope DI graph, so it runs inside the per-session command and audit envelope that the rest of the EDK uses.

Authentication and authorization

The rich API is the surface that exposes the most operator-significant verbs (deactivation, key mapping inspection, controller mutation), so it warrants careful authorization. Three layers usually combine:

  1. Transport-level authentication. Place the adapter behind your tenant's authenticated ingress; the EDK ships with JWT and OAuth2 integrations that work out of the box.
  2. Command-level authorization. Every endpoint is an HttpEndpointCommand with a stable command ID, so Authorization policies can permit or deny by command. This is the right granularity for "operator role can list and read but only platform-admin role can deactivate".
  3. Audit. The same command envelope that enforces authorization emits audit events through the Audit pipeline, so every mutation is traceable to a subject, a request, and a tenant.

When to reach for the Universal Registrar instead

If a particular consumer is a DIF-aware tool that already knows how to drive /1.0/create, /1.0/update, /1.0/deactivate, point it at the Universal Registrar and keep the rich API for everything else. Both APIs share the same state, so a DID created through one is visible and editable through the other immediately.

The rule of thumb: external interop and document-level operations through the Universal Registrar; everything internal, granular, queryable, or operator-facing through the rich API.