Method GET/passport /{identifier} /vc
Retrieve the signed credential of a passport, in SD-JWT-VC format, filtered to the access tier asked for. Public endpoint at the public tier.
On this page
You retrieve the passport of a product as a digital credential signed by the brand. When you leave this page, you will know how to ask for that credential at the access tier that concerns you, how to read the six fields of the response, and where to find the public key that lets you check its signature without trusting us.
Full address:
GET https://api.sealtrust.io/v1/passport/{identifier}/vcThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/passport/{identifier}/vc. Both addresses call the
same code. Use the /v1 form for a new integration.
The response is a JSON object of six fields. The passport itself sits in a
single field, as a character string signed in the SD-JWT-VC format. To read the
passport data as directly usable JSON, call GET /v1/passport/{identifier}.
#
None for the public tier. This endpoint is public at that tier only.
A partner API key opens nothing here. The tiers that ask for an identity are opened with a user account session token, never with an API key.
The five other values of the access_tier parameter require an account session, which you
present through the Authorization: Bearer <session token> header or through
the session cookie set at sign-in.
| Tier asked for | What it takes |
|---|---|
public | nothing For a battery passport, the fields that point 1 of Annex XIII makes public are added: hazardous substances, critical raw materials, carbon footprint and its class, due diligence report, post-consumer recycled content per metal and share of renewable content. Conversely the part numbers, the battery status and the expected lifetime in cycles are removed: points 2 and 4 reserve them to persons with a legitimate interest. For a textile passport, the name and the percentage of each material are added, the fibre composition that Regulation (EU) No 1007/2011 requires on the label. For a footwear passport, the material of the upper, the lining, the insole and the sole, which Directive 94/11/EC puts on the shoe. |
end_user | a session, whatever the account |
repairer | a session, and an active repairer accreditation on the brand of the product, or access to that brand, or the authority role |
recycler | a session, and an active recycler accreditation on the brand of the product, or access to that brand, or the authority role |
upstream | a session of an administrator of the product's brand, or the authority role. No accreditation opens this tier. |
authority | a session carrying the market surveillance authority role |
A session of an administrator of the product's brand opens the three professional tiers on the products of that brand. A client, repairer or recycler account that is still tied to the brand, a former administrator for instance, is not part of it: it receives 403, or the one tier of its accreditation.
The access tier rules are the same as those of the
GET /v1/passport/{identifier} endpoint. This path therefore never gives access
to more fields than the JSON read.
One difference separates the two paths. This endpoint only serves the passports whose visibility is public. A passport reserved for the owner or reserved for the brand is never returned here, not even to its owner.
#The origin check
Your server-to-server calls go through as they are. Two situations give a 403.
A call issued by a web page opened on a domain that is not ours carries an
Origin or Referer header that we refuse. So do not call this endpoint from
the browser of a visitor, call it from your server.
A call that carries the session cookie without an Origin or Referer header
is refused as well. The session cookie only holds from a page served by one of
our domains. From a server or from the command line, present the token in
Authorization: Bearer.
#Rate limit
600 calls per 60-second slice, counted per calling network address. The window is fixed.
That counter is shared by every read (GET) whose path starts with /passport. The calls you
address to one of them eat into the budget of the others. The /v1 prefix does
not create a second budget: /v1/passport/EXEMP1E00001/vc and
/passport/EXEMP1E00001/vc fill the same counter.
Every accepted response carries three headers.
| Header | Content |
|---|---|
X-RateLimit-Limit | the limit applied over the window, here 600 |
X-RateLimit-Remaining | what is left to you in the current window |
X-RateLimit-Reset | the timestamp of the end of the window, in seconds since January 1, 1970 |
Going over returns 429, with those three headers and Retry-After. On this
endpoint, Retry-After is the duration of the window, that is 60 seconds.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The product whose credential you want. Three forms are accepted, see below. |
access_tier | string | no | The access tier asked for. Defaults to public. Six accepted values, listed further down. |
No header is required in the request.
#The three accepted forms of identifier
| Form | What it looks like | Where you find it |
|---|---|---|
| Serial number | 12 characters, digits and uppercase letters. The letters I, L, O and U never appear in it. | Printed on the product, it is what its QR code carries |
| Token identifier | A run of digits, often very long | 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 just as for a product with an NFC chip. The server draws it at random for a QR product, it derives it from the identifier of the chip for an NFC product.
The server recognizes the form from the way it is written. It looks up a value
that starts with 0x and is exactly 66 characters long as an identifier hash.
It looks up any other value first as a token identifier. It tries the serial
number last, when the first two lookups have returned nothing.
The server recognizes the identifier hash whatever the case. It also recognizes
the serial number whatever the case, and it canonicalizes it the way the QR
resolver does: it reads the letters I and L as a 1, the letter O as a
0. You can therefore send it a number copied by hand from a label.
This endpoint only resolves the products still in the catalog of the brand. An item destroyed on chain, replaced by a later version or withdrawn without replacement answers 404.
#The six values of access_tier
These tiers are different audiences, with no hierarchy between them. A recycler is not above a repairer. Each of the three professional tiers inherits the public tier and the end user tier, then adds what its trade calls for.
| Value | What the credential reveals |
|---|---|
public | product identification, ESPR compliance, REACH and CE marking, recyclability percentage, recycled content percentage, labels, general battery specification, the Article 13(4) and 13(5) markings, usable extinguishing agent, EU declaration of conformity, waste management information. These fields appear in the clear in the signed token, no disclosure is attached. |
end_user | environmental impact, full circularity, primary material, certified organic material, durability, energy efficiency, carbon footprint |
repairer | bill of materials, dismantling instructions, repairability index, battery state of health |
recycler | material composition, substances of concern, dismantling instructions, battery state of health |
upstream | material composition, substances of concern, manufacturing, supply chain |
authority | every field of the document |
The fields the console writes at the root of the passport each have their own tier, named one by one: see Passport access rules.
A brand can replace these rules with its own. The table above describes what applies in the absence of rules of the brand's own. The same rules apply here and on the JSON read.
The server refuses any other value with 422. It returns the tier actually served
in the access_tier field of the response and in the X-DPP-Access-Tier
header. Read one of the two rather than assuming it.
#Request body
None. This request has no body.
#Example request
Public credential of the product whose printed number is EXEMP1E00001.
curl -s "https://api.sealtrust.io/v1/passport/EXEMP1E00001/vc?access_tier=public"const identifier = "EXEMP1E00001";
const url = new URL(
`https://api.sealtrust.io/v1/passport/${encodeURIComponent(identifier)}/vc`,
);
url.searchParams.set("access_tier", "public");
const response = await fetch(url, { method: "GET" });
if (response.status === 403) {
throw new Error(
"Call refused: run this request from your server, never from a browser.",
);
}
if (!response.ok) {
throw new Error(`SealTrust replied ${response.status}`);
}
const justificatif = await response.json();
console.log(justificatif.issuer, justificatif.access_tier);
console.log(justificatif.format, justificatif.vct);
const segments = justificatif.sd_jwt_vc.split("~");
console.log("Signed token:", segments[0]);
console.log("Segments de divulgation :", segments.slice(1, -1).length);import requests
from urllib.parse import quote
identifier = "EXEMP1E00001"
response = requests.get(
f"https://api.sealtrust.io/v1/passport/{quote(identifier, safe='')}/vc",
params={"access_tier": "public"},
timeout=30,
)
if response.status_code == 403:
raise SystemExit(
"Call refused: run this request from your server, never from a browser."
)
if not response.ok:
raise SystemExit(f"SealTrust replied {response.status_code}")
justificatif = response.json()
print(justificatif["issuer"], justificatif["access_tier"])
print(justificatif["format"], justificatif["vct"])
segments = justificatif["sd_jwt_vc"].split("~")
print("Signed token:", segments[0])
print("Segments de divulgation :", len(segments[1:-1]))The three tabs call the same address with the same values. The curl tab writes
the raw response to standard output. The TypeScript and Python tabs extract the
same fields from it, in the same order.
#Example response
HTTP status code 200.
The values below are fictitious. The signed token and the disclosure segments are shortened, a real token is several thousand characters long.
{
"passport_id": 1,
"issuer": "did:web:api.sealtrust.io:brand:4242",
"vct": "https://schema.sealtrust.io/vct/digital-product-passport",
"access_tier": "public",
"format": "dc+sd-jwt",
"sd_jwt_vc": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRjK3NkLWp3dCIsImtpZCI6ImRpZDp3ZWI6YXBpLnNlYWx0cnVzdC5pbzpicmFuZDo0MjQyI2tleS0xIn0.RVhFTVBMRV9DSEFSR0VfVVRJTEU.RVhFTVBMRV9TSUdOQVRVUkU~"
}| Field | Type | Presence | Description |
|---|---|---|---|
passport_id | integer | always | The identifier of the passport version this credential relates to. |
issuer | string | always | The did:web decentralized identifier of the brand that signed. It is what leads to the public verification key. Always filled in on this endpoint. |
vct | string | always | The identifier of the credential template. Is https://schema.sealtrust.io/vct/digital-product-passport in the absence of a value recorded on the passport. |
access_tier | string | always | The tier actually served. |
format | string | always | Always dc+sd-jwt. It is the media type of the selective disclosure credential. |
sd_jwt_vc | string | always | The credential itself. See below. |
#Response headers worth knowing
| Header | Content |
|---|---|
X-DPP-Access-Tier | the tier actually served |
Cache-Control | public, max-age=60, s-maxage=300 at the public tier, private, no-store at any other tier. Only the public response, the same for every anonymous caller, may be kept by a cache. |
#Reading the sd_jwt_vc field
The content of sd_jwt_vc is a run of segments separated by the ~ character.
The last segment is always empty, so the string ends with a ~.
<signed token>~<disclosure>~<disclosure>~The first segment is a signed token in three parts, separated by dots. The header and the payload are encoded in base64url, without padding. You decode them without a key.
The header carries three values.
| Value | Content |
|---|---|
alg | ES256. The signature is an ECDSA signature on curve P-256. |
typ | dc+sd-jwt |
kid | The identifier of the key that signed, in the form <brand did>#key-<version number>. |
The payload carries the following fields.
| Field | Content |
|---|---|
iss | The did:web identifier of the issuing brand. It holds the same value as the issuer field of the response. |
vct | The identifier of the credential template. |
iat | The issuance date, in seconds since January 1, 1970. |
@context | ["https://www.w3.org/ns/credentials/v2", "https://schema.sealtrust.io/dpp/v1"] |
type | ["VerifiableCredential", "DigitalProductPassport"] |
issuer | A repeat of iss, expected by the verifiable credentials data model. |
validFrom | The issuance date in ISO 8601 format, to the second, in universal time. |
credentialSubject | The passport data. The public fields appear in the clear in it. The others are replaced by hashes, under the _sd key. |
credentialSchema | An object with two keys, id which repeats vct, and type which is JsonSchema. |
product | The identity of the product: uid_hash, token_id and name. Never masked. |
brand | The identity of the brand: name, lei_code, eori_number, website_url, postal_address, contact_email. Never masked. The values that are not filled in are absent. |
A passport can cover a product model or one specific item. When the brand
publishes a model passport, it holds for every item that shares the same product
code, and the credential issued at that publication carries uid_hash and
token_id at null in the product block: it designates no item in
particular.
The following segments are the disclosures. Each one is an array of three elements encoded in base64url: a salt, the name of the field, its value. You decode them without a key. Their number depends on the tier asked for.
WyJFWEVNUExFMDAwMDAwMDAwMDAwMDAwMCIsInJlcGFpcmFiaWxpdHlfaW5kZXgiLDguMl0That example segment decodes to
["EXEMPLE0000000000000000", "repairability_index", 8.2].
At the public tier, the response reveals nothing more than the fields always
present in the signed document. The string is then reduced to the signed token
followed by a ~. Each other tier adds the disclosures that concern it.
#Check the signature yourself
The issuer field carries a did:web identifier. It designates a public
document that contains the public keys of the brand, expressed as
JsonWebKey2020. The key to use is the one whose identifier matches the kid
of the token header. That document contains only the keys that are not revoked,
so a revoked key no longer appears in it.
An identifier of the form did:web:api.sealtrust.io:brand:4242 resolves to
https://api.sealtrust.io/brand/4242/did.json. An identifier of the form
did:web:id.exemple-sas.example resolves to
https://id.exemple-sas.example/.well-known/did.json. A brand can host that
document itself on its own domain, in which case the verification of its
passports depends on none of our servers.
If you prefer the verification to be done for you, call
GET /v1/passport/{identifier}/vc/verify.
#Model or lot passport
A passport of a model, or of a production lot, is bound to no item. It
therefore has no identifier to pass in {identifier}. Its signed credential is read at the
GS1 address of the passport itself, next to the passport and its proof:
GET https://api.sealtrust.io/v1/passport/01/{gtin}/vc
GET https://api.sealtrust.io/v1/passport/01/{gtin}/10/{lot}/vcThe first address targets the passport of the model that carries this GTIN. The
second targets the passport of lot {lot} of that model. Like the other
addresses of this page, each one also answers without the /v1 prefix.
| Name | Type | Required | Description |
|---|---|---|---|
gtin | string | yes | The GTIN of the model, 8, 12, 13 or 14 digits. Its check digit is verified. |
lot | string | on the second address | The lot number (GS1 application identifier 10), as printed, upper and lower case included. A reserved character, such as /, is percent-encoded. |
It is the only way to read the credential of a brand on the Compliance offer, which registers no item.
The access tier, the authorization rules, the rate limit and the fields of the response are those of this page. The response adds two fields, and a third one for a lot.
| Field | Type | Presence | Description |
|---|---|---|---|
level | string | on these two addresses | model for a model passport, batch for a lot passport. |
gtin | string | on these two addresses | The GTIN, brought to 14 digits. |
batch_code | string | on the lot address | The lot number. |
The credential of a model or a lot carries the identity of the model only. In
its product block, uid_hash and token_id are null.
The address of a lot targets the passport of that lot only. A lot without a published passport answers 404, and never with the credential of the model.
#Errors
The body of an error response carries a detail field.
| Code | Condition | What to do |
|---|---|---|
| 401 | You ask for end_user without a session. detail is End-user-tier access requires authentication. | Sign in with any account, or request the public tier. |
| 401 | You ask for authority without a session. detail is Authority-tier access requires authentication. | Sign in, then present the session token in Authorization: Bearer. |
| 401 | You ask for repairer, recycler or upstream without a session. detail is Professional-tier access requires authentication. | Sign in, then present the session token in Authorization: Bearer. An API key will not do. |
| 403 | You ask for authority with a session that does not carry that role. detail is Authority-tier access is restricted to market surveillance authorities. | Ask for a tier that matches your situation. |
| 403 | You ask for a professional tier without an active accreditation on the brand of the product, without being an administrator of that brand and without the authority role. detail starts with This tier is restricted to the product's brand. | Ask the brand to accredit you, then ask again for the tier that matches your trade. |
| 403 | The call carries an Origin or Referer header that does not designate one of our domains, which happens for any call issued from a web page hosted elsewhere. detail is Forbidden origin. | Call this endpoint from your server, never from the browser of a visitor. |
| 403 | The call carries a session cookie without an Origin or Referer header. detail is Origin or Referer header required. | Present the token in Authorization: Bearer instead of the session cookie. |
| 404 | No product in the catalog matches this identifier, under any of the three accepted forms. detail is Product not found. | Check the identifier. An item destroyed on chain, replaced by a later version or withdrawn gives this same response. |
| 404 | The product exists, but no passport published with public visibility is attached to it, neither directly nor through its model. detail is No published passport found for this product. | Do not treat this response as a failure. This product has no public passport. A passport reserved for the owner or for the brand gives the same response. |
| 404 | The passport exists and it is public, but no signed credential has been issued for it yet. detail starts with No VC issued for this passport yet. | Read the passport in JSON with GET /v1/passport/{identifier}. Publishing a version of a passport issues its credential: ask the brand to republish the current version. |
| 404 | The brand of the passport is not resolvable. detail is Brand not found. | Report it to support. No action on your side changes this response. |
| 404 | The call arrives on a brand's verified domain name, and the product belongs to another brand. detail is Product not found. | Call this address on the domain of the brand concerned, or on api.sealtrust.io. The message is the same as for an unknown identifier, so as not to confirm that it exists. |
| 400 | On the address of a model or a lot, the check digit of the GTIN is wrong. detail is Invalid GTIN: the check digit does not match. | Copy the GTIN again from the label. |
| 404 | On the address of a model or a lot: no passport published with public visibility answers for this GTIN or this lot, several brands publish a passport under this GTIN, or the call arrives on the verified domain of another brand. detail is Unknown GS1 Digital Link in all these cases. | Check the GTIN and the lot number. The message is the same in all these cases, so as not to confirm that a GTIN exists. |
| 422 | The value of access_tier is not one of the six accepted. detail is a list of objects that name the parameter at fault. | Correct the value. The six accepted values are listed above. |
| 429 | The limit of 600 calls per 60 seconds is reached for your network address, across every /passport read. detail is Rate limit exceeded: 600 requests per 60s. | Wait the number of seconds indicated by Retry-After, then try again. Cache the response on your side. |
| 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 quoting the value of X-Request-Id. |
#See also
GET /passport/{identifier}/vc/preview, see, without a signature, what an access tier would expose.GET /passport/{identifier}/vc/verify, check the signature of the credential and read the data revealed.GET /brand/{brand_id}/did.json, retrieve the public signing keys of a brand.- Publish a Digital Product Passport, publish, choose who sees which fields, export and have it verified.
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.