Universal Registrar (DIF)
The Universal Registrar is the EDK's implementation of the DIF Universal Registrar HTTP API. It is the standardized way to register, update, and deactivate DIDs over REST, and the surface most cross-vendor integrations expect to find when they speak DID lifecycle to a registrar component.
The whole point of the spec is uniform interop. Every endpoint takes a method-agnostic envelope and returns a state object that callers can interpret without knowing which DID method ran underneath. That uniformity is also the spec's most important limitation: it deals in entire DID Documents, not in the individual verification methods or services inside them, and it does not concern itself with listing, filtering, or any kind of inventory operation. If those are the things you need, use the rich DID manager REST API instead; if you need spec-shaped, vendor-portable lifecycle operations, this is the right surface.
What this API is and is not
The Universal Registrar is intentionally narrow. It does five things:
- Create a new DID for a chosen method.
- Apply a document-level update to an existing DID.
- Deactivate an existing DID.
- Tell callers which DID methods this registrar supports.
- Tell callers what those methods can do (create only, update, deactivate, key generation).
Notably absent: there is no GET /1.0/identifiers in the registrar (that lives on the Universal Resolver), there is no list endpoint, there is no concept of an admin-side query, and there is no first-class hook for editing individual verification methods or services. Those gaps exist because the DIF spec deliberately leaves all of them out, so any tool that drives the Universal Registrar can do so without coordinating on extensions.
Mounting
The registrar is provided by the lib-did-rest-registrar-server module and ships as an HttpAdapter (UniversalRegistrarHttpAdapter) bound under base path /1.0. The adapter wires five injected endpoint commands that each delegate to the IDK DID manager. The Spring Boot integration registers the adapter automatically when the module is on the classpath.
dependencies {
implementation("com.sphereon.edk:lib-did-rest-registrar-server:0.25.0")
implementation("com.sphereon.edk:idk-spring-support:0.25.0")
// The DID methods you want to register through this registrar.
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")
// Persistence for managed DIDs. Pick one per tenant; PostgreSQL is the
// EDK enterprise choice.
implementation("com.sphereon.edk:lib-did-persistence-postgresql:0.25.0")
}
sphereon:
did:
registrar:
enabled: true
base-path: /did/registrar
The base-path setting prefixes the adapter so a POST /1.0/create from a client becomes, on the wire, POST /did/registrar/1.0/create. Leave it blank to mount at the bare DIF paths.
The five endpoints
All paths are relative to the adapter mount (/1.0 plus any configured base path).
| Method | Path | Endpoint command | Purpose |
|---|---|---|---|
POST | /1.0/create | CreateDidEndpointCommand | Create a new DID |
POST | /1.0/update | UpdateDidEndpointCommand | Apply a document-level update |
POST | /1.0/deactivate | DeactivateDidEndpointCommand | Deactivate a DID |
GET | /1.0/methods | GetRegistrarMethodsEndpointCommand | List supported methods |
GET | /1.0/properties | GetRegistrarPropertiesEndpointCommand | Capability matrix per method |
Each command is a thin adapter over the matching DID manager service command, so behaviour stays identical to programmatic IDK usage.
Create a DID
POST /1.0/create takes the spec request envelope (method, options, secret, didDocument) and returns the spec response envelope. The fields the DID manager looks at depend on the method.
POST /1.0/create HTTP/1.1
Content-Type: application/json
{
"method": "web",
"options": {
"domain": "example.com",
"path": ["users", "alice"]
},
"secret": {
"verificationMethod": [{
"type": "JsonWebKey2020",
"purpose": ["authentication", "assertionMethod"]
}]
}
}
The response carries a didState whose state is finished when the registrar produced a usable DID synchronously, action when the caller needs to publish something (the canonical example is the registrar handing you back a did.json you have to host yourself for did:web), or failed with a reason for spec-conformant failure reporting.
{
"jobId": null,
"didState": {
"state": "finished",
"did": "did:web:example.com:users:alice",
"didDocument": {
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:web:example.com:users:alice",
"verificationMethod": [{
"id": "did:web:example.com:users:alice#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:example.com:users:alice",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}],
"authentication": ["did:web:example.com:users:alice#key-1"],
"assertionMethod": ["did:web:example.com:users:alice#key-1"]
}
},
"didDocumentMetadata": {},
"didRegistrationMetadata": {}
}
A few practical notes that the spec leaves implicit:
- The
secret.verificationMethod[].purposearray drives which top-level relationship arrays (authentication,assertionMethod, ...) the new key ends up in. Emptypurposemeans "key is present in the document but bound to no relationship", which is almost never what you want. - For methods where the registrar generates a key (the default when
secret.verificationMethod[].privateKeyJwkis absent), the new key material lands in the KMS configured for the tenant. The endpoint returns only public material; private keys never leave the KMS. optionsis method-specific. Fordid:webit isdomainandpath. Fordid:webvhit includes the witness configuration. Fordid:keyanddid:jwkthe body ofoptionsis usually empty because the DID is fully derived from the key.
Update a DID
POST /1.0/update takes the existing DID, an optional secret, an array of didDocumentOperation verbs, and an array of didDocument patches to apply. The verbs and patches are positional: the i-th verb describes what to do with the i-th patch.
POST /1.0/update HTTP/1.1
Content-Type: application/json
{
"did": "did:web:example.com:users:alice",
"options": {},
"didDocumentOperation": ["addToDidDocument"],
"didDocument": [{
"service": [{
"id": "#hub",
"type": "LinkedDomains",
"serviceEndpoint": "https://hub.example.com"
}]
}]
}
The supported operations follow the DIF spec set: setDidDocument to replace, addToDidDocument to merge in, removeFromDidDocument to subtract. The registrar applies them in order, validates the resulting document against the method provider, persists, and republishes if the method requires it (writing a new did.json for did:web, appending a log entry for did:webvh).
Because update is document-level, a single call can change multiple parts of the document at once. That is convenient for batch reconfiguration; it is also the reason the rich API offers sub-resource endpoints for the more delicate cases (rotating a single verification method without disturbing anything else, for example).
Deactivate a DID
POST /1.0/deactivate marks the DID as deactivated through the method-specific mechanism. For did:web that means publishing a new did.json with "deactivated": true. For did:webvh it appends a deactivation entry to the log. For did:key and did:jwk deactivation is not meaningful because the DID is self-derived and immutable; the registrar reports that through /1.0/properties.
POST /1.0/deactivate HTTP/1.1
Content-Type: application/json
{
"did": "did:web:example.com:users:alice",
"options": {
"reason": "Key rotation"
}
}
Deactivation through this endpoint also flips the deactivated flag on the locally tracked DID, so resolution returns the deactivated document and downstream consumers (issuer, verifier, trust evaluator) stop treating it as live.
Discover what the registrar supports
GET /1.0/methods returns the list of DID methods this instance can create, update, or deactivate. The values depend on which lib-did-methods-* modules are on the classpath.
GET /1.0/methods HTTP/1.1
{
"methods": ["key", "jwk", "web", "webvh"]
}
GET /1.0/properties is the spec's capability advertisement. It returns a per-method matrix describing whether create, update, and deactivate are supported. That matters because not every DID method is updatable: did:key and did:jwk are immutable by design, while did:web and did:webvh support the full lifecycle.
GET /1.0/properties HTTP/1.1
{
"methods": ["key", "jwk", "web", "webvh"],
"methodCapabilities": {
"key": { "create": true, "update": false, "deactivate": false },
"jwk": { "create": true, "update": false, "deactivate": false },
"web": { "create": true, "update": true, "deactivate": true },
"webvh": { "create": true, "update": true, "deactivate": true }
}
}
Callers should consult /1.0/properties before attempting an update or deactivate, especially when they want to be polite to immutable methods rather than catching the spec-conformant failure response.
Async jobs and jobId
The DIF spec models long-running operations through jobId: the registrar can return jobId: "<some-id>" with a non-finished didState, and the client polls back to the same endpoint with the same jobId to advance the state. The EDK's current implementation performs all method operations synchronously and returns jobId: null on completion. Clients should treat a non-null jobId as the asynchronous path defined by the spec, which is forward-compatible if a later method provider (for example a registrar that talks to an external on-chain method) introduces it.
Authentication and authorization
The DIF spec is silent on authentication, so you secure the registrar like any other write surface in the tenant.
sphereon:
did:
registrar:
require-authentication: true
security:
jwt:
issuer: https://auth.example.com
audience: did-registrar
For finer-grained control (which subjects may create which methods, which may deactivate, and so on), reach for the authorization stack and put a policy in front of the registrar's command IDs. See Authorization Overview for how to attach Cedar or OPA policies to specific commands.
When the Universal Registrar is the wrong choice
The Universal Registrar is the right choice when you need DIF interop or when document-level operations match what you actually want to do. It is a poor fit when:
- you need to enumerate DIDs ("list all DIDs of method
webcreated in the last seven days"), since there is no list endpoint; - you need to rotate a single verification method without rewriting the entire document, since there is no per-VM endpoint;
- you need to inspect the KMS key mappings for a DID, which are not part of the DIF model;
- you need to attach internal-only metadata such as aliases or canonical identifiers, which are intentionally outside the spec.
For those, the rich DID manager REST API is the right tool. The two coexist over the same DID state, so picking the one that fits a particular consumer does not preclude using the other for a different one.
Related material
- Decentralized Identifiers overview
- Rich DID manager REST API
- IDK DID Overview for the DID manager DSL, providers, and method-specific options.