Skip to main content

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 scope

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:update permission, 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 reach https://{domain}/.well-known/did.json and receive a 200 OK with Content-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

  1. VerifierBuyer, partner, AI agent
  2. brand.example.comTenant-hosted
  3. VCFrom DPP page
  1. 1Read proof.verificationMethod (did:web:brand.example.com#key-1)
  2. 2GET /.well-known/did.json
  3. 3DID Document with publicKeyJwk
  4. 4Verify VC signature locally with the JWK
A verifier resolves the DID Document from the tenant's domain, then uses the public key to verify the VC signature without contacting Keyban.

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"]
}
}
Domain format

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 (or application/did+json). Some CDNs default to text/plain for unknown extensions — override it.
  • No redirect. Verifiers may refuse to follow a redirect from .well-known/did.json for security reasons.
  • 200 OK on GET. 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 keysRotate, 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:

  1. 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.
  2. 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.
  3. Publish the new did.json at https://{domain}/.well-known/did.json, replacing the previous file.
  4. 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:

reasonMeaningTypical fix
blocked_addressThe domain resolves to a private, loopback, link-local or otherwise reserved IP address. Keyban refuses to fetch it as a precaution against SSRFPoint the domain at a publicly-routable IP
unreachableDNS failed or connection refusedConfirm the domain resolves and serves traffic
tls_invalidCertificate chain validation failedRenew / install a publicly-trusted certificate
timeoutRequest exceeded 5 secondsInvestigate latency at the origin or CDN
http_error4xx or 5xx responseMake .well-known/did.json reachable with HTTP 200
redirect_not_allowed3xx response on .well-known/did.jsonThe did:web spec forbids redirects — serve the file directly
not_jsonBody wasn't parseable JSON, or exceeded 1 MiBCheck Content-Type and the file content
invalid_did_documentJSON parsed but id or shape is wrongRe-copy the document returned by GET .../did-web-config
key_mismatchPublished verificationMethod[].publicKeyJwk doesn't include the org's active keyRe-fetch the canonical document and republish
missing_assertion_methodassertionMethod doesn't reference the active keySame fix as above

Troubleshooting

SymptomLikely causeFix
Verifier reports DID resolution faileddid.json not reachable, HTTPS misconfigured, redirect in the chainRun curl -v https://{domain}/.well-known/did.json and check status, content-type, certificate validity
Verifier reports key mismatchdid.json on the domain is stale after a Keyban rotationRe-fetch from GET /v1/dpp/certification-key/did-web-config and republish
400 Bad Request when setting the domainScheme, path or port in the payloadSend the bare host only (e.g. brand.example.com)
did.json returns text/plainCDN inferred a non-JSON content typeOverride the response header to application/json
Browser verifier reports a network/CORS error but curl worksThe file is served without Access-Control-Allow-OriginAdd Access-Control-Allow-Origin: * to the .well-known/did.json response
Older VCs stop verifying after rotationStale did.json on the domain — Keyban already includes rotated keys, but the published file is outdatedRe-fetch from GET .../did-web-config and republish; confirm with POST .../did-web-config/verify