# GET /timeline/{identifier}

Lire l'historique public d'un produit : ses vérifications et ses changements de propriétaire, à partir du numéro imprimé, de l'identifiant de jeton ou de l'empreinte de puce. Point d'entrée public.

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

---

Vous lisez l'historique d'un seul produit. En quittant cette page, vous saurez
récupérer la liste de ses vérifications et de ses changements de propriétaire,
triée du plus récent au plus ancien, ainsi que la fiche du produit auquel cet
historique appartient.

Adresse complète :

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

Le même point d'entrée répond aussi sans le préfixe `/v1`, à
`https://api.sealtrust.io/timeline/{identifier}`. Les deux adresses appellent le
même code. Utilisez la forme `/v1` pour une nouvelle intégration.

## Autorisation

Aucune. Ce point d'entrée est public.

Vous n'avez pas besoin de clef d'API. Si vous en envoyez une dans l'en-tête
`Authorization`, nous la décodons comme un jeton de session de la console. Une
clef d'API n'est pas un jeton de session : la lecture échoue en silence, et
nous traitons l'appel comme un appel anonyme. Vous recevez donc la même réponse
avec ou sans clef d'API.

Un jeton de session de la console change en revanche la réponse. Nous le lisons
dans l'en-tête `Authorization` comme dans le cookie de session posé par la
console. L'administrateur de la marque et le propriétaire actuel du produit
reçoivent alors les adresses e-mail en clair. L'administrateur reçoit en plus
les adresses de portefeuille dans `from_address` et `to_address`.

Sur un appel anonyme, vous recevez une projection anonymisée :

- nous ne rendons pas les adresses de portefeuille, `from_address` et
  `to_address` valent `null` ;
- nous masquons les adresses e-mail : la valeur rendue garde les deux premiers
  caractères de la partie locale, puis `...`, puis six caractères stables, par
  exemple `ma...3f9c1d`.

> [!ATTENTION] Cet historique est public
> N'importe qui possédant le numéro imprimé sur un produit peut appeler ce
> point d'entrée et lire cette réponse. Ne comptez pas dessus pour transporter
> une information que vous ne voulez pas voir publiée.

## Plafond d'appels

30 appels par tranche de 60 secondes, comptés par adresse IP appelante. La
fenêtre est fixe.

Les appels sur `/v1/timeline/…` et sur `/timeline/…` alimentent le même
compteur. Passer d'une forme à l'autre ne relève donc pas le plafond.

Un dépassement renvoie 429, avec un corps qui redit le plafond. Cette réponse
ne porte aucun `Retry-After`. Elle porte en revanche `x-ratelimit-limit`,
`x-ratelimit-remaining` et `x-ratelimit-reset`, dont les valeurs ne décrivent
pas le plafond de ce point d'entrée.

Les réponses 200 portent ces trois mêmes en-têtes, avec les mêmes valeurs.
N'utilisez pas ces en-têtes ici pour régler votre cadence. Sur un 429, attendez
la fin de la fenêtre en cours, soit au plus 60 secondes.

> [!INFO] Cet appel ne consomme aucun quota
> Le quota quotidien d'une clef d'API n'est pas entamé par cet appel, et le
> quota mensuel de produits de votre offre non plus. Ce point d'entrée
> n'interroge ni l'un ni l'autre.

## Paramètres de chemin et de requête

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | oui | L'identifiant du produit. Trois formes sont acceptées, voir ci-dessous. |

Ce point d'entrée n'a aucun paramètre de requête.

### Les trois formes d'identifiant acceptées

Ce paramètre accepte trois formes. Vous envoyez celle que vous avez sous la
main.

| Forme | À quoi elle ressemble | Où vous la trouvez |
| --- | --- | --- |
| Numéro de série | 12 caractères, chiffres et lettres majuscules, dans un alphabet qui exclut I, L, O et U | Imprimé sur le produit, c'est ce que porte son QR |
| Identifiant de jeton | Une suite de chiffres, souvent très longue | Rendu par nos réponses dans le champ `token_id` |
| Empreinte de puce | `0x` suivi de 64 caractères hexadécimaux | Rendue par nos réponses dans le champ `uid_hash` |

Nous reconnaissons le numéro de série quelle que soit la casse. Nous lisons les
caractères `I` et `L` comme un `1`, et le caractère `O` comme un `0`, pour
accepter un numéro recopié à la main depuis une étiquette. Nous ne rattrapons
pas le `U` : il ne fait partie ni de l'alphabet des numéros ni des caractères
traduits, et un identifiant qui en contient ne désigne aucun produit.

