Method GET/passport /{identifier}
Read a product's published digital passport from its printed number, its token identifier or its chip hash, at the requested access tier. Public endpoint.
On this page
- Authorization
- Rate limit
- Path and query parameters
- The three accepted identifier forms
- The six values of access_tier
- Response headers to know about
- Request body
- Example request
- Example response
- The fields of the response
- The warranty block
- The evidence block
- The lifecycle block
- The integrity block
- The JSON-LD response
- Errors
- Why the IPFS copy is not always given at the public tier
- See also
You read the published digital passport of a single product. By the end of this page, you will know how to retrieve its data at the access tier you request, read its warranty, know on what basis each section can be believed, and recognize a product withdrawn from the catalog.
Full address:
GET https://api.sealtrust.io/v1/passport/{identifier}The same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/passport/{identifier}. Both addresses call the same
code. Use the /v1 form for a new integration.
#
None for the public tier, the default one. This endpoint is public at that tier.
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 proceeds as an anonymous call.
The five other values of the access_tier parameter do, on the other hand, 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 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 a repairer accreditation on the product's brand |
recycler | a session, and a recycler accreditation on the product's brand |
upstream | a session of an administrator of the product's brand, or the authority role |
authority | a session carrying the market surveillance authority role |
A session of an administrator of the product's brand opens the three trade tiers on its own products. A session carrying the market surveillance authority role opens them as well. 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.
#Rate limit
600 calls per 60-second window, counted per calling IP address. The window is
fixed. This limit is shared by every read (GET) whose address starts with /passport, and the
/passport/… and /v1/passport/… forms feed the same counter.
Every accepted response carries three headers that describe this counter.
| 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 |
Going over returns 429, with the same three headers and a Retry-After in
seconds.
#Path and query parameters
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The product's identifier. Three forms are accepted, see below. |
access_tier | string | no | The access tier requested. Is public by default. Six accepted values, listed further down. |
verify_integrity | boolean | no | Is false by default. At true, the server retrieves the passport's IPFS copy, compares its hash, and adds an integrity block to the response. |
format | string | no | Absent by default, and the server then returns the JSON described below. The value jsonld returns the same filtered content, expressed in Schema.org and GS1. The server ignores any other value and returns the default response. |
#The three accepted identifier forms
| 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 string of digits, often very long | Returned by our responses in the token_id field |
| Chip hash | 0x followed by 64 hexadecimal characters | Returned by our responses in the uid_hash field |
The server recognizes the chip hash whatever the case. It recognizes the serial
number the same way, and it canonicalizes it the way the QR resolver does: it
reads the letters I and L as a 1, and the letter O as a 0. So you can
retype by hand the number read off a label, even if you confuse those
characters.
The server recognizes the form from the way it is written. It looks for a value
that starts with 0x and is exactly 66 characters long as a chip hash. It looks
for any other value first as a token identifier, then, if that lookup gives
nothing, as a serial number.
#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 trade tiers inherits the public tier and the end user tier, then adds what its trade requires.
| Value | What it adds to the fields of data |
|---|---|
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, link to the EU declaration of conformity and its structured content once the brand has filled it in, waste management information, and for a textile or a footwear passport the label mentions described above |
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 entirety of the data, with no filtering |
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, by product category. The table above describes what applies in the absence of rules specific to the brand.
The server refuses any other value with a 422. It returns the tier actually
served in the access_tier field and in the X-DPP-Access-Tier header of the
default JSON response. Read one of the two rather than assuming it. With
format=jsonld, neither that field nor that header exists, see below.
#Response headers to know about
| 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. |
The server sets X-DPP-Access-Tier only on the default JSON response.
Cache-Control carries the same value on both formats.
No header is required in the request.
#Request body
None. This request has no body.
#Example request
Reading the public passport of the product whose printed number is
EXEMP1E00001. The three examples make the same call, stop the program on an
error response, then display the same three values:
passport_version, access_tier and data.product_identity.
curl --fail-with-body -s \
"https://api.sealtrust.io/v1/passport/EXEMP1E00001?access_tier=public" \
| jq '{passport_version, access_tier, product_identity: .data.product_identity}'const identifier = "EXEMP1E00001";
const url = new URL(
`https://api.sealtrust.io/v1/passport/${encodeURIComponent(identifier)}`,
);
url.searchParams.set("access_tier", "public");
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
throw new Error(`SealTrust replied ${response.status}`);
}
const passport = await response.json();
console.log({
passport_version: passport.passport_version,
access_tier: passport.access_tier,
product_identity: passport.data.product_identity,
});import requests
from urllib.parse import quote
identifier = "EXEMP1E00001"
response = requests.get(
f"https://api.sealtrust.io/v1/passport/{quote(identifier, safe='')}",
params={"access_tier": "public"},
timeout=30,
)
response.raise_for_status()
passport = response.json()
print(
{
"passport_version": passport["passport_version"],
"access_tier": passport["access_tier"],
"product_identity": passport["data"]["product_identity"],
}
)In the curl example, --fail-with-body returns a non-zero exit code when the
server answers an error, and still displays the body. The jq tool only serves
to read the JSON in the terminal, it takes no part in the call.
#Example response
HTTP code 200.
This product is still in the catalog, it has not been claimed by a customer, and
the passport is requested at the public tier. Its data describes no material and
declares no substance of concern, so the evidence block rates the unit only.
{
"id": 1,
"product_id": 1,
"brand_code": "K7M2QF9XBN",
"brand_id": 42,
"schema_version": "1.0",
"passport_version": 3,
"data": {
"product_identity": {
"gtin": "03701234567891",
"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
}
},
"data_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"ipfs_uri": null,
"ipfs_gateway_url": null,
"visibility": "public",
"access_tier": "public",
"is_owner": false,
"identity_included": true,
"published_at": "2026-08-01T09:00:00+00:00",
"product_name": "Cartable Exemple 32",
"brand_name": "Exemple SAS",
"image_url": "https://example-sas.test/images/cartable-32.jpg",
"gtin": "03701234567891",
"gs1_digital_link": "https://id.gs1.org/01/03701234567891/21/EXEMP1E00001",
"passport_service_provider": null,
"warranty": {
"status": "active",
"ends_at": "2028-08-01T09:00:00+00:00",
"duration_months": 24,
"transferable": true,
"remaining_days": 730
},
"evidence": {
"sections": {
"identity": "verified",
"integrity": "declared"
},
"legend": {
"declared": "Stated by the brand. Recorded, dated and attributable, but not independently checked.",
"verified": "Rests on an on-chain reference that anyone can re-check independently. SealTrust records it and does not re-query the chain on each request."
},
"derived": true,
"note": "Statuses are computed by SealTrust from verifiable facts. A brand cannot set them."
}
}The domain of the gs1_digital_link field is that of the resolver configured for
your integration. https://id.gs1.org is only the fallback value, used when no
resolver is configured. Do not hard-code this domain, read the value returned.
#The fields of the response
| Field | Type | Description |
|---|---|---|
id | integer | The identifier of the passport version served. |
product_id | integer | The unit to which this passport is attached, or null when the passport covers the batch or the model and applies to all of their units. A unit with no passport of its own receives the one of its production batch, failing that the one of its model. |
brand_code | string | The public identifier of the brand, ten characters. This is the one to quote and to keep. |
brand_id | integer | The internal number of the brand. ⚠️ Kept so existing integrations do not break; prefer brand_code. |
schema_version | string | The version of the passport's data schema. |
passport_version | integer | The published version number. It goes up with each new publication. |
data | object | The passport's data, filtered according to the tier served. Its shape depends on the product category. |
data_hash | string or null | The hash of this version's complete data, 64 hexadecimal characters. null when no hash has been recorded for this version. |
ipfs_uri | string | The IPFS address of the passport's copy. See the note below: filled in at the public and end_user tiers for passports published since 14 July 2026, null for older ones. |
ipfs_gateway_url | string | The HTTP address that copy reads from. Follows exactly the same rule as ipfs_uri. |
visibility | string | The visibility of the version served: public, or owner_only when the current owner is authenticated. The brand_only visibility is never served here. |
access_tier | string | The tier actually served, which may differ from the tier requested for the product's owner. |
is_owner | boolean | true when the call is authenticated and the account is the unit's current owner. |
identity_included | boolean | true when the unit has its identity: an owner, a chip, the claim and the transfer. A unit created on the Compliance + Identity offer keeps it, even if its brand later moves to the Compliance offer. false for a unit without identity. null when the answer does not say, which never means a refusal. |
available_tiers | array of string | The tiers YOUR call may ask for on this passport, in the order public, end_user, repairer, recycler, upstream, authority: ["public"] without authentication, end_user added for a signed-in account, the trade the brand accredited for a partner, all of them for an authority. The list grants nothing: every tier asked is still checked. A response carrying it for a signed-in account must go to no shared cache. |
published_at | string | Date and time this version was published, in ISO 8601 format, or null. |
product_name | string or null | The product's name. null when no name has been recorded on the item. |
brand_name | string | The brand's name, or null if the product is attached to none. |
image_url | string | null | The full address of the photograph of the piece's model: its main photo, or else the first image of its media library. Computed as on the passport of the model and of the lot. null when the model has none, or when it belongs to a brand other than this passport's. It is the light copy of the photo (WebP, 800 pixels at most on the long side, 100 KB at most, no EXIF data) when it exists, else the original. An address of another site is returned as given. |
gtin | string | The model's GTIN, brought back to 14 digits. null when the model carries none, or when the recorded value is not a valid GTIN. |
gs1_digital_link | string | The GS1 Digital Link that identifies this unit, of the form <resolution domain>/01/<14-digit gtin>/21/<serial number>. null when the GTIN or the serial number is missing. |
passport_service_provider | object or null | The passport service provider keeping the back-up copy of this version (Regulation (EU) 2024/1781, article 10(4) and Annex III point (l)): identifier, its reference, name, its legal name, and backed_up_at, the date of the copy in ISO 8601 format. null until the copy of this version exists, or when its last check failed. Outside data, so outside the data_hash fingerprint. |
warranty | object | The warranty summary, or null when the product has none. See below. |
evidence | object | On what basis each section can be believed. See below. Absent if its computation fails. |
lifecycle | object | Present only when the unit is destroyed or out of the catalog. See below. |
integrity | object | Present only when verify_integrity=true and the IPFS link is served at your tier. See below. |
#The warranty block
| Field | Type | Description |
|---|---|---|
status | string | active, expiring_soon, expired or void. Recomputed on each read. |
ends_at | string | End date, in ISO 8601 format, or null for a lifetime warranty. |
duration_months | integer | The announced duration, in months. |
transferable | boolean | true when the warranty follows the product on a change of owner. |
remaining_days | integer | Whole days remaining. Negative when the warranty has passed. null for a lifetime or voided warranty. |
#The evidence block
Three values exist, and they are computed by SealTrust. A brand cannot choose them.
| Value | What it says |
|---|---|
verified | Rests on an on-chain reference that anyone can re-check independently. SealTrust records it and does not re-query the chain on each request. |
document_backed | A third-party document is attached and can be retrieved. Its content has not been audited by SealTrust. |
declared | Declared by the brand. Recorded, dated, attributable, not independently verified. |
Four sections can carry one of these values: identity, integrity,
composition and substances_of_concern. Only the sections the passport
actually contains receive one.
identity and integrity are always present, because they bear on the unit
itself. The identity section goes to verified when the unit carries a token
identifier on the chain. The integrity section goes to verified when this
version's hash has been anchored and still matches the recorded data.
composition and substances_of_concern appear only when the passport data
carries them. A passport that describes no material receives no composition
status, and sections then holds two entries. A materials block left empty (rows with no name, no share and no origin) describes no material. The number of entries therefore
ranges from two to four depending on the passport served.
The block additionally carries legend, which restates the meaning of the
values present, derived at true, and note.
Display a status only for the sections that sections names. An absent section
is neither verified nor disproved, it is simply not rated.
#The lifecycle block
It appears only if the unit is destroyed or out of the catalog. Its passport is still served so that the identifier keeps resolving.
{
"lifecycle": {
"status": "superseded",
"is_burned": false,
"superseded": true,
"note": "This unit is superseded or withdrawn; its passport is retained so the identifier stays resolvable (EN 18219 §4.2.2 persistence)."
}
}Read is_burned before status.
The status field is superseded when the unit has been replaced by another,
and archived when it was withdrawn without replacement. The superseded field
is true only for the first case.
The block also appears when the unit has been destroyed, that is, when
is_burned is true. In that case status carries the product's current
state, which may be null or an active value. So never infer destruction from
the value of status.
#The integrity block
{
"integrity": {
"ipfs_fetched": true,
"ipfs_hash": "1111111111111111111111111111111111111111111111111111111111111111",
"expected_hash": "1111111111111111111111111111111111111111111111111111111111111111",
"match": true
}
}| Field | Type | Description |
|---|---|---|
ipfs_fetched | boolean | true when the server managed to read the IPFS copy. |
ipfs_hash | string | The hash of the content actually read on IPFS. |
expected_hash | string | The hash expected for this content. |
match | boolean | The verdict of the comparison. |
expected_hash is the hash of the PUBLIC projection of the passport, the one
deposited on IPFS. It differs from data_hash, which covers the complete data,
including the fields reserved for the professional tiers. The two values coincide
only when the passport carries no non-public field. So never compare
expected_hash and data_hash.
The match field is true when the IPFS copy matches, false when it differs,
and null when the copy could not be retrieved. In that last case
ipfs_fetched is false, an error field replaces the two hashes, and null
means that nothing could be concluded.
#The JSON-LD response
With format=jsonld, the response carries the content type
application/ld+json. It is a Schema.org and GS1 document whose fields are
filtered by the same access tier. It contains neither passport_version, nor
data_hash, nor the warranty, evidence, lifecycle and integrity blocks
described above: the warranty is expressed there as WarrantyPromise, and the
other blocks do not appear in it. The back-up copy provider appears under
espr:passportServiceProvider (identifier, name, espr:backedUpAt), on
the same terms as passport_service_provider, and is absent until the copy
exists.
Two other differences matter for your integration.
The document carries no access_tier field. The server does not return the
X-DPP-Access-Tier header either. To know the tier actually served, call
without format, or stick to the tier you requested.
The server ignores verify_integrity in this format. It returns the JSON-LD
document before computing the integrity block, so this parameter changes
nothing in the response and no error signals it to you.
#Errors
The body of an error response carries a detail field.
| Code | Condition | What to do |
|---|---|---|
| 401 | access_tier=end_user is requested with no valid session. detail is End-user-tier access requires authentication. | Sign in with any account, or request the public tier. |
| 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 tier. |
| 403 | access_tier=authority is requested by a signed-in account that does not carry that role. detail is Authority-tier access is restricted to market surveillance authorities. | Request the tier that matches your authorization. |
| 403 | A professional tier is requested by a signed-in account that is neither an administrator of the product's brand nor the holder of 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 trade, then request the tier of that trade. |
| 404 | No product matches the identifier, under any of the three accepted forms. detail is Product not found. | Check the number you copied. A destroyed product or one withdrawn from the catalog is still resolved here, so this response does mean that the identifier is unknown. |
| 404 | The product exists, but no published passport version matches it. detail is No published passport found for this product. | The brand must publish a version. An unpublished draft is never served, and neither is a version with brand_only visibility. |
| 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. |
| 422 | A parameter value is refused: an access_tier that is not one of the six values, or a verify_integrity that is not a boolean. detail is a list, each entry carrying loc, type and msg. | Read loc to find out which parameter is at fault, then fix its value. |
| 429 | More than 600 calls have been made from your IP address to a /passport read within the current 60-second window. detail is Rate limit exceeded: 600 requests per 60s. | Wait the number of seconds indicated by the Retry-After header, then try again. |
| 500 | An unexpected error occurred while processing your call. detail is Internal Server Error. | Try again. If the error persists, contact support giving the time of the call and the value of the response's X-Request-Id header. |
#Why the IPFS copy is not always given at the public tier
An IPFS copy is readable by anyone who knows its content identifier. Until 14 July 2026 we pinned the full passport there, restricted fields included. Handing the address to a public reader would have bypassed the access tiers, so it was hidden from everyone except the authenticated tiers.
Since that date we pin only the public projection of the passport: the copy no longer holds anything restricted, and its address is given to everyone. Passports published before that date keep a full-data copy, for ever, because a content identifier cannot be withdrawn. Their address therefore stays restricted to the authenticated tiers.
Measured in production on 12 September 2026: of sixteen passports carrying an IPFS copy, five predate that change.
#See also
GET /passport/01/{gtin}, read a model's published passport, from its GTIN.GET /passport/{identifier}/verify, check the integrity of an item's published passport.GET /passport/{identifier}/proof, gather the public proofs of an item's passport.GET /passport/{identifier}/vc, retrieve the passport's signed credential, in SD-JWT-VC format.- Publishing 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.