# Intégrer le SDK mobile

Ce que le SDK iOS et Android sait faire, ce que votre application garde à sa charge, comment l'installer aujourd'hui, et pourquoi il n'est pas encore distribuable à une équipe extérieure.

Source : https://docs.sealtrust.io/sdk-mobile/

---

Le SDK mobile est un client typé de l'API publique SealTrust, plus trois écrans
prêts à poser, pour iOS et pour Android. En quittant cette page, vous saurez ce
qu'il sait faire, ce qu'il ne fera jamais, ce que votre application doit garder
à sa charge, comment le brancher, et dans quel état de distribution il se
trouve aujourd'hui.

Commençons par cet état, parce qu'il change la réponse à la question
« puis-je m'en servir cette semaine ».

## Où en est ce SDK aujourd'hui

Les deux plateformes sont en version `0.1.0` et ne sont publiées sur aucun
dépôt de paquets.

Côté iOS, le manifeste du paquet Swift ne se trouve pas à la racine d'un dépôt
git. Le gestionnaire de paquets Swift ne résout un paquet depuis une adresse
git que dans ce cas. La déclaration `.package(url:)` ne fonctionne donc pas, et
seule la déclaration par chemin local fonctionne.

Côté Android, les deux modules déclarent bien des coordonnées Maven,
`io.sealtrust:sealtrust-core` et `io.sealtrust:sealtrust-ui`, en version
`0.1.0`. La configuration de publication ne déclare aucun dépôt distant. La
seule publication possible est donc locale, sur votre propre machine.

## Le partage du travail

Le SDK ne lit pas la puce. C'est la première chose à comprendre, parce qu'elle
détermine tout le reste.

Une puce NFC SealTrust prouve son authenticité en calculant une signature neuve
à chaque lecture, avec une clef propre à la puce. Vérifier cette signature exige
les clefs de puce de la marque, et ces clefs ne quittent pas nos serveurs.

| Qui | Fait quoi |
| --- | --- |
| Le système du téléphone | Lit l'adresse que la puce émet, ou le QR imprimé. |
| Votre application | Possède la session NFC, l'appareil photo, la connexion de l'utilisateur. |
| Le SDK | Lit cette adresse, appelle l'API, transforme la réponse en verdict affichable. |
| L'API SealTrust | Vérifie la cryptographie de la puce et répond. |

Trois conséquences concrètes.

Le SDK ne demande aucune autorisation NFC et aucune autorisation de
localisation. C'est le module Android des écrans qui déclare l'autorisation
d'accès à internet, et c'est la seule qu'il déclare. Le module client seul ne
déclare aucune autorisation.

La vérification hors ligne n'existe pas. Chaque contrôle est un appel réseau.

Le SDK ne contient aucun secret. Il n'y a pas de clef d'API mobile, pas de clef
de signature, pas d'identifiant embarqué. La seule chose qu'il peut porter est
le jeton de session d'un utilisateur déjà connecté par vos soins. Il l'envoie
sur les appels de revendication de propriété, et sur la lecture de passeport
quand ce jeton est disponible.

## Un produit peut porter deux codes, et ils ne prouvent pas la même chose

Le SDK nomme lequel des deux a été lu. Le traitement diffère ensuite pour
chacun.

| Ce qui a été scanné | Ce que cela prouve | Ce que vous appelez |
| --- | --- | --- |
| La puce NFC | L'objet physique était là. La puce recalcule une signature neuve à chaque lecture. | `verifyScan` |
| Le QR imprimé | Quel article c'est. Une photographie de l'étiquette redonne la même chaîne. | `passport`, `certificate`, `history`, et `requestClaimTicket` à partir du numéro de série |

Beaucoup de produits ne portent qu'un QR. La plateforme sert ce mode à part
entière. Le passeport, le certificat et l'historique se lisent alors
normalement, à partir du seul numéro imprimé. C'est la preuve de présence
physique qui n'est pas fournie.

