# GET /partner-portal/products/{identifier}

Find a product of a brand that has accredited you, from what is written on the object, and read its passport filtered on the only accreditations that brand has recognized for you.

Source: https://docs.sealtrust.io/en/reference/get-partner-portal-products/

---

You find a product from the identifier read on the object, and you receive its
passport filtered on the accreditations that brand has recognized for you, and
on those alone. By the end of this page, you will know which identifiers this
endpoint accepts, what the response contains, and which errors it returns.

This endpoint belongs to the partner portal. It is reserved for repairer and
recycler accounts, and it authenticates with the account session. The brand's
API key has no currency here.

Full address:

```http
GET https://api.sealtrust.io/v1/partner-portal/products/{identifier}
```

The same endpoint also answers without the `/v1` prefix, at
`https://api.sealtrust.io/partner-portal/products/{identifier}`. Both addresses
call the same code. Use the `/v1` form for a new integration.

## Authorization

Session of a partner account. You open that session with
`POST /v1/auth/login`, which returns an access token and also sets a session
cookie. You then present the token in the `Authorization` header, in `Bearer`
format. It is valid for 60 minutes.

You must meet three conditions, in this order.

1. Your session is valid and your account is active. Otherwise the response is
   401.
2. Your account is of the repairer or recycler type. An account of another type
   receives 403.
3. At least one brand has accredited you, and that accreditation is active.
   Without that, the response is 403 before any product search at all.

The API searches only among the brands that have accredited you. It never
returns a product of another brand to you.

> [!ATTENTION] An API key does not work on this path
> API keys serve the `/v1/partner/*` endpoints, which are a different surface.
> If you present an API key here, the API answers 401.

The portal is intended for the partner application. The API also accepts a
server to server call that carries the token in the `Authorization` header.

The API refuses with a 403 a call that relies on the session cookie and that
comes from an origin the API does not accept.

## Call limit

A call limit applies to this endpoint. It is set for the normal use of the
portal, where you look up a product then record an intervention.

Beyond it, the API answers 429. The refusal carries a `Retry-After` header that
gives the number of seconds to wait. Wait that delay, then call again.

The limit covers the whole partner portal. Alternating between endpoints
therefore does not give you headroom back. Space out your calls instead of
sending them in bursts.

The value of the limit is not a commitment and can change without notice. Do not
write any threshold into your code, rely on `Retry-After`.

> [!INFO] This call counts against no quota
> This call does not eat into the brand's monthly product quota. No API key
> quota comes into play either, since this endpoint does not use an API key.

## Path and query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | yes | What identifies the product. The API accepts four forms, see below. |

This endpoint has no query parameter.

The API tries the four forms in this order.

| Form | What it looks like | Note |
| --- | --- | --- |
| Physical identifier hash | starts with `0x` | Case is ignored. |
| Token number | digits only | The number assigned to the product on the chain. |
| Certificate number | as it appears on the certificate | Compared identically, with no case tolerance. |
| Serial number | 12 characters, the one printed on the object | See the input tolerance below. |

The serial number is the only form a human has in front of them. It is written
in an alphabet that excludes the characters the eye confuses. On reading, the
API puts the input in uppercase, then it brings `I` and `L` onto `1`, and `O`
onto `0`. So you can enter the letter `I`, the API reads it as the digit `1`. An
operator copying a label is not punished for a character confusion. The API
treats as a serial number only a string of exactly 12 characters, all taken from
that alphabet.

The API does not resolve a destroyed product, nor a product the brand has
withdrawn from the catalog. The response is then 404.

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | no | `Bearer` followed by the session token returned by the login. Necessary if your call does not carry the session cookie. |

## Request body

None. This request has no body.

## Example request

Search for the product whose printed serial number is `EXEMPLE00001`. Replace
`VOTRE_JETON_DE_SESSION` with the access token the login returned to you.

The TypeScript SDK does not cover this surface, so the TypeScript tab shows a
direct `fetch` call.

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/partner-portal/products/EXEMPLE00001 \
  -H "Authorization: Bearer VOTRE_JETON_DE_SESSION"
