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

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

---

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:

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

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

> [!INFO] The check applies to the filtered presentation
> We first reduce the credential to the access tier you request, then we check
> its signature. This endpoint can therefore never show more fields than an
> ordinary read of the passport at the same tier.

## Authorization

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.

> [!INFO] This call consumes no quota
> The daily quota of an API key is not touched by this call, and neither is the
> monthly product quota of your plan. This endpoint queries neither one.

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

> [!INFO] The TypeScript SDK does not cover this endpoint
> No method of the `@sealtrust-io/sdk` package in version 0.3.0 calls this URL.
> The TypeScript example below uses `fetch`, without a dependency, and runs
> under Node. The MCP server, for its part, exposes this check under the tool
> name `verify_credential`.

:::onglets
```bash title="curl"
curl -i "https://api.sealtrust.io/v1/passport/EXEMPLE00001/vc/verify?access_tier=public"
```
```typescript
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());
}
```
```python
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`.

```json
{
  "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.

```json
{
  "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`. |

> [!ATTENTION] No error code signals an invalid signature
> A credential whose signature does not hold answers 200 with `verified` set to
> `false`. A process that merely looks at the HTTP code would therefore accept
> a passport whose signature is worth nothing.

## See also

- [`GET /passport/{identifier}/vc`](/en/reference/get-passport-vc/),
  retrieve the signed credential of the passport, in the SD-JWT-VC format.
- [`GET /passport/{identifier}/vc/preview`](/en/reference/get-passport-vc-preview/),
  see, without a signature, what an access tier would expose.
- [`GET /brand/{brand_id}/did.json`](/en/reference/get-brand-did-json/),
  retrieve the public signing keys of a brand.
- [`GET /.well-known/did.json`](/en/reference/get-well-known-did-json/),
  serve the identity document of a brand on its own domain.