La fonction de lecture d'adresse vous dit laquelle des deux vous avez entre les
mains, et rend une valeur vide quand l'adresse n'est ni l'une ni l'autre. Ce
dernier cas arrive souvent en production : un client qui pointe votre scanner
sur le lien GS1 d'une autre marque mérite une réponse propre.

:::onglets
```swift
import SealTrustKit

func traiter(_ chaineScannee: String, avec client: SealTrustClient) async throws {
    switch ScanURL.read(chaineScannee) {
    case .chip(let preuve):
        let verification = try await client.verifyScan(preuve)
        print(verification.productName ?? "")
    case .printedSerial(let numeroDeSerie):
        let passeport = try await client.passport(identifier: numeroDeSerie)
        print(passeport.productName ?? "")
    case nil:
        print("Ce code n'est pas un code SealTrust.")
    }
}
```
```kotlin
import io.sealtrust.sdk.core.ScanUrl
import io.sealtrust.sdk.core.ScannedCode
import io.sealtrust.sdk.core.SealTrustClient

// Le client bloque : appelez-le hors du fil principal.
fun traiter(chaineScannee: String, client: SealTrustClient) {
    when (val code = ScanUrl.read(chaineScannee)) {
        is ScannedCode.Chip -> println(client.verifyScan(code.proof).productName)
        is ScannedCode.PrintedSerial -> println(client.passport(code.serial).productName)
        null -> println("Ce code n'est pas un code SealTrust.")
    }
}
```
:::

Le QR imprimé porte deux formes : `/p/{serial}` et la forme GS1
`/01/{gtin}/21/{serial}`. Un lien `/01/{gtin}` sans numéro de série nomme une
référence produit, donc la lecture ne rend aucun numéro de série. Ce n'est pas
une impasse : une référence a son propre passeport, celui du modèle, et le
client le lit avec `referencePassport`. Gardez la distinction en tête, un
passeport de référence décrit un modèle et ne prouve rien sur l'objet que vous
tenez.

Le passeport se sert à deux niveaux seulement : le modèle, par son code produit
GTIN, et l'article, par son identifiant. Un passeport de modèle couvre tous les
exemplaires qui partagent le même code produit. Le règlement ESPR autorise aussi
un passeport de lot, que la plateforme ne sert pas aujourd'hui.

Le numéro de série est rendu exactement tel qu'il est imprimé, jamais réécrit.
C'est le serveur qui applique les règles de normalisation. Une application
publiée ne se met pas à jour au même rythme que le serveur, et un client qui
appliquerait ses propres règles répondrait « produit introuvable » sur une
étiquette valable le jour où les deux jeux de règles divergeraient.

## Plateformes et versions minimales

| Caractéristique | iOS | Android |
| --- | --- | --- |
| Version minimale | iOS 16 | niveau d'API 26 |
| Distribution | Paquet Swift `SealTrustSDK` | Projet Gradle, deux modules |
| Client | Bibliothèque `SealTrustKit` | Module `sealtrust-core` |
| Écrans | Bibliothèque `SealTrustUI` | Module `sealtrust-ui` |
| Dépendances du client | Aucune, Foundation seul | Aucune |
| Dépendances des écrans | SwiftUI seul | Jetpack Compose et `androidx.lifecycle:lifecycle-viewmodel-compose` |

Le module Android du client est un module Kotlin ordinaire, sans rien
d'Android dedans. Il compile en Java 17. Le module des écrans vise le niveau de
compilation 35 et le niveau minimal 26, ce dernier étant imposé par la lecture
des dates. Descendez plus bas uniquement avec le sucre syntaxique de la
bibliothèque de base active dans votre application.

## Installer

> [!DANGER] Ce SDK ne se récupère pas depuis un dépôt public
> Écrivez-nous pour obtenir l'accès aux sources, ou appelez directement l'API
> publique. Les deux chemins d'installation ci-dessous supposent que vous
> disposez déjà des sources sur votre machine.

