Skip to content

Link your websites to your DID

Your DID can name other websites you run, for example your shop, your documentation or a brand site. A claim made by the DID alone proves nothing, because anyone can write any address into their own document. DID.is counts a website as linked only when both sides say so:

  1. The DID lists the website in alsoKnownAs.
  2. The website publishes a DID Configuration file that names the DID, signed with one of the DID’s own keys.

When both are true, the website is shown as CONFIRMED. This page walks through the setup, using the link between did:web:did.is and https://docs.did.is as a working example.

Your websitehttps://shop.example
DID.is checks
  • The DID lists the website in alsoKnownAs
  • The website names the DID
  • Signed with the DID's own key
  • Not expired, exact origin
Anyone who checksCONFIRMED

What a confirmation shows

  • Whoever controls the DID’s signing key also controls what the website serves at /.well-known/did-configuration.json.
  • The link was checked in both directions at the time shown.

What it does not show

  • Who that party is. A confirmation links a DID and a website; it does not identify a company or person.
  • Anything about a specific page or profile path. Only the website origin (scheme, host and port) is confirmed.
  • A DID you control with a key listed under assertionMethod. The example uses did:web:did.is and its key did:web:did.is#key-1 (Ed25519).
  • The ability to publish a file at https://<your-website>/.well-known/did-configuration.json.
  • The website must use https://. Plain http:// addresses cannot be confirmed.

Add the website to alsoKnownAs in your DID document. A bare origin is clearest; a path such as https://shop.example/about is accepted, but only its origin (https://shop.example) is checked.

{
"id": "did:web:did.is",
"alsoKnownAs": ["https://docs.did.is/"],
"verificationMethod": [{ "id": "did:web:did.is#key-1", "…": "…" }],
"assertionMethod": ["did:web:did.is#key-1"]
}

Step 2: publish a DID Configuration on the website

Section titled “Step 2: publish a DID Configuration on the website”

The file follows the DIF Well Known DID Configuration specification. It holds a Domain Linkage Credential: a JWT signed with your DID’s key that says “this DID is linked to this origin”.

The credential must have:

Field Value
header kid A key in your DID’s assertionMethod, e.g. did:web:did.is#key-1
iss, sub, vc.issuer, vc.credentialSubject.id Your DID, all four the same
vc.credentialSubject.origin The website origin exactly, e.g. https://docs.did.is
vc.type ["VerifiableCredential", "DomainLinkageCredential"]
nbf / exp and issuanceDate / expirationDate Valid now. Pick an expiry you will remember to renew.

Sign it on a machine that holds the key, never in the browser and never on the website itself:

import { SignJWT, importJWK } from "jose";
import { readFileSync, writeFileSync } from "node:fs";
const did = "did:web:did.is";
const origin = "https://docs.did.is";
const privateJwk = JSON.parse(readFileSync("private.jwk", "utf8")); // keep this file private
const now = new Date();
const until = new Date(Date.now() + 365 * 24 * 3600 * 1000);
const iso = (d: Date) => d.toISOString().replace(/\.\d{3}Z$/, "Z");
const jwt = await new SignJWT({
vc: {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://identity.foundation/.well-known/did-configuration/v1",
],
type: ["VerifiableCredential", "DomainLinkageCredential"],
issuer: did,
issuanceDate: iso(now),
expirationDate: iso(until),
credentialSubject: { id: did, origin },
},
})
.setProtectedHeader({ alg: "EdDSA", typ: "JWT", kid: `${did}#key-1` })
.setIssuer(did)
.setSubject(did)
.setNotBefore(Math.floor(now.getTime() / 1000))
.setExpirationTime(Math.floor(until.getTime() / 1000))
.sign(await importJWK(privateJwk, "EdDSA"));
writeFileSync(
"did-configuration.json",
JSON.stringify(
{ "@context": "https://identity.foundation/.well-known/did-configuration/v1", linked_dids: [jwt] },
null,
2,
),
);

Upload did-configuration.json so that it is served at https://<your-website>/.well-known/did-configuration.json with status 200. You can see the live example at docs.did.is/.well-known/did-configuration.json.

One website can link several DIDs: add one credential per DID to linked_dids.

Open your DID on did.is, for example did.is/did:web:did.is. The Linked accounts section lists each alsoKnownAs entry and its status. Or use the API:

Terminal window
curl -s https://did.is/api/v1/resolve/did:web:did.is
{
"evidence": {
"linkedAccounts": [
{
"value": "https://docs.did.is/",
"kind": "WEB_ORIGIN",
"origin": "https://docs.did.is",
"status": "CONFIRMED",
"configurationStatus": "VERIFIED",
"detail": "https://docs.did.is publishes a DID Configuration signed by this DID's key."
}
]
}
}
status Meaning What to fix
CONFIRMED Both directions check out. Nothing.
NOT_CONFIRMED The website does not link back: no file, or the file names other DIDs only. Publish the file from step 2.
FAILED The file names this DID, but the credential does not verify: wrong key, wrong origin, expired, or a bad signature. Read detail, then re-sign.
UNSUPPORTED The credential uses a proof format DID.is does not verify. Use the JWT format shown above.
UNREACHABLE The website could not be fetched. Check that the file is public over https://.
NOT_CHECKABLE Not a web address (another DID, acct:…), plain http://, or over the limit. Only https:// websites can be confirmed.

DID.is reports up to 8 alsoKnownAs entries and checks up to 4 different websites per lookup. Your DID’s own website is shown as SAME_ORIGIN and uses the existing origin binding check.

If you accept DIDs from partners and want to be sure a DID belongs to a specific website, use the linkedAccounts policy rule. List 1 to 4 bare https:// origins; each must be declared by the DID and confirmed.

Terminal window
curl -X POST https://did.is/api/v1/policies/evaluate \
-H 'content-type: application/json' \
-d '{"did":"did:web:did.is","policy":{"name":"official-website","rules":{"didResolution":"required","linkedAccounts":["https://docs.did.is"]}}}'

For did:web:did.is this returns PASS with “All 2 evaluated rules pass.” The observedValue of the rule shows each origin and its status:

{
"rule": "linkedAccounts",
"status": "PASS",
"expected": ["https://docs.did.is"],
"observedValue": { "https://docs.did.is": "CONFIRMED" },
"message": "1 linked website(s) confirmed in both directions"
}

A website the DID does not list is reported as NOT_DECLARED and the rule fails. A website that could not be reached makes the rule INDETERMINATE, which is never a pass. You can try this in the policy playground with the Official website preset. See Policies for every rule.

A link can break without anyone touching the DID: the file is removed in a redesign, or the credential expires. Add the DID to monitoring in the console. When a linked website’s status changes while the DID document stays the same, DID.is sends a linked_accounts.changed event:

{
"type": "linked_accounts.changed",
"data": {
"changes": [{ "origin": "https://docs.did.is", "from": "CONFIRMED", "to": "NOT_CONFIRMED" }],
"documentHash": "…"
}
}

The credential stops counting the moment its expiry passes, and the website then shows FAILED. Put the expiry date in your calendar, sign a new credential a few weeks before, and replace the file. The DID document does not need to change.