Method GET/brand /{brand_id} /did.json
Retrieve a brand's identity document: the list of its public signing keys, in did:web format. Public endpoint.
On this page
- Authorization
- Rate limit
- Path and query parameters
- Headers
- Request body
- Example request
- Example response
- An entry of verificationMethod
- Finding the key that signed a credential
- What the absence of a key means
- The id field always matches the address called
- The same path on a reseller's shared domain
- Errors
- See also
You retrieve the public keys a brand uses to sign its digital product passports. By the end of this page, you will know how to request this document, find in it the key that signed a given credential, and understand what the absence of a key means.
Full address:
GET https://api.sealtrust.io/v1/brand/{brand_id}/did.jsonThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/brand/{brand_id}/did.json. Both addresses call the
same code. Use the /v1 form for a new integration.
This document is what a verifier goes and fetches on its own. The signed
credential of a passport carries an issuer identifier of the form
did:web:api.sealtrust.io:brand:4242. The public rule of the did:web format
translates that identifier into the address
https://api.sealtrust.io/brand/4242/did.json, that is, this endpoint. Any
standard did:web library performs that translation without knowing anything
about SealTrust.
#
None, this is a public endpoint. You send no API key, no session and no origin header. The response is the same for everyone.
The document contains public keys only. No private key ever leaves our system, and none can be reconstructed from this document.
#Rate limit
600 calls per 60-second window, counted per calling network address. This
counter is the brand DID document's own: the public keys served under /brand/
have theirs. The /v1 prefix does not create a second budget:
/brand/{brand_id}/did.json and /v1/brand/{brand_id}/did.json share the same
counter.
Every response carries three headers.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
brand_id | integer | yes | The brand's numeric identifier. It is the number that follows brand: in the issuer identifier of the signed credential. |
This endpoint has no query parameter.
Here, the brand is named by its number, not by the public code brand_code
that the other routes take. This number is part of the did:web identifier
already signed into the issued credentials. A DID document must live at the
address its own identifier names. So this address does not change.
#Headers
No header is required.
#Request body
None. This request has no body.
#Example request
Identity document of the brand whose identifier is 4242.
curl -i https://api.sealtrust.io/v1/brand/4242/did.jsonconst response = await fetch(
"https://api.sealtrust.io/v1/brand/4242/did.json",
);
if (response.status === 404) {
console.log("No brand carries this identifier.");
} else if (response.ok) {
const document = await response.json();
console.log("Brand identifier:", document.id);
console.log("Published keys:", document.verificationMethod.length);
for (const methode of document.verificationMethod) {
console.log(methode.id, methode.type, methode.publicKeyJwk.crv);
}
// Find the key that signed a given credential.
const kid = "did:web:api.sealtrust.io:brand:4242#key-2";
const clef = document.verificationMethod.find((m) => m.id === kid);
if (clef) {
console.log("Signing key found:", clef.publicKeyJwk);
} else {
console.log("This key is no longer published. Reject the signature.");
}
} else {
console.log(response.status, await response.json());
}import requests
response = requests.get(
"https://api.sealtrust.io/v1/brand/4242/did.json",
timeout=30,
)
if response.status_code == 404:
print("No brand carries this identifier.")
elif response.ok:
document = response.json()
print("Brand identifier:", document["id"])
print("Published keys:", len(document["verificationMethod"]))
for methode in document["verificationMethod"]:
print(methode["id"], methode["type"], methode["publicKeyJwk"]["crv"])
# Find the key that signed a given credential.
kid = "did:web:api.sealtrust.io:brand:4242#key-2"
clef = next(
(m for m in document["verificationMethod"] if m["id"] == kid),
None,
)
if clef:
print("Signing key found:", clef["publicKeyJwk"])
else:
print("This key is no longer published. Reject the signature.")
else:
print(response.status_code, response.json())#Example response
HTTP code 200. The body is JSON, served with the header
Content-Type: application/json.
Here, the brand Exemple SAS has rotated its key once. Both versions remain
published, so the credentials signed under the older one remain verifiable.
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"id": "did:web:api.sealtrust.io:brand:4242",
"verificationMethod": [
{
"id": "did:web:api.sealtrust.io:brand:4242#key-1",
"type": "JsonWebKey2020",
"controller": "did:web:api.sealtrust.io:brand:4242",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"y": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
}
},
{
"id": "did:web:api.sealtrust.io:brand:4242#key-2",
"type": "JsonWebKey2020",
"controller": "did:web:api.sealtrust.io:brand:4242",
"publicKeyJwk": {
"kty": "EC",
"crv": "P-256",
"x": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
"y": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
}
}
],
"assertionMethod": [
"did:web:api.sealtrust.io:brand:4242#key-1",
"did:web:api.sealtrust.io:brand:4242#key-2"
],
"authentication": [
"did:web:api.sealtrust.io:brand:4242#key-1",
"did:web:api.sealtrust.io:brand:4242#key-2"
]
}The five top-level fields are always present.
| Field | Type | Description |
|---|---|---|
@context | string[] | The two vocabularies that give the document's fields their meaning. Always these two values, in this order. |
id | string | The brand's identifier, in did:web format. It is the value carried by the issuer field of a signed credential. |
verificationMethod | object[] | One entry per published public key, from the oldest version to the most recent. |
assertionMethod | string[] | The identifiers of the keys allowed to sign a credential. Repeats the same entries as verificationMethod, in the same order. |
authentication | string[] | The identifiers of the keys allowed to prove control of this identifier. Repeats the same entries. |
#An entry of verificationMethod
| Field | Type | Description |
|---|---|---|
id | string | The key's identifier, of the form <brand identifier>#key-<version number>. |
type | string | Always JsonWebKey2020. |
controller | string | The identifier of the brand that controls this key. Always equals the document's id field. |
publicKeyJwk | object | The public key itself, in JWK format. |
publicKeyJwk carries four fields: kty equals EC, crv equals P-256, x
and y are the two coordinates of the public point, encoded in base64url. These
values are passed as they are to a JWS verification library.
#Finding the key that signed a credential
The header of a passport credential carries a kid field. That kid equals
exactly one of the id values of verificationMethod, for example
did:web:api.sealtrust.io:brand:4242#key-2. You look that value up in the
list, you take the matching publicKeyJwk, and you check the signature with
the ES256 algorithm.
If the kid is not in the list, the signature must be rejected.
#What the absence of a key means
A revoked key is removed from the document. Every credential signed under that version stops being verifiable, and that is the intended result.
A key replaced by a more recent version stays published. It is no longer used to sign new credentials, and the older ones keep verifying.
verificationMethod can be an empty list, with assertionMethod and
authentication empty too. That means the brand has not published any signing
key yet. No credential from that brand is verifiable then.
#The id field always matches the address called
This endpoint always returns a document whose id field equals
did:web:api.sealtrust.io:brand:<brand_id>, exactly the identifier that the
did:web translation rule maps to the address you have just called. You can
therefore check that equality, and reject the document if it does not hold.
This stays true even when the brand otherwise carries its identity on its own
domain name. In that case it signs its new credentials under
did:web:<its domain>, but the credentials it had signed under the identifier
above remain verifiable here, forever. That is deliberate: those credentials
are on goods already sold, and nobody can re-sign them.
A brand can therefore hold two identity documents at once, at two different addresses, each carrying the identifier of its own address. Nothing stands in the way: every credential names the one that concerns it.
#
A reseller may serve all of its client brands under one domain, for example
passeports.revendeur.com. A client brand that signs under
did:web:passeports.revendeur.com:brand:4242 has its identifier translated to
https://passeports.revendeur.com/brand/4242/did.json, and this same endpoint
answers.
On that domain, the document's id field then equals
did:web:passeports.revendeur.com:brand:4242: always the identifier of the
address called. And the domain only answers for the brands our database attaches
to that reseller. For any other brand it returns 404, even when it exists.
On a brand's own domain, however, this path returns 404 for every identifier,
the brand's own included. We issue no identifier of the form
did:web:<brand domain>:brand:<id>, so no document served at that address
could carry the identifier of the address called. A brand's identity on its
own domain is read at
/.well-known/did.json.
#Errors
The body of an error response carries a detail field. Every response from
this endpoint, successful or in error, carries an X-Request-Id header.
| Code | Condition | What to do |
|---|---|---|
| 404 | No brand carries this identifier, or the call arrives on the shared domain of a reseller that does not serve this brand, or on a brand's own domain. detail equals Brand not found. | Check the number that follows brand: in the issuer identifier. Reject the signature: an issuer whose identity document cannot be found proves nothing. |
| 422 | The value sent in the path is not an integer. detail carries the list of validation errors, with the name of the parameter at fault. | Correct the identifier. A brand identifier is written in digits only. |
| 422 | The identity document of the brand found cannot be built. | Report it to support, giving the identifier you called. There is nothing you can correct on your side. |
| 500 | An unexpected error occurred while processing your call. detail equals Internal Server Error. | Retry. If the error persists, contact support, giving the value of X-Request-Id. |
This endpoint returns neither 401 nor 403: it is public and reads no authorization.
#See also
GET /.well-known/did.json, serve a brand's identity document on its own domain.GET /passport/{identifier}/vc, retrieve the signed credential of the passport, in SD-JWT-VC format.GET /passport/{identifier}/vc/verify, check the credential's signature and read the disclosed data.- Trust and proofs, what each proof establishes and how a third party redoes the verification.
Was this page helpful?
Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.