:::onglets
```swift
// Package.swift de votre application.
// Remplacez le chemin par celui de votre copie locale des sources.
dependencies: [
    .package(name: "SealTrustSDK", path: "<chemin-vers-les-sources>/ios"),
],
targets: [
    .target(
        name: "VotreApplication",
        dependencies: [
            .product(name: "SealTrustKit", package: "SealTrustSDK"),
            .product(name: "SealTrustUI", package: "SealTrustSDK"),
        ]
    ),
]
```
```kotlin
// settings.gradle.kts de votre application.
// Remplacez le chemin par celui de votre copie locale des sources.
includeBuild("<chemin-vers-les-sources>/android")

// build.gradle.kts de votre module.
dependencies {
    implementation("io.sealtrust:sealtrust-core")
    implementation("io.sealtrust:sealtrust-ui")
}
```
:::

Le paramètre `name:` est obligatoire sur iOS. Un paquet déclaré par chemin
prend pour identité le nom de son répertoire. Sans ce paramètre, le
gestionnaire de paquets Swift ne trouve pas `SealTrustSDK` et la compilation
échoue.

Sur Android, une seconde voie existe si le build composite ne vous convient
pas. Depuis le répertoire du projet Gradle du SDK, `./gradlew
publishToMavenLocal` installe les deux modules dans votre dépôt Maven local.
Vous les consommez ensuite comme des dépendances versionnées.

```kotlin
repositories {
    mavenLocal()
}

dependencies {
    implementation("io.sealtrust:sealtrust-core:0.1.0")
    implementation("io.sealtrust:sealtrust-ui:0.1.0")
}
```

Si vous ne voulez que le client, prenez `SealTrustKit` seul, ou
`sealtrust-core` seul. Vous ne payez alors rien pour les écrans.

Une conséquence sur Android : c'est le module des écrans qui déclare
l'autorisation d'accès à internet. Si vous prenez `sealtrust-core` seul,
déclarez `android.permission.INTERNET` dans votre propre manifeste.

## Créer le client

Le client se construit avec une configuration. Ses quatre valeurs :

| Valeur | Défaut | Rôle |
| --- | --- | --- |
| Adresse de base | `https://api.sealtrust.io` | À remplacer pour viser un autre environnement ou votre propre relais. |
| Délai d'attente | 15 secondes | Délai de connexion et de lecture. |
| Identifiant du client | `sealtrust-sdk-ios/0.1.0` ou `sealtrust-sdk-android/0.1.0` | Envoyé en en-tête `X-SealTrust-Client`. |
| Fournisseur de jeton | absent | Rend le jeton de session de l'utilisateur connecté, ou rien. |

Le SDK ne connecte personne. Les appels de revendication de propriété exigent
un utilisateur authentifié, donc votre application fournit le jeton. Rendez une
valeur vide quand personne n'est connecté : le SDK échoue alors avec une erreur
d'authentification au lieu d'envoyer une requête qui ne peut revenir qu'en 401.

:::onglets
```swift
import Foundation
import SealTrustKit

enum SessionApplication {
    /// Jeton de l'utilisateur connecté, alimenté par votre propre connexion.
    static var jeton: String?
}

let client = SealTrustClient(
    configuration: SealTrustConfiguration(
        accessTokenProvider: { SessionApplication.jeton }
    )
)
```
```kotlin
import io.sealtrust.sdk.core.SealTrustClient
import io.sealtrust.sdk.core.SealTrustConfiguration

object SessionApplication {
    /** Jeton de l'utilisateur connecté, alimenté par votre propre connexion. */
    @Volatile
    var jeton: String? = null
}

val client = SealTrustClient(
    SealTrustConfiguration(accessTokenProvider = { SessionApplication.jeton }),
)
```
:::

Deux en-têtes accompagnent chaque appel : `Accept: application/json` et
`X-SealTrust-Client`. Les appels qui portent un corps ajoutent
`Content-Type: application/json`.

L'en-tête `Authorization` est ajouté sur les appels de revendication, et sur la
lecture de passeport quand votre application a fourni un jeton. Un propriétaire
connecté voit alors sa lecture élargie.

Une différence de style entre les deux plateformes, à ne pas rater. Sur iOS,
toutes les méthodes qui appellent le réseau sont asynchrones. La construction
de l'adresse du certificat en PDF est synchrone, parce qu'elle n'appelle rien.
Sur Android, les méthodes sont bloquantes : appelez-les hors du fil principal.
Les trois écrans Android le font déjà pour vous.