```
```typescript
const reponse = await fetch(
  "https://api.sealtrust.io/v1/partner-portal/products/EXEMPLE00001",
  {
    method: "GET",
    headers: {
      Authorization: "Bearer VOTRE_JETON_DE_SESSION",
    },
  },
);

console.log(reponse.status);
console.log(await reponse.json());
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/partner-portal/products/EXEMPLE00001",
    headers={
        "Authorization": "Bearer VOTRE_JETON_DE_SESSION",
    },
    timeout=30,
)

print(response.status_code)
print(response.json())
```
:::

> [!INFO] The TypeScript SDK does not cover the partner portal
> `@sealtrust-io/sdk` authenticates with an API key and serves the
> `/v1/partner/*` endpoints. Call this path with `fetch`, as above.

## Example response

HTTP code `200`.

```json
{
  "product": {
    "product_id": 4821,
    "token_id": "1029384756",
    "uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "product_name": "Sac Modèle A",
    "brand_id": 12,
    "brand_name": "Exemple SAS",
    "category_name": "Maroquinerie",
    "status": "written"
  },
  "passport": {
    "available": true,
    "access_tier": "recycler",
    "passport_version": 3,
    "data": {
      "product_identity": {
        "gtin": "03701234567890",
        "model": "Modèle A",
        "brand": "Exemple SAS",
        "made_in": "FR"
      },
      "materials": {
        "primary": { "name": "Cuir pleine fleur", "percentage": 70, "origin": "IT" },
        "certified_organic": false
      },
      "circularity": {
        "repairability_index": 7.8,
        "expected_lifetime_years": 15,
        "disassembly_instructions_url": "https://exemple-sas.test/demontage/modele-a"
      },
      "compliance": {
        "eu_espr": true,
        "reach": true
      }
    }
  },
  "allowed_event_types": [
    "after_sale_service",
    "maintenance",
    "reconditioning",
    "repair"
  ]
}
```

The response has three top level fields.

| Field | Type | Description |
| --- | --- | --- |
| `product` | `object` | The product found. Eight fields, see below. |
| `passport` | `object` | The passport, filtered for your access level. Four fields, see below. |
| `allowed_event_types` | `string[]` | The intervention types your accreditations on this brand allow you to record. List sorted in alphabetical order. |

### The `product` block

| Field | Type | Description |
| --- | --- | --- |
| `product_id` | `integer` | The number of the product. It is what ties your interventions to this object. |
| `token_id` | `string` or `null` | The number of the token on the chain. Is `null` as long as the mint is not confirmed. |
| `uid_hash` | `string` or `null` | The hash of the physical identifier. |
| `product_name` | `string` or `null` | The name of the product. |
| `brand_id` | `integer` or `null` | The number of the owning brand. |
| `brand_name` | `string` or `null` | The name of the brand. Is `null` if the brand is no longer readable. |
| `category_name` | `string` or `null` | The name of the category. Is `null` if the product has no category. |
| `status` | `string` or `null` | The state of the product. See the list below. |

`status` takes one of these values: `draft`, `minting`, `mined`, `written`,
`burn_submitted`, `burned`, `superseded`, `archived`, `stolen`, `revoked`. The
`superseded` and `archived` states take the product out of the catalog. This
endpoint never returns a product in either of those two states, nor a destroyed
product.

> [!ATTENTION] You only see your own trade
> The passport this portal returns to you is filtered on the accreditations the
> brand has actually recognized for you, and on those alone. An accredited
> recycler sees the materials and the substances of concern; it does not see the
> manufacturing data nor the upstream provenance, which belong to another trade.
> Holding one accreditation opens none of the others.

### The `passport` block

| Field | Type | Description |
| --- | --- | --- |
| `available` | `boolean` | `true` when this product has a published passport. |
| `access_tier` | `string` | The trades actually served, joined by a `+` and ordered alphabetically: `recycler`, or `repairer+recycler` for a partner holding both accreditations on this brand. Is `public` when the brand has recognized none of them for you, and the passport is then filtered at the public level. |
| `passport_version` | `integer` or `null` | The version number of the passport returned. Is `null` when there is no passport. |
| `data` | `object` or `null` | The content of the passport, filtered. Is `null` when there is no passport. |

When the API finds no passport, the block is
`{"available": false, "access_tier": "public", "passport_version": null, "data": null}`.
The API returns the rest of the response normally. You can record an
intervention on a product with no passport.

Three rules decide the passport returned.

- The API only returns a published passport, in public visibility or in
  visibility reserved for the owner. It does not return a draft, nor a passport
  the brand keeps for its internal use.
- The API first looks for the passport belonging to the item you resolved.
  Failing that, and if the product is attached to a model, it returns the
  reference passport of that model. An item with no passport of its own is
  therefore described by the reference of its model. The per unit data you read
  always concerns the item you resolved.
- When several versions exist, the API returns the highest one.

> [!ATTENTION] `data` does not contain the whole passport
> The API filters the content on your accreditations, then according to the
> rules the brand has defined. Two brands can therefore open different fields to
> you for a comparable product. The example above is a leather goods passport.
> The shape of the content depends on the published passport.

### The `allowed_event_types` block

These values are exactly the ones that
`POST /v1/partner-portal/interventions` accepts for this product. That endpoint
refuses with a 422 any type absent from this list.

| Accreditation type | Intervention types returned |
| --- | --- |
| Repairer | `after_sale_service`, `maintenance`, `reconditioning`, `repair` |
| Recycler | `destruction`, `end_of_life`, `recycling`, `return` |

An account holding both active accreditations on the same brand receives the
eight values. The API computes this list brand by brand. The same account can
therefore receive a different list for a product of another brand.

## Errors

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

| Code | Condition | What to do |
| --- | --- | --- |
| 401 | You present no session: no `Authorization` header, no session cookie. `detail` is `Not authenticated`. The response also carries `WWW-Authenticate: Bearer`. | Log in, then present the token returned. |
| 401 | The token is unreadable, badly signed or expired. `detail` is `Invalid JWT token`. | Renew your session. An access token is valid for 60 minutes. |
| 401 | The token you present is not a session token. `detail` is `Invalid token`. | Use the access token returned by the login. |
| 401 | The token you present is the one from a login stopped at the two step verification stage. `detail` is `MFA verification required`. | Finish the two step verification, then use the token returned at the end. |
| 401 | A logout or a password change has revoked this token. `detail` is `Token has been revoked`. | Log in again. |
| 401 | The token carries no account address. `detail` is `Invalid token: missing email`. | Log in again. |
| 401 | Your account is no longer active. `detail` is `Account disabled`. | Contact the brand that accredited you. |
| 403 | Your call carries the session cookie, without announcing either `Origin` or `Referer`. `detail` is `Origin or Referer header required`. | Call from the partner application, or present the token in the `Authorization` header instead of the cookie. |
| 403 | Your call carries no token in the `Authorization` header, and it announces an origin the API does not accept. `detail` is `Forbidden origin`. | Call from the partner application, or from your server presenting the token in the `Authorization` header. |
| 403 | Your account is neither repairer nor recycler. `detail` is `Partner account required (repairer or recycler)`. | This path is not addressed to this type of account. |
| 403 | No brand has accredited you, or your accreditations are no longer active. `detail` is `Aucune accréditation active`. | Ask the brand to reactivate your accreditation. |
| 403 | Your accreditations do not allow you to act on this product. | Ask the brand concerned for an accreditation. |
| 404 | This identifier designates no product your accreditations let you reach. `detail` is `Produit introuvable`. | Check the input. A destroyed product or one withdrawn from the catalog gives the same response. |
| 404 | The account tied to the token no longer exists. `detail` is `User not found`. | Log in again. If the error persists, contact the brand that accredited you. |
| 429 | You have gone over the call limit. The response carries a `Retry-After` header. | Wait the number of seconds indicated by `Retry-After`, then try again. Spread out your calls instead of sending them in bursts. |
| 500 | An unexpected error occurred while handling your call. `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

- [`POST /partner-portal/interventions`](/en/reference/post-partner-portal-interventions/),
  record an intervention on a product.
- [`GET /partner-portal/interventions`](/en/reference/get-partner-portal-interventions/),
  list the interventions your account has recorded.
- [`GET /partner-portal/me`](/en/reference/get-partner-portal-me/),
  read your partner profile and the brands that have accredited you.
