# GET /timeline/{identifier}

Read the public history of a product: its verifications and its changes of ownership, from the printed number, the token identifier or the chip hash. Public endpoint.

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

---

You read the history of a single product. By the end of this page, you will know
how to retrieve the list of its verifications and its changes of ownership,
sorted from the most recent to the oldest, along with the record of the product
this history belongs to.

Full address:

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

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

## Authorization

None. This endpoint is public.

You do not need an API key. If you send one in the `Authorization` header, we
decode it as a console session token. An API key is not a session token: the
read fails silently, and we treat the call as an anonymous call. You therefore
receive the same response with or without an API key.

A console session token, on the other hand, changes the response. We read it
from the `Authorization` header as well as from the session cookie set by the
console. The brand administrator and the product's current owner then receive
the email addresses in the clear. The administrator additionally receives the
wallet addresses in `from_address` and `to_address`.

On an anonymous call, you receive an anonymized projection:

- we do not return the wallet addresses, `from_address` and `to_address` equal
  `null`;
- we mask the email addresses: the value returned keeps the first two characters
  of the local part, then `...`, then six stable characters, for example
  `ma...3f9c1d`.

> [!ATTENTION] This history is public
> Anyone holding the number printed on a product can call this endpoint and read
> this response. Do not rely on it to carry information you do not want
> published.

## Rate limit

30 calls per 60 second window, counted per calling IP address. The window is
fixed.

Calls on `/v1/timeline/…` and on `/timeline/…` feed the same counter. Switching
from one form to the other therefore does not raise the limit.

Going over returns 429, with a body that restates the limit. That response
carries no `Retry-After`. It does carry `x-ratelimit-limit`,
`x-ratelimit-remaining` and `x-ratelimit-reset`, whose values do not describe
this endpoint's limit.

The 200 responses carry these same three headers, with the same values. Do not
use these headers here to set your pace. On a 429, wait for the end of the
current window, that is, at most 60 seconds.

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

## Path and query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | yes | The product's identifier. Three forms are accepted, see below. |

This endpoint has no query parameter.

### The three accepted forms of identifier

This parameter accepts three forms. You send the one you have at hand.

| Form | What it looks like | Where you find it |
| --- | --- | --- |
| Serial number | 12 characters, digits and capital letters, in an alphabet that excludes I, L, O and U | Printed on the product, it is what its QR 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 |

We recognize the serial number whatever the case. We read the characters `I` and
`L` as a `1`, and the character `O` as a `0`, in order to accept a number copied
by hand from a label. We do not recover the `U`: it belongs neither to the
alphabet of serial numbers nor to the translated characters, and an identifier
that contains one designates no product.

### Headers

No header is required.

## Request body

None. This request has no body.

## Example request

Reading the history of the product whose printed number is `EXEMP1E00001`.

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/timeline/EXEMP1E00001
```
```typescript
import { SealTrustClient } from "@sealtrust-io/sdk";

// This endpoint is public and ignores the key, but the SDK client
// refuses to be constructed without it.
const sealtrust = new SealTrustClient({
  apiKey: "st_test_0000000000000000000000000000000000000000000000",
});

const historique = await sealtrust.verify.timeline("EXEMP1E00001");

console.log(historique.product_name, historique.timeline.length);
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/timeline/EXEMP1E00001",
    timeout=30,
)

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

## Example response

HTTP code `200`.

This product has been minted, it has not been claimed by a customer yet, and it
has been scanned once by QR.

```json
{
  "token_id": "7719472615821079694904732333912527190217998977709370935963838933860875309329",
  "product_name": "Sac modèle 1",
  "brand_id": 42,
  "brand_name": "Exemple SAS",
  "category_id": null,
  "category_name": "Maroquinerie",
  "metadata_uri": "ipfs://exemple-de-contenu-non-reel",
  "image_url": "https://exemple-sas.test/images/sac-modele-1.jpg",
  "current_owner_email": null,
  "current_owner_is_vault": true,
  "timeline": [
    {
      "type": "verify",
      "token_id": "7719472615821079694904732333912527190217998977709370935963838933860875309329",
      "uid_hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
      "contract_address": "0x2222222222222222222222222222222222222222",
      "timestamp": "2026-08-18T14:02:11.482000Z",
      "signature": "signature-exemple",
      "is_valid": true,
      "source": "qr",
      "from_address": null,
      "to_address": null,
      "from_email": null,
      "to_email": null,
      "from_display": null,
      "to_display": null,
      "comment": "Scanned",
      "event_id": null
    },
    {
      "type": "transfer",
      "token_id": "7719472615821079694904732333912527190217998977709370935963838933860875309329",
      "uid_hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
      "contract_address": "0x2222222222222222222222222222222222222222",
      "timestamp": "2026-08-12T09:30:00.000000Z",
      "signature": null,
      "is_valid": null,
      "source": null,
      "from_address": null,
      "to_address": null,
      "from_email": "Mint",
      "to_email": null,
      "from_display": "Mint",
      "to_display": "Vault",
      "comment": "Factory mint",
      "event_id": null
    }
  ],
  "contract_address": "0x2222222222222222222222222222222222222222"
}
```

The response counts twelve fields and nothing else. You receive `null` for the
fields without a value, they do not disappear from the response.