## Vérifier un scan

C'est l'appel central. Vous lui donnez l'adresse lue sur la puce, il rend un
verdict.

:::onglets
```swift
import SealTrustKit

func verifier(_ adresse: URL, avec client: SealTrustClient) async -> ScanOutcome {
    do {
        return ScanOutcome.from(try await client.verifyScan(url: adresse))
    } catch {
        return ScanOutcome.from(error)
    }
}
```
```kotlin
import io.sealtrust.sdk.core.ScanOutcome
import io.sealtrust.sdk.core.SealTrustClient

fun verifier(adresse: String, client: SealTrustClient): ScanOutcome = try {
    ScanOutcome.of(client.verifyScan(adresse))
} catch (erreur: Throwable) {
    ScanOutcome.of(erreur)
}
```
:::

> [!ATTENTION] Transmettez l'adresse telle que vous l'avez lue
> Les paramètres de la puce font partie de ce qu'elle a signé. Ne les
> normalisez pas, ne les mettez pas en majuscules, n'en inventez aucun. Une
> adresse réécrite produit une signature qui ne correspond plus, donc un
> avertissement de contrefaçon sur un produit sain. Le SDK transmet ces
> paramètres tels quels, faites-en autant en amont.

Le verdict prend huit formes, et les distinguer compte pour l'utilisateur.

| Verdict | Ce qui s'est passé | Ce que l'écran doit dire |
| --- | --- | --- |
| Authentique | La puce a répondu une signature que le serveur a validée. | Produit authentique. |
| Rejeu | Cette adresse exacte a déjà été présentée. | Alerte. C'est le signal qu'une étiquette a pu être copiée. |
| Signature invalide | La signature ne correspond pas à cette puce. | Ce n'est pas une puce SealTrust, ou c'est une copie. |
| Frappe en cours | Puce authentique, son certificat est en cours d'écriture. | Demandez à l'utilisateur de réessayer dans un moment. |
| Jamais enregistré | Puce authentique, rien n'a jamais été enregistré pour elle. | L'anomalie est du côté de la marque. L'acheteur n'a rien à corriger. |
| Lien illisible | L'adresse ne portait pas de preuve exploitable, ou l'API a refusé les paramètres. | Invitez à rescanner. |
| Trop d'appels | Le plafond par adresse IP est atteint. | Invitez à réessayer dans une minute. |
| Échec | Tout le reste, y compris le réseau injoignable. | Erreur technique, sans verdict d'authenticité. |

Un conflit non reconnu est lu comme un rejeu. Le SDK remonte donc l'anomalie
au lecteur.

## Le scellé électronique et la déclaration de vol

Deux informations voyagent avec la vérification et méritent un traitement
particulier.

Le scellé Tag Tamper est un état à trois valeurs : jamais ouvert, ouvert, ou
rien à dire. Une puce standard et une puce dont le scellé n'a jamais été activé
rendent toutes deux la troisième valeur, sur des produits authentiques.
N'affichez jamais l'absence d'information comme un scellé rompu. Le SDK expose
une propriété qui combine cet état avec la politique choisie par la marque, et
qui rend une valeur vide dès qu'il n'y a rien à montrer. Trois politiques
existent : silence, information, et confiance affichée dégradée. La valeur par
défaut est l'information.

> [!INFO] Un scellé rompu n'est pas une contrefaçon
> Le client qui déballe sa boîte rompt le scellé, et tout produit d'occasion l'a
> rompu. L'authenticité cryptographique reste celle du verdict du scan.

Une déclaration de vol ouverte sur une unité ne fait pas échouer la
vérification. Le scan répond normalement, et un booléen porte l'avertissement.
Affichez-le, sinon il n'est dit nulle part.

## Lire un passeport, ses preuves et son certificat

Le client expose sept lectures, toutes publiques.