### En-têtes

Aucun en-tête n'est requis.

## Corps de la requête

Aucun. Cette requête n'a pas de corps.

## Requête d'exemple

Lecture de l'historique du produit dont le numéro imprimé est `EXEMP1E00001`.

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

// Ce point d'entrée est public et ignore la clef, mais le client du SDK
// refuse de se construire sans elle.
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())
```
:::

## Réponse d'exemple

Code HTTP `200`.

Ce produit a été frappé, il n'a pas encore été réclamé par un client, et il a
été scanné une fois par 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"
}
```

La réponse compte douze champs et rien d'autre. Vous recevez `null` pour les
champs sans valeur, ils ne disparaissent pas de la réponse.

| Champ | Type | Description |
| --- | --- | --- |
| `token_id` | `string` | L'identifiant du jeton sur la chaîne, rendu sous forme de texte. Vaut la chaîne vide quand le produit n'a pas encore d'identifiant de jeton. |
| `product_name` | `string` | Le nom du produit, ou `null`. |
| `brand_id` | `integer` | Le numéro de la marque à laquelle le produit appartient, ou `null` si le produit n'est rattaché à aucune. |
| `brand_name` | `string` | Le nom de la marque, ou `null` si le produit n'est rattaché à aucune. |
| `category_id` | `integer` | Toujours `null` sur ce point d'entrée. Le champ est déclaré dans la forme de la réponse, et ce point d'entrée ne le remplit jamais. |
| `category_name` | `string` | Le nom de la catégorie du produit, ou `null`. |
| `metadata_uri` | `string` | L'adresse des métadonnées du produit, ou `null`. |
| `image_url` | `string` | L'adresse de l'image du produit, ou `null`. |
| `current_owner_email` | `string` | L'adresse e-mail du propriétaire actuel, masquée. `null` quand aucun propriétaire n'est connu. |
| `current_owner_is_vault` | `boolean` | `true` quand le produit est encore détenu par le coffre de la marque, donc pas encore réclamé par un client. |
| `timeline` | `object[]` | Les événements, du plus récent au plus ancien. Voir le tableau ci-dessous. |
| `contract_address` | `string` | L'adresse du contrat qui porte ce produit sur la chaîne, ou `null`. |

Le tableau `timeline` n'est pas paginé et n'a pas de taille maximale. Il
contient toutes les vérifications et tous les mouvements enregistrés pour ce
produit. Un produit très scanné rend donc une réponse volumineuse.
Dimensionnez votre lecture en conséquence.

### Un événement de la chronologie

Chaque entrée compte seize champs. Les champs qui n'ont pas de sens pour le
type d'événement valent `null`.

| Champ | Type | Description |
| --- | --- | --- |
| `type` | `string` | `verify` pour une vérification, `transfer` pour un mouvement de propriété. Ce sont les deux seules valeurs. |
| `token_id` | `string` | L'identifiant du jeton concerné. |
| `uid_hash` | `string` | L'empreinte de puce concernée, `0x` suivi de 64 caractères hexadécimaux. |
| `contract_address` | `string` | L'adresse du contrat concerné. |
| `timestamp` | `string` | Date et heure de l'événement, en temps universel, au format ISO 8601. |
| `signature` | `string` | La signature enregistrée avec la vérification. `null` sur un mouvement de propriété. |
| `is_valid` | `boolean` | Le résultat de la vérification. `null` sur un mouvement de propriété. |
| `source` | `string` | D'où vient la vérification. Les valeurs écrites aujourd'hui sont `qr`, `sdm-url` et `sdm-json`. `null` sur un mouvement de propriété. |
| `from_address` | `string` | Toujours `null` sur un appel public. |
| `to_address` | `string` | Toujours `null` sur un appel public. |
| `from_email` | `string` | L'adresse e-mail de la partie qui cède, masquée. Vaut `Mint` quand l'événement est la frappe du produit. |
| `to_email` | `string` | L'adresse e-mail de la partie qui reçoit, masquée. |
| `from_display` | `string` | Un libellé prêt à afficher pour la partie qui cède. `Mint` pour la frappe, `Vault` pour le coffre de la marque, sinon l'adresse e-mail masquée ou une adresse de portefeuille tronquée. |
| `to_display` | `string` | Le même libellé, pour la partie qui reçoit. |
| `comment` | `string` | Une phrase courte en anglais qui résume l'événement : `Scanned`, `Scanned by <e-mail masqué>`, `Transfer`, `Transferred from <e-mail masqué> to <e-mail masqué>` ou `Factory mint`. |
| `event_id` | `integer` | Toujours `null` sur ce point d'entrée. Le champ est déclaré dans la forme de la réponse, et ce point d'entrée ne le remplit jamais. |