| Field | Type | Description |
| --- | --- | --- |
| `token_id` | `string` | The identifier of the token on the chain, returned as text. Equals the empty string when the product does not have a token identifier yet. |
| `product_name` | `string` | The product's name, or `null`. |
| `brand_id` | `integer` | The number of the brand the product belongs to, or `null` if the product is attached to none. |
| `brand_name` | `string` | The brand's name, or `null` if the product is attached to none. |
| `category_id` | `integer` | Always `null` on this endpoint. The field is declared in the shape of the response, and this endpoint never fills it. |
| `category_name` | `string` | The name of the product's category, or `null`. |
| `metadata_uri` | `string` | The address of the product's metadata, or `null`. |
| `image_url` | `string` | The address of the product's image, or `null`. |
| `current_owner_email` | `string` | The email address of the current owner, masked. `null` when no owner is known. |
| `current_owner_is_vault` | `boolean` | `true` when the product is still held by the brand's vault, and therefore not yet claimed by a customer. |
| `timeline` | `object[]` | The events, from the most recent to the oldest. See the table below. |
| `contract_address` | `string` | The address of the contract that carries this product on the chain, or `null`. |

The `timeline` array is not paginated and has no maximum size. It contains every
verification and every movement recorded for this product. A heavily scanned
product therefore returns a large response. Size your read accordingly.

### An event of the timeline

Each entry counts sixteen fields. The fields that have no meaning for the type of
event equal `null`.

| Field | Type | Description |
| --- | --- | --- |
| `type` | `string` | `verify` for a verification, `transfer` for a change of ownership. These are the only two values. |
| `token_id` | `string` | The identifier of the token concerned. |
| `uid_hash` | `string` | The chip hash concerned, `0x` followed by 64 hexadecimal characters. |
| `contract_address` | `string` | The address of the contract concerned. |
| `timestamp` | `string` | Date and time of the event, in universal time, in the format ISO 8601. |
| `signature` | `string` | The signature recorded with the verification. `null` on a change of ownership. |
| `is_valid` | `boolean` | The result of the verification. `null` on a change of ownership. |
| `source` | `string` | Where the verification comes from. The values written today are `qr`, `sdm-url` and `sdm-json`. `null` on a change of ownership. |
| `from_address` | `string` | Always `null` on a public call. |
| `to_address` | `string` | Always `null` on a public call. |
| `from_email` | `string` | The email address of the party giving up ownership, masked. Equals `Mint` when the event is the minting of the product. |
| `to_email` | `string` | The email address of the receiving party, masked. |
| `from_display` | `string` | A display-ready label for the party giving up ownership. `Mint` for the minting, `Vault` for the brand's vault, otherwise the masked email address or a truncated wallet address. |
| `to_display` | `string` | The same label, for the receiving party. |
| `comment` | `string` | A short sentence in English that sums up the event: `Scanned`, `Scanned by <masked email>`, `Transfer`, `Transferred from <masked email> to <masked email>` or `Factory mint`. |
| `event_id` | `integer` | Always `null` on this endpoint. The field is declared in the shape of the response, and this endpoint never fills it. |

> [!INFO] Minting appears as a change of ownership
> A product that has been minted and never transferred still carries an entry of
> type `transfer`, with `from_display` set to `Mint` and the comment
> `Factory mint`. It describes the product entering circulation.

> [!INFO] One and the same movement is counted only once
> You get one entry per change of ownership. When two records describe the same
> movement, at the same second and between the same parties, the response
> returns the one that carries a transaction reference verifiable on a chain
> explorer.

## Errors

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

| Code | Condition | What to do |
| --- | --- | --- |
| 400 | The identifier sent has the shape of none of the three accepted forms: it contains something other than digits and letters, or it exceeds 64 characters. `detail` equals `Invalid UID hash format (must be 0x + 64 hex characters)`. | Send a serial number, a token identifier or a chip hash. Check that no space or punctuation character is left over. |
| 400 | The identifier starts with `0x` and is 66 characters long, but it contains a character that is not hexadecimal. Same value of `detail`. | A chip hash accepts only the digits `0` to `9` and the letters `a` to `f`. |
| 404 | The identifier is well formed, but no product matches it, or no event has been recorded for it. `detail` equals `No events found for this UID`. | Check the number you copied. A product removed from the catalog answers with the other 404 message, described on the next row. A destroyed product queried by its printed number or by its token identifier answers here. |
| 404 | Events exist for this identifier, but no product still in the catalog is attached to it. `detail` equals `Product not found for this UID`. | The product has been removed from the catalog. Its history is no longer served. |
| 429 | You have made more than 30 calls from your IP address in the current 60 second window. `detail` equals `Rate limit exceeded: 30 requests per 60s`. | Wait for the end of the window, at most 60 seconds, then retry. The `x-ratelimit-*` headers of this response describe another counter, do not use them to compute your wait. |
| 500 | An unexpected error occurred while processing your call. `detail` equals `Internal Server Error`. | Retry. If the error persists, contact support, giving the time of the call. |

> [!ATTENTION] A destroyed product still answers on its chip hash
> A destroyed product queried by its printed number or by its token identifier
> answers 404. Queried by its chip hash, it still answers 200, with its full
> history. Do not use this endpoint to find out whether a product has been
> destroyed.

> [!INFO] An unknown identifier and a malformed identifier do not answer the same way
> A well formed but unknown identifier answers 404. The 400 code is reserved for
> the identifiers whose shape is recognized by none of the three rules above.
> The message of the 404 stays the same whatever the unknown identifier, so that
> it is not possible to guess which products exist by watching the responses.

## See also

- [`GET /resolve/{identifier}`](/en/reference/get-resolve/),
  read in one call everything a product page displays.
- [`GET /products/{uid}/public`](/en/reference/get-products-uid-public/),
  read the public information of a product.
- [`GET /p/{serial}`](/en/reference/get-p-serial/),
  translate the printed serial number into a consumer page address.
- [Core concepts](/en/notions/),
  tell model, batch and item apart before ordering a single label.
