# 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 and end_user tiers.

Source: https://docs.sealtrust.io/en/reference/get-passport-vc/

---

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:

```http
GET https://api.sealtrust.io/v1/passport/{identifier}/vc
```

The 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}`.

> [!INFO] What SD-JWT-VC means
> It is a selective disclosure verifiable credential format. The brand signs the
> document once, at issuance, with all of the fields. Each non-public field
> occupies a separate segment in it, called a disclosure. The server then
> chooses which segments it attaches to the response. The fields that do not
> concern you stay out of the response, and the signature of the document stays
> valid.

## Authorization

None for the `public` and `end_user` tiers. This endpoint is public at those two
tiers.

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.

Four 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 |
| `end_user` | nothing |
| `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 with access to the brand of the product, or the authority role. No accreditation opens this tier. |
| `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 the products of that brand.

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

60 calls per 60-second slice, counted per calling network address. The window is
fixed.

That counter is shared by every path that 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 `60` |
| `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. 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 |

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`.

> [!INFO] The TypeScript SDK does not cover this endpoint
> `@sealtrust-io/sdk` exposes no method for this address. The TypeScript example
> below uses `fetch`, available with no dependency. Run it server-side, under
> Node. From a browser, the origin check described above refuses the call with
> 403.

:::onglets
```bash title="curl"
curl -s "https://api.sealtrust.io/v1/passport/EXEMP1E00001/vc?access_tier=public"
```
```typescript
const identifiant = "EXEMP1E00001";

const url = new URL(
  `https://api.sealtrust.io/v1/passport/${encodeURIComponent(identifiant)}/vc`,
);
url.searchParams.set("access_tier", "public");

const response = await fetch(url, { method: "GET" });

if (response.status === 403) {
  throw new Error(
    "Appel refusé : exécutez cette requête depuis votre serveur, jamais depuis un navigateur.",
  );
}
if (!response.ok) {
  throw new Error(`SealTrust a répondu ${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("Jeton signé :", segments[0]);
console.log("Segments de divulgation :", segments.slice(1, -1).length);
```
```python
import requests
from urllib.parse import quote

identifiant = "EXEMP1E00001"

response = requests.get(
    f"https://api.sealtrust.io/v1/passport/{quote(identifiant, safe='')}/vc",
    params={"access_tier": "public"},
    timeout=30,
)

if response.status_code == 403:
    raise SystemExit(
        "Appel refusé : exécutez cette requête depuis votre serveur, jamais depuis un navigateur."
    )
if not response.ok:
    raise SystemExit(f"SealTrust a répondu {response.status_code}")

justificatif = response.json()

print(justificatif["issuer"], justificatif["access_tier"])
print(justificatif["format"], justificatif["vct"])

segments = justificatif["sd_jwt_vc"].split("~")
print("Jeton signé :", 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.

```json
{
  "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` | `no-store, max-age=0`, whatever the tier served. Do not put this response behind any shared 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 `~`.

```text
<jeton signé>~<divulgation>~<divulgation>~
```

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.

```text
WyJFWEVNUExFMDAwMDAwMDAwMDAwMDAwMCIsInJlcGFpcmFiaWxpdHlfaW5kZXgiLDguMl0
```

That 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.

> [!ATTENTION] The document is signed once, the response is filtered at each call
> The signature covers the token, it does not cover the list of the disclosures
> attached. A response that is missing segments stays verifiable. Do not
> conclude from an absent field that it does not exist in the passport, conclude
> that it is not open to you at this tier.

### 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`.

## Errors

The body of an error response carries a `detail` field.

| Code | Condition | What to do |
| --- | --- | --- |
| 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 access to 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. |
| 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 60 calls per 60 seconds is reached for your network address, across every `/passport` path. `detail` is `Rate limit exceeded: 60 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`](/en/reference/get-passport-vc-preview/),
  see, without a signature, what an access tier would expose.
- [`GET /passport/{identifier}/vc/verify`](/en/reference/get-passport-vc-verify/),
  check the signature of the credential and read the data revealed.
- [`GET /brand/{brand_id}/did.json`](/en/reference/get-brand-did-json/),
  retrieve the public signing keys of a brand.
- [Publish a Digital Product Passport](/en/passeport-dpp/),
  publish, choose who sees which fields, export and have it verified.