> [!INFO] La frappe apparaît comme un mouvement de propriété
> Un produit frappé et jamais transféré porte quand même une entrée de type
> `transfer`, avec `from_display` à `Mint` et le commentaire `Factory mint`.
> Elle décrit la mise en circulation du produit.

> [!INFO] Un même mouvement n'est compté qu'une fois
> Vous obtenez une entrée par mouvement de propriété. Quand deux
> enregistrements décrivent le même mouvement, à la même seconde et entre les
> mêmes parties, la réponse rend celui qui porte une référence d'opération
> vérifiable sur un explorateur de chaîne.

## Erreurs

Le corps d'une réponse d'erreur porte un champ `detail`.

| Code | Condition | Que faire |
| --- | --- | --- |
| 400 | L'identifiant envoyé n'a la forme d'aucune des trois formes acceptées : il contient autre chose que des chiffres et des lettres, ou il dépasse 64 caractères. `detail` vaut `Invalid UID hash format (must be 0x + 64 hex characters)`. | Envoyez un numéro de série, un identifiant de jeton ou une empreinte de puce. Vérifiez qu'aucun espace ni caractère de ponctuation ne traîne. |
| 400 | L'identifiant commence par `0x` et fait 66 caractères, mais il contient un caractère qui n'est pas hexadécimal. Même valeur de `detail`. | Une empreinte de puce n'accepte que les chiffres `0` à `9` et les lettres `a` à `f`. |
| 404 | L'identifiant est bien formé, mais aucun produit ne lui correspond, ou aucun événement n'a été enregistré pour lui. `detail` vaut `No events found for this UID`. | Vérifiez le numéro recopié. Un produit retiré du catalogue répond avec l'autre message 404, décrit à la ligne suivante. Un produit détruit interrogé par son numéro imprimé ou par son identifiant de jeton répond ici. |
| 404 | Des événements existent pour cet identifiant, mais aucun produit encore en catalogue ne s'y rattache. `detail` vaut `Product not found for this UID`. | Le produit a été retiré du catalogue. Son historique n'est plus servi. |
| 429 | Vous avez fait plus de 30 appels depuis votre adresse IP dans la fenêtre de 60 secondes en cours. `detail` vaut `Rate limit exceeded: 30 requests per 60s`. | Attendez la fin de la fenêtre, au plus 60 secondes, puis réessayez. Les en-têtes `x-ratelimit-*` de cette réponse décrivent un autre compteur, ne vous en servez pas pour calculer votre attente. |
| 500 | Une erreur inattendue s'est produite pendant le traitement de votre appel. `detail` vaut `Internal Server Error`. | Réessayez. Si l'erreur persiste, contactez le support en indiquant l'heure de l'appel. |

> [!ATTENTION] Un produit détruit répond encore sur son empreinte de puce
> Un produit détruit interrogé par son numéro imprimé ou par son identifiant de
> jeton répond 404. Interrogé par son empreinte de puce, il répond encore 200,
> avec son historique complet. N'utilisez pas ce point d'entrée pour savoir si
> un produit a été détruit.

> [!INFO] Un identifiant inconnu et un identifiant mal formé ne répondent pas pareil
> Un identifiant bien formé mais inconnu répond 404. Le code 400 est réservé
> aux identifiants dont la forme n'est reconnue par aucune des trois règles
> ci-dessus. Le message du 404 reste le même quel que soit l'identifiant
> inconnu, pour ne pas permettre de deviner quels produits existent en
> observant les réponses.

## Voir aussi

- [`GET /resolve/{identifier}`](/reference/get-resolve/),
  lire en un appel tout ce qu'une page produit affiche.
- [`GET /products/{uid}/public`](/reference/get-products-uid-public/),
  lire les informations publiques d'un produit.
- [`GET /p/{serial}`](/reference/get-p-serial/),
  traduire le numéro de série imprimé en adresse de page consommateur.
- [Notions de base](/notions/),
  distinguer modèle, lot et article avant de commander la moindre étiquette.
