Method GET/passport /{identifier} /vc /verify
Check the signature of the digital passport delivered as a verifiable credential, and read the data revealed at the access tier requested. No session is required for the public and end_user tiers.
On this page
- Authorization
- Where you call from
- The tier you request
- Rate limit
- Path and query parameters
- The three identifier forms accepted
- The six values of access_tier
- Headers
- Request body
- Example request
- Example response
- A check that fails is still a 200 response
- What flips verified to false
- Checking the signature yourself
- Errors
- See also
You have the signature of a product's digital passport checked, and you get back the data that this signature covers. By the time you leave this page, you will know how to request this check from any product identifier, tell a check that fails apart from a request that fails, and know which data the response shows you depending on the access tier you request.
Full URL:
GET https://api.sealtrust.io/v1/passport/{identifier}/vc/verifyThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/passport/{identifier}/vc/verify. Both URLs call the
same code. Use the /v1 form for a new integration.
The passport is delivered in the SD-JWT-VC format, a signed credential in which
each field can be revealed or withheld separately. The issuer is the brand,
identified by a did:web decentralized identifier. This endpoint reassembles
the credential at the access tier you request, checks its signature, and
returns the revealed data to you.
#
No API key scope is checked on this endpoint. Two checks apply all the same: the origin of your call, then the access tier you request.
#Where you call from
Call this endpoint from your server.
We refuse with a 403 any call that carries an Origin or Referer header
designating a domain other than ours. The detail field is then
Forbidden origin. A browser always sets one of these two headers, so a web
page hosted anywhere other than with us cannot call this URL from its visitor's
browser.
The session cookie only works from a page served by one of our domains. We
refuse with a 403 a call that carries this cookie without an Origin or
Referer header, with detail set to Origin or Referer header required.
From a server, present the session token in the Authorization: Bearer header.
#The tier you request
The public and end_user tiers require no account. The four other values of
the access_tier parameter require an account session, presented through the
Authorization: Bearer <session token> header or through the session cookie
set at sign-in.
| Tier requested | What is needed |
|---|---|
public | nothing |
end_user | nothing |
repairer | a session, and an active repairer accreditation on the brand of the product |
recycler | a session, and an active recycler accreditation on the brand of the product |
upstream | a session with access to the brand of the product, or the authority role |
authority | a session carrying the market surveillance authority role |
A session with access to the brand of the product opens the three professional tiers on its own products. The market surveillance authority role opens them as well, on every product.
A partner API key opens nothing here. The token received in the Authorization
header is decoded as a user account session token, and an API key is not one.
The read fails silently and the call goes on as an anonymous call.
#Rate limit
60 calls per 60 second slice, counted per calling IP address. The window is fixed.
This ceiling is shared by every URL that starts with /passport. The
/passport/… and /v1/passport/… forms feed the same counter, the /v1
prefix does not create a second budget.
Every accepted response carries three headers that describe this counter.
| Header | Content |
|---|---|
X-RateLimit-Limit | the ceiling applied over the window, here 60 |
X-RateLimit-Remaining | what is left to you in the current window |
X-RateLimit-Reset | the end timestamp of the window, in seconds |
Going over returns 429, with the same three headers and a Retry-After. On
this endpoint, Retry-After is worth the length of the window, that is 60
seconds.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The identifier of the product. Three forms are accepted, see below. |
access_tier | string | no | The access tier requested. Is public by default. Six values accepted, listed further down. We refuse any other value with a 422. |
#The three identifier forms accepted
| Form | What it looks like | Where you find it |
|---|---|---|
| Serial number | 12 characters, digits and letters | Printed on the product, it is what its QR carries |
| Token identifier | A run of digits, often a very long one | Returned by our responses in the token_id field |
| Identifier hash | 0x followed by 64 hexadecimal characters | Returned by our responses in the uid_hash field |
The identifier hash exists for a QR only product as much as for a product with an NFC chip. The server draws it at random for a QR only product. It derives it from the identifier of the chip for a product with an NFC chip. Both forms therefore look the same and are requested in the same way.
We recognize the form from the way it is written. A value that starts with 0x
and is exactly 66 characters long, we look up as an identifier hash. Any other
value, we look up first as a token identifier, then as a serial number when the
first lookup found nothing.
You write the identifier hash in whatever case you want. You write the serial
number in whatever case you want as well, and we canonicalize it the way the QR
resolver does: in it we read the letters I and L as a 1, the letter O
as a 0. So you can copy by hand a number read on a label.
The certificate of authenticity number is not accepted here.
A product destroyed on the chain or withdrawn from the catalog no longer resolves through this endpoint, and the response is then 404.
#The six values of access_tier
These tiers are different audiences, with no hierarchy between them. Each of the three professional tiers inherits the public tier and the end user tier, then adds what its profession requires.
| Value | What it adds to the fields revealed |
|---|---|
public | product identification, ESPR compliance, REACH and CE marking, recyclability percentage, recycled content percentage, labels, general battery specification |
end_user | environmental impact, full circularity, primary material, certified organic material, durability, energy efficiency, carbon footprint |
repairer | bill of materials, disassembly instructions, repairability index, battery state of health |
recycler | material composition, substances of concern, disassembly instructions, battery state of health |
upstream | material composition, substances of concern, manufacturing, supply chain |
authority | the whole of the data, with no filtering |
A brand can replace these rules with its own. The table above describes what applies in the absence of rules specific to the brand.
#Headers
No header is required for the public and end_user tiers. The five other
tiers require the Authorization header or the session cookie. In every case,
respect the origin rule described above.
#Request body
None. This request has no body.
#Example request
Check of the credential of the product whose printed number is
EXEMPLE00001, at the public tier.
The three examples run from a server. None of them works in a visitor's
browser: the browser sets an Origin header that we refuse, and you receive a
403.
curl -i "https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify?access_tier=public"const url = new URL(
"https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify",
);
url.searchParams.set("access_tier", "public");
const reponse = await fetch(url);
if (reponse.status === 404) {
console.log("Aucune attestation signée à contrôler pour ce produit.");
} else if (reponse.status === 403) {
console.log(
"Appel refusé : exécutez ce code depuis un serveur, jamais depuis un navigateur.",
);
} else if (reponse.ok) {
const resultat = await reponse.json();
console.log(resultat.verified, resultat.error);
console.log(resultat.issuer, resultat.key_version);
console.log(resultat.credential_subject);
} else {
console.log(reponse.status, await reponse.json());
}import requests
response = requests.get(
"https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify",
params={"access_tier": "public"},
timeout=30,
)
if response.status_code == 404:
print("Aucune attestation signée à contrôler pour ce produit.")
elif response.status_code == 403:
print("Appel refusé : exécutez ce code depuis un serveur, jamais depuis un navigateur.")
elif response.ok:
resultat = response.json()
print(resultat["verified"], resultat["error"])
print(resultat["issuer"], resultat["key_version"])
print(resultat["credential_subject"])
else:
print(response.status_code, response.json())#Example response
HTTP code 200. The signature is valid and the tier requested is public.
{
"passport_id": 1,
"issuer": "did:web:api.sealtrust.io:brand:4242",
"vct": "https://schema.sealtrust.io/vct/digital-product-passport",
"key_version": 1,
"access_tier": "public",
"verified": true,
"error": null,
"credential_subject": {
"product_identity": {
"gtin": "03701234567890",
"model": "Cartable Exemple 32",
"brand": "Exemple SAS",
"made_in": "FR"
},
"compliance": {
"eu_espr": true,
"reach": true,
"ce_marking": false
},
"circularity": {
"recyclability_percentage": 62,
"recycled_content_percentage": 0
}
}
}The response has eight fields and nothing else.
| Field | Type | Description |
|---|---|---|
passport_id | integer | The identifier of the passport version the check applied to. |
issuer | string | The did:web decentralized identifier of the issuing brand. Always filled in on this endpoint. |
vct | string | The credential type. Is https://schema.sealtrust.io/vct/digital-product-passport when the brand has not declared another one. |
key_version | integer or null | The version number of the brand signing key that signed the credential. null when that number was not recorded at issuance. |
access_tier | string | The access tier requested, carried over as is. This endpoint never changes the tier requested. |
verified | boolean | true when the signature was checked successfully. |
error | string or null | null when verified is true. Is verification_failed otherwise. That is the only possible value. |
credential_subject | object or null | The data revealed at the tier requested, as the signature covers it. Is null as soon as verified is false. |
#A check that fails is still a 200 response
That is the point to remember from this page. A check failure is not an HTTP error. The response stays 200 and carries the verdict.
{
"passport_id": 1,
"issuer": "did:web:api.sealtrust.io:brand:4242",
"vct": "https://schema.sealtrust.io/vct/digital-product-passport",
"key_version": 1,
"access_tier": "public",
"verified": false,
"error": "verification_failed",
"credential_subject": null
}So always read verified. A 200 code says nothing on its own.
#What flips verified to false
| Cause | What it means |
|---|---|
| The signature does not match the content | The credential was modified after it was issued. |
| The issuer written in the credential is not the one expected for this brand | The credential was issued under an identity other than that of the brand of the product. |
| The credential designates no readable key version | The header of the credential carries no usable key version number. |
| The key version cited by the credential does not exist for this brand | The key that signed is not known. |
| That key version was revoked | The brand withdrew this key. The credentials it signed are no longer recognized. |
The response does not say which of these causes applies. The error field is
verification_failed in all of these cases.
#Checking the signature yourself
You are not required to ask us for this verdict. The brand publishes its public
signing keys in a decentralized identity document, served publicly at
GET https://api.sealtrust.io/brand/{brand_id}/did.json. A brand that hosts
its identity on its own domain serves it at
https://<its domain>/.well-known/did.json.
With that document and any off-the-shelf did:web and SD-JWT-VC library, you
check the signature without going through us. That is what makes the passport
enforceable without depending on our availability.
#Errors
The body of an error response carries a detail field.
The checks run in this order: origin of the call, resolution of the product, lookup of the published passport, check of the tier requested, presence of an issued credential, then identification of the issuing brand. The first step that fails gives the response.
| Code | Condition | What to do |
|---|---|---|
| 401 | access_tier=authority is requested with no valid session. detail is Authority-tier access requires authentication. | Sign in with an account carrying the market surveillance authority role. A partner API key will not do. |
| 401 | access_tier is repairer, recycler or upstream, and the call carries no valid session. detail is Professional-tier access requires authentication. | Present an account session token, or request the public or end_user tier. |
| 403 | The call carries an Origin or Referer header that does not designate one of our domains, which happens for any call sent from a web page hosted elsewhere. detail is Forbidden origin. | Call this endpoint from your server. A call sent by a visitor's browser cannot succeed. |
| 403 | The call carries the session cookie and has neither an Origin header nor a Referer header, which happens when a browser cookie is replayed on the command line. detail is Origin or Referer header required. | Remove the cookie and present the session token in the Authorization: Bearer header. |
| 403 | access_tier=authority is requested by a signed-in account that does not carry this role. detail is Authority-tier access is restricted to market surveillance authorities. | Request the tier that matches your clearance. |
| 403 | A professional tier is requested by a signed-in account that has neither access to the brand of the product, nor the matching accreditation on that brand. detail is This tier is restricted to the product's brand, partners holding the matching accreditation, and market-surveillance authorities. | Ask the brand for the accreditation that matches your profession, then request the tier of that profession. |
| 404 | No product matches this identifier, under any of the three accepted forms. detail is Product not found. | Check the identifier. A product destroyed on the chain or withdrawn from the catalog gives this same response. |
| 404 | The product exists, but no public passport is published for it nor for its model. detail is No published passport found for this product. | There is nothing to check. A passport reserved for the owner or for the brand also gives this response. |
| 404 | A public passport exists, but no signed credential was issued for this version. detail is No VC issued for this passport yet. | Ask the brand to issue the credential for this version of the passport. The passport stays readable through the read endpoints. |
| 404 | The brand of the passport could not be found. detail is Brand not found. | Contact support giving the identifier you called. No action on your side fixes this response. |
| 422 | access_tier is not one of the six accepted values. detail is a list, each entry carrying loc, type and msg. | Read loc to know which parameter is at fault, then fix its value. |
| 429 | The ceiling of 60 calls per 60 seconds is reached for your IP address, across every path starting with /passport. detail is Rate limit exceeded: 60 requests per 60s. | Wait the number of seconds given by Retry-After, then try again. |
| 500 | An unexpected error occurred while your call was being processed. detail is Internal Server Error. The response carries an X-Request-Id header. | Try again. If the error persists, contact support giving the value of X-Request-Id. |
#See also
GET /passport/{identifier}/vc, retrieve the signed credential of the passport, in the SD-JWT-VC format.GET /passport/{identifier}/vc/preview, see, without a signature, what an access tier would expose.GET /brand/{brand_id}/did.json, retrieve the public signing keys of a brand.GET /.well-known/did.json, serve the identity document of a brand on its own domain.
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.