| Appel | Ce qu'il rend |
| --- | --- |
| `passport` | Le passeport numérique publié d'une unité. |
| `passportProof` | Le faisceau de preuves de ce passeport. |
| `referencePassport` | Le passeport du modèle, à partir d'un GTIN. |
| `referencePassportProof` | Le faisceau de preuves du modèle. |
| `certificate` | Le certificat d'authenticité public. |
| `certificatePDFURL` sur iOS, `certificatePdfUrl` sur Android | L'adresse du même certificat en PDF. Rien n'est appelé, l'adresse est construite. |
| `history` | Les mouvements de propriété et les scans d'authentification, du plus récent au plus ancien. |

Le passeport se lit à un niveau d'accès. Le SDK expose les six valeurs que
l'API accepte : `public`, `end_user`, `repairer`, `recycler`, `upstream` et
`authority`. Les trois niveaux de métier sont des publics distincts. Aucun ne contient
les autres : détenir l'accréditation de réparateur n'ouvre pas celle du
recycleur. Le défaut est `public`. Les niveaux supérieurs sont contrôlés par
le serveur, qui répond 401 ou 403 à un appelant qui n'y a pas droit. Le niveau
réellement servi figure dans la réponse, et il n'est pas toujours celui
demandé : un propriétaire connecté voit sa lecture publique élargie
automatiquement.

:::onglets
```swift
let passeport = try await client.passport(
    identifier: "0x0000000000000000000000000000000000000000000000000000000000000000"
)
print(passeport.accessTier, passeport.productName ?? "")

let preuves = try await client.passportProof(
    identifier: "0x0000000000000000000000000000000000000000000000000000000000000000"
)
print(preuves.dataHash ?? "", preuves.ipfsURI ?? "")

let modele = try await client.referencePassport(gtin: "03701234567890")
print(modele.productName ?? "")
```
```kotlin
val passeport = client.passport(
    "0x0000000000000000000000000000000000000000000000000000000000000000",
)
println("${passeport.accessTier} ${passeport.productName}")

val preuves = client.passportProof(
    "0x0000000000000000000000000000000000000000000000000000000000000000",
)
println("${preuves.dataHash} ${preuves.ipfsUri}")

val modele = client.referencePassport("03701234567890")
println(modele.productName)
```
:::

Le faisceau de preuves porte l'empreinte du contenu, la copie sur IPFS, l'état
d'ancrage sur Base et l'état du certificat vérifiable. L'état d'ancrage prend
trois valeurs distinctes : ancré, jamais ancré, et ancré puis modifié. La
troisième est la seule que ce mécanisme existe pour lever. Ne l'affichez jamais
comme un « non ancré ».

Il prend une quatrième forme, une valeur vide, quand le serveur n'a servi aucun
bloc d'ancrage. Traitez ce cas à part et n'affichez jamais cette valeur vide
comme un « non ancré ». Elle signifie que vous n'avez aucune information sur
l'ancrage de ce produit. L'ancrage sur Base est une opération que SealTrust
déclenche à la main, et la plupart des produits ne sont jamais ancrés. Une
marque ne peut pas le déclencher elle-même.

## Revendiquer la propriété d'un produit

Le parcours enregistre un produit scanné au nom de l'utilisateur connecté. Il
tient en deux appels, plus une lecture d'état.

Le premier appel demande un ticket à partir de la lecture de la puce. Un ticket
n'est délivré que tant que le produit n'appartient à personne.

Une réponse en succès sans ticket est normale. Le champ de détail dit alors
pourquoi : déjà revendiqué, déjà transféré, identifiant inconnu, revendication
concurrente en cours, ou code d'achat exigé.

