Self-host your DID Web for DPP verification
Keyban signs every Digital Product Passport with a P-256 key that belongs to your tenant. Verifiers — buyers, B2B partners, AI agents — need to fetch the matching public key to validate the signature. Hosting that key on your own domain (did:web:brand.example.com) means the trust chain points to you, not to Keyban: even if Keyban ever disappeared, the signatures stay verifiable for as long as you keep the file online.
This guide walks through configuring the domain in Keyban, retrieving the DID Document Keyban generates from your active certification key, publishing it at https://{domain}/.well-known/did.json, and rotating the key when needed.
When to use self-hosted DID Web
Use self-hosted DID Web when either is true:
- Your brand identity matters in the verification UX (consumers expect to see your domain in the proof chain, not Keyban's).
- You want guarantees that VC verification keeps working even if your contract with Keyban ends.
If neither applies, the default did:key mode is fine: the public key is embedded in the DID itself, so there is no hosting to manage.
V1 supports one domain per tenant and manual key rotation. CNAME-based hosting delegation (did:web:tenant.keyban.io pointing to your domain) and automated rotation are deferred to V2.
Prerequisites
- A Keyban tenant with DPP enabled and an active certification key (visible in the Admin Console under Certification keys).
- An API key with the
dpp:updatepermission, or an Admin Console seat with the equivalent role. - A domain you control (
brand.example.com), served over HTTPS with a valid TLS certificate. The verifier must reachhttps://{domain}/.well-known/did.jsonand receive a200 OKwithContent-Type: application/json. - The ability to publish a small static JSON file on that domain (CDN, S3 + CloudFront, GitHub Pages, your own web server — anything that serves
.well-known/correctly).
How the verification works
- VerifierBuyer, partner, AI agent
- brand.example.comTenant-hosted
- VCFrom DPP page
- 1Read proof.verificationMethod (did:web:brand.example.com#key-1)
- 2GET /.well-known/did.json
- 3DID Document with publicKeyJwk
- 4Verify VC signature locally with the JWK
Keyban is not in the verification path. The verifier only contacts your domain.
Step 1 — Configure the domain in Keyban
Tell Keyban which bare host you intend to use. Pass the host only — no scheme, no path, no port.
curl -X PUT 'https://api.keyban.io/v1/dpp/certification-key/did-web-config' \
-H 'X-Api-Key: $KEYBAN_API_KEY' \
-H 'Content-Type: application/json' \
-d '{ "domain": "brand.example.com" }'
The response returns the full DID Document Keyban built from your active certification key:
{
"domain": "brand.example.com",
"didDocument": {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:brand.example.com",
"verificationMethod": [
{
"id": "did:web:brand.example.com#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:brand.example.com",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
}
],
"assertionMethod": ["did:web:brand.example.com#key-1"],
"authentication": ["did:web:brand.example.com#key-1"]
}
}
Only lowercase hostnames with valid TLD characters are accepted (e.g. brand.example.com). Passing https://brand.example.com, brand.example.com/dpp or brand.example.com:8443 returns a 400 Bad Request. To disable self-hosted DID Web later, pass "domain": null.
Step 2 — Publish did.json on your domain
Copy the didDocument object from the response and serve it verbatim at:
https://brand.example.com/.well-known/did.json
The file must be reachable over HTTPS with these conditions:
Content-Type: application/json(orapplication/did+json). Some CDNs default totext/plainfor unknown extensions — override it.- No redirect. Verifiers may refuse to follow a redirect from
.well-known/did.jsonfor security reasons. 200 OKonGET. No auth, no rate limiting on this endpoint.- TLS valid. Self-signed certificates break resolution; use Let's Encrypt or your CDN's managed certificate.
- CORS open. Browser-based verifiers (such as the Keyban Verify app) fetch this file from another origin. Serve it with
Access-Control-Allow-Origin: *— the document is public and non-sensitive — otherwise the browser blocks the request and verification fails.
Quick smoke test:
curl -sSf https://brand.example.com/.well-known/did.json | jq .id
# "did:web:brand.example.com"
Step 3 — Verify the configuration end-to-end
Once published, anyone can re-derive your public key from your domain. The active key is the one referenced in assertionMethod; pick its publicKeyJwk from verificationMethod rather than relying on positional index — after a rotation the array contains multiple entries:
curl -sSf https://brand.example.com/.well-known/did.json \
| jq '.assertionMethod[0] as $id | .verificationMethod[] | select(.id == $id).publicKeyJwk'
Compare the x / y coordinates with what Keyban returned in Step 1 — they must match byte-for-byte. If a verifier reports key mismatch, the file has drifted (typically because a rotation happened in Keyban but the file on the domain was not updated).
You can read back the canonical document from Keyban at any time:
curl -sSf 'https://api.keyban.io/v1/dpp/certification-key/did-web-config' \
-H 'X-Api-Key: $KEYBAN_API_KEY' \
| jq '.didDocument'
Rotating your certification key
When you rotate the active certification key (Admin Console → Certification keys → Rotate, or POST /v1/dpp/certification-key/rotate), Keyban generates a new P-256 key pair. VCs issued before the rotation remain signed by the previous key.
Keyban emits a multi-key DID Document: after a rotation, GET /v1/dpp/certification-key/did-web-config returns a document whose verificationMethod array contains both the new active key and every rotated key the org has owned. Each key carries a stable #key-N fragment (1-based, chronological order over the org's full key history) so a VC's proof.verificationMethod reference keeps resolving forever. Only the active key is referenced in assertionMethod and authentication, so rotated keys remain valid for verification but cannot sign new VCs. Revoked keys are excluded entirely.
V1 rotation procedure:
- Trigger the rotation in Keyban. The on-chain rotation jobs run asynchronously per network. Wait for the active key to flip in the Admin Console.
- Fetch the new DID Document from
GET /v1/dpp/certification-key/did-web-config. It already includes the previously active key under its original fragment. - Publish the new
did.jsonathttps://{domain}/.well-known/did.json, replacing the previous file. - Run the verification check (see next section) to confirm the new file is correctly served.
Verify your published did.json
Keyban exposes a server-side check to confirm the did.json you publish on your domain matches the active certification key. Use it after the initial publication, after every rotation, and any time you change CDN configuration.
curl -sSf -X POST 'https://api.keyban.io/v1/dpp/certification-key/did-web-config/verify' \
-H 'X-Api-Key: $KEYBAN_API_KEY' | jq
The endpoint always responds with HTTP 200 and a discriminated body, so a wrapper UI can render a clear OK / KO badge without branching on status. A 400 is returned only when no domain is configured for the tenant.
{
"status": "ok",
"domain": "brand.example.com",
"fetchedAt": "2026-05-28T10:00:00.000Z"
}
On failure, status: "failed" carries a reason you can map to a localized message:
reason | Meaning | Typical fix |
|---|---|---|
blocked_address | The domain resolves to a private, loopback, link-local or otherwise reserved IP address. Keyban refuses to fetch it as a precaution against SSRF | Point the domain at a publicly-routable IP |
unreachable | DNS failed or connection refused | Confirm the domain resolves and serves traffic |
tls_invalid | Certificate chain validation failed | Renew / install a publicly-trusted certificate |
timeout | Request exceeded 5 seconds | Investigate latency at the origin or CDN |
http_error | 4xx or 5xx response | Make .well-known/did.json reachable with HTTP 200 |
redirect_not_allowed | 3xx response on .well-known/did.json | The did:web spec forbids redirects — serve the file directly |
not_json | Body wasn't parseable JSON, or exceeded 1 MiB | Check Content-Type and the file content |
invalid_did_document | JSON parsed but id or shape is wrong | Re-copy the document returned by GET .../did-web-config |
key_mismatch | Published verificationMethod[].publicKeyJwk doesn't include the org's active key | Re-fetch the canonical document and republish |
missing_assertion_method | assertionMethod doesn't reference the active key | Same fix as above |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Verifier reports DID resolution failed | did.json not reachable, HTTPS misconfigured, redirect in the chain | Run curl -v https://{domain}/.well-known/did.json and check status, content-type, certificate validity |
Verifier reports key mismatch | did.json on the domain is stale after a Keyban rotation | Re-fetch from GET /v1/dpp/certification-key/did-web-config and republish |
400 Bad Request when setting the domain | Scheme, path or port in the payload | Send the bare host only (e.g. brand.example.com) |
did.json returns text/plain | CDN inferred a non-JSON content type | Override the response header to application/json |
Browser verifier reports a network/CORS error but curl works | The file is served without Access-Control-Allow-Origin | Add Access-Control-Allow-Origin: * to the .well-known/did.json response |
| Older VCs stop verifying after rotation | Stale did.json on the domain — Keyban already includes rotated keys, but the published file is outdated | Re-fetch from GET .../did-web-config and republish; confirm with POST .../did-web-config/verify |
Related
- DPP Certification and Trust Chain — conceptual overview of why VCs are signed and how the trust chain anchors on-chain.
- W3C DID Method Web specification — the upstream standard implemented here.
- W3C JsonWebKey2020 suite — public key serialization format used inside the DID Document.