:::onglets
```swift
import SealTrustKit

func revendiquer(_ chaineScannee: String, avec client: SealTrustClient) async throws {
    guard case .chip(let preuve) = ScanURL.read(chaineScannee) else {
        print("Ce code n'est pas une lecture de puce.")
        return
    }
    let demande = try await client.requestClaimTicket(for: preuve)
    guard let ticket = demande.ticket else {
        print(demande.detail ?? "Aucun ticket délivré.")
        return
    }
    let resultat = try await client.completeClaim(ticket)
    print(resultat.txHash ?? "")
}
```
```kotlin
import io.sealtrust.sdk.core.ScanUrl
import io.sealtrust.sdk.core.ScannedCode
import io.sealtrust.sdk.core.SealTrustClient

// Le client bloque : appelez-le hors du fil principal.
fun revendiquer(chaineScannee: String, client: SealTrustClient) {
    val code = ScanUrl.read(chaineScannee)
    if (code !is ScannedCode.Chip) {
        println("Ce code n'est pas une lecture de puce.")
        return
    }
    val demande = client.requestClaimTicket(code.proof)
    val ticket = demande.ticket
    if (ticket == null) {
        println(demande.detail ?: "Aucun ticket délivré.")
    } else {
        val resultat = client.completeClaim(ticket)
        println(resultat.txHash)
    }
}
```
:::

Le second appel transforme le ticket en propriété inscrite sur la chaîne. Il
exige le jeton de l'utilisateur connecté. La réponse est écrite dès que le
transfert est diffusé, avant qu'il soit gravé, donc l'empreinte de transaction
qu'elle porte désigne une opération en attente.

Certaines marques remettent un code d'achat avec le produit. N'en demandez un à
l'utilisateur qu'après que l'API a répondu une première fois qu'il est exigé.
Les clients des marques qui n'en émettent aucun ne verront alors jamais ce
champ.

Le ticket porte un nonce. C'est la preuve que quelqu'un a physiquement lu
l'étiquette. Gardez-le en mémoire, ne le journalisez pas, ne le stockez pas.

> [!DANGER] Deux erreurs 502 se ressemblent et disent le contraire
> `TRANSFER_NOT_SENT` signifie que rien n'a bougé sur la chaîne : réessayer est
> sans danger. `TRANSFER_SENT_PENDING` signifie que le transfert a bien été
> diffusé et que seule l'écriture du suivi a échoué. Réessayer demande alors à
> l'utilisateur une seconde opération sur la chaîne. Les deux écrans du SDK
> affichent ce second cas comme « revenez dans un moment », jamais comme un
> bouton réessayer.

Un troisième appel lit l'état d'un ticket : délivré, consommé, expiré ou
annulé. Il exige aussi le jeton de l'utilisateur. Le serveur renvoie cet état
tel quel, sans filtrage. Traitez-le comme une chaîne ouverte et prévoyez un
affichage par défaut pour une valeur que vous ne connaissez pas.

Il existe une variante qui demande un ticket à partir du numéro de série
imprimé, sans puce. Elle est authentifiée, et elle ne fonctionne que sur la
toute première revendication d'un produit : un produit déjà possédé ou déjà
revendiqué est refusé en 409. Un numéro imprimé prouve beaucoup moins qu'une
lecture de puce, et ce verrouillage en est la conséquence. Elle n'est pas
ouverte sur tous les comptes : si elle ne l'est pas sur le vôtre, elle répond
404, et c'est la seule lecture à faire de ce code sur cet appel. Vérifiez avec
nous qu'elle est ouverte pour votre application avant de l'intégrer.

## Les trois écrans prêts à poser

Le module d'interface fournit trois écrans construits sur le client. Ils
existent en SwiftUI et en Compose, avec les mêmes paramètres.

| Écran | Ce qu'il prend | Ce qu'il rend à votre application |
| --- | --- | --- |
| Vérification | Le client, l'adresse scannée ou la preuve, des coordonnées facultatives, les textes. | Deux rappels : ouvrir le passeport, ou lancer la revendication. |
| Revendication | Le client, la preuve, un booléen disant si un utilisateur est connecté, le nom du produit, les textes. | Deux rappels : demander la connexion, ou annoncer la revendication faite. |
| Passeport | Le client, l'identifiant, le niveau d'accès, les textes. | Rien, c'est un écran de lecture. |

L'écran de revendication ne lance pas votre connexion. Vous lui dites si un
utilisateur est connecté, il demande la connexion au lieu de déclencher un
appel qui ne peut revenir qu'en 401.

Tous les textes visibles sont surchargeables champ par champ. Un jeu anglais
complet et un jeu français complet sont fournis, et aucune phrase visible n'est
figée à l'intérieur d'un écran.

Les couleurs sont des champs d'un thème et se remplacent une par une. Les trois
écrans n'ont qu'une seule apparence : le thème porte des couleurs fixes et ne
réagit pas au mode sombre du système.

## Les erreurs

Le SDK ramène tout à cinq cas.

| Cas | Quand |
| --- | --- |
| Adresse de scan invalide | Le lien ne portait pas de preuve exploitable. |
| Non authentifié | Un appel authentifié a été tenté sans jeton de session. |
| Erreur d'API | L'API a répondu en 4xx ou 5xx. Le cas porte le code HTTP, le détail, et le code machine quand l'API en préfixe un. |
| Décodage | Le corps de la réponse n'a pas la forme attendue. |
| Transport | La requête n'a jamais abouti : DNS, TLS, délai dépassé, pas de réseau. |

Le code machine est extrait du détail quand celui-ci commence par un mot en
majuscules contenant au moins un tiret bas, par exemple `MINT_PENDING` ou
`CLAIM_EXPIRED`. Un mot en majuscules sans tiret bas n'est jamais extrait. Le
code machine vaut une valeur vide quand l'API répond une phrase libre.

## Plafonds d'appels

Les appels du SDK visent des points d'entrée publics, dont les plafonds se
comptent par adresse IP, sur une fenêtre de 60 secondes.

| Point d'entrée | Plafond |
| --- | --- |
| Vérification de scan | 30 par minute |
| Demande de ticket depuis un scan | 30 par minute |
| Passeport et preuves | 60 par minute |
| Certificat | 60 par minute |
| Historique du produit | 30 par minute |
| Revendication | 20 par minute |

Un dépassement revient en 429, et le SDK le traduit par le verdict « trop
d'appels » sur le chemin de vérification.

## Coordonnées et vie privée

Les coordonnées sont facultatives sur la vérification de scan. Les deux
paramètres valent une valeur vide par défaut, sur les deux plateformes : un
intégrateur qui ne les passe jamais ne transmet rien.

Elles servent aux contrôles anti-fraude côté serveur. Ne les passez qu'avec le
consentement de l'utilisateur.

Le SDK ne lit aucune position, ne demande aucune autorisation de localisation et
ne vérifie pas que vous avez obtenu quoi que ce soit. Le consentement, sa trace
et sa mention dans votre politique de confidentialité sont votre obligation
d'éditeur de l'application.

Ce que la réponse rend en retour est une zone de scan au niveau de la ville,
sous la forme d'un nom de ville et d'un code pays, et uniquement quand vous avez
fourni des coordonnées. La réponse ne contient aucune coordonnée.

Sur iOS, le paquet embarque un manifeste de confidentialité Apple. Il déclare
l'absence de suivi et deux types de données collectées.

La position précise, uniquement quand votre application passe des coordonnées.

L'identifiant utilisateur, c'est-à-dire le jeton de session. Le manifeste dit
qu'il est envoyé uniquement sur `POST /claims/from-serial`, sur
`POST /claims/complete`, sur `GET /claims/{id}`, et sur `GET /passport/{id}`
quand un jeton est disponible. Reprenez cette liste telle quelle si vous
remplissez la déclaration de confidentialité de l'App Store.

Sur Android, rien d'équivalent ne se déclare à l'intérieur du module. C'est
l'éditeur de l'application qui remplit le formulaire de sécurité des données de
la boutique, et il doit y couvrir les données collectées par chaque bibliothèque
embarquée, celle-ci comprise.

## Ce qui n'est pas dans le SDK

- Le transfert de propriété entre deux personnes. Il demande un code à usage
  unique et un scan concordant des deux côtés. C'est un autre parcours.
- La frappe de produits et la destruction. Ce sont des opérations authentifiées,
  elles se conduisent depuis la console.
- La lecture des puces, le scan des QR, le chargement des images, la connexion
  des utilisateurs. Votre application possède déjà tout cela, et le SDK
  n'embarque volontairement aucune dépendance pour les faire.
