# POST /partner/sellout

Déclarer qu'un produit a été remis au client final, depuis le système d'un distributeur. Droit sellout:write.

Source : https://docs.sealtrust.io/reference/post-partner-sellout/

---

Vous déclarez qu'un produit a quitté le rayon et a été remis au client final,
en nommant le point de vente qui l'a vendu.

L'adresse complète est `https://api.sealtrust.io/v1/partner/sellout`. La même
route existe sans le préfixe `/v1`, et c'est la forme `/v1` qui est recommandée
pour une nouvelle intégration.

Cet appel est rejouable. Un produit déjà activé renvoie l'activation existante
avec `status` à `already_activated`, et complète seulement les informations qui
manquaient : le rattachement au point de vente, le pays, la ville. Rien n'est
écrasé. L'en-tête `Idempotency-Key` n'est pas lu par ce point d'entrée.

## Autorisation

Clef d'API dans l'en-tête `Authorization`, au format `Bearer`, avec le droit
`sellout:write`.

```http
Authorization: Bearer votre_clef
```

Une clef sans ce droit reçoit un 403 dont le message nomme le droit manquant.

L'offre de votre marque doit par ailleurs comprendre l'accès API. Sinon vous
recevez un 403 dont le champ `detail` porte le code `FEATURE_NOT_AVAILABLE`.

## Plafond d'appels

Deux plafonds distincts s'appliquent.

| Plafond | Valeur | Ce qui le déclenche |
| --- | --- | --- |
| Débit | 120 appels par fenêtre de 60 secondes par défaut, la valeur réelle dépend de votre offre | un compteur par clef et un compteur pour la somme des clefs de votre marque, avec le même plafond |
| Quota journalier | celui attaché à votre clef, vide signifie illimité | 1 unité par appel, prélevée une fois le point de vente validé, avant la résolution du produit |

Le compteur de débit repart de zéro à chaque nouvelle fenêtre de 60 secondes.
Le quota journalier repart de zéro au passage de minuit en temps universel.

Toute réponse qui passe le plafond de débit porte les en-têtes
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` et
`X-RateLimit-Scope`. `X-RateLimit-Reset` donne l'instant de la remise à zéro,
en secondes depuis le 1er janvier 1970 en temps universel. `X-RateLimit-Scope`
vaut `key` ou `brand`, et désigne le compteur le plus contraignant des deux.
Créer des clefs supplémentaires n'augmente pas le débit total autorisé à votre
marque.

> [!ATTENTION] Trois de ces en-têtes arrivent en deux exemplaires
> Un second compteur, celui de l'adresse d'où part l'appel, s'applique à toute
> notre API et pose lui aussi `X-RateLimit-Limit`, `X-RateLimit-Remaining` et
> `X-RateLimit-Reset`. La réponse porte donc deux valeurs pour chacun de ces
> trois en-têtes, et la plupart des clients HTTP vous les rendent collées et
> séparées par une virgule. Ne lisez aucun des trois comme un nombre.
> `X-RateLimit-Scope` n'apparaît qu'une fois, et c'est le seul en-tête qui
> désigne à coup sûr le compteur de votre clef ou de votre marque.

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

Ce point d'entrée n'a aucun paramètre de chemin ni de requête. Tout passe par le
corps de la requête.

## Corps de la requête

Format `application/json`. Tout champ absent de ce tableau fait refuser la
requête en 422.

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `identifier` | `string` | oui | Le produit vendu. De 1 à 200 caractères. Nous acceptons quatre formes, voir ci-dessous. |
| `retailer_code` | `string` | oui | Le code du point de vente, de 1 à 64 caractères. C'est celui que vous avez défini dans votre console, section Distribution. |
| `country` | `string` | non | Pays de la vente, code ISO à deux lettres. Nous mettons la valeur en majuscules. Une chaîne vide vaut absence. |
| `city` | `string` | non | Ville de la vente, 100 caractères maximum. |

### Les quatre formes acceptées pour `identifier`

Nous les essayons dans cet ordre, et nous bornons toujours la recherche à
votre marque.

| Forme | Exemple | Reconnue à |
| --- | --- | --- |
| Empreinte d'étiquette | `0x0000000000000000000000000000000000000000000000000000000000000000` | `0x` suivi de 64 caractères hexadécimaux, soit 66 caractères en tout. C'est la valeur que nos réponses rendent dans le champ `uid_hash` |
| Identifiant de jeton | `10000000000000000000000000000000000000000000000000000000000000000000000000000` | une suite de chiffres, souvent très longue. C'est la valeur que nos réponses rendent dans le champ `token_id` |
| Numéro de certificat | `ST-CERT-000000000000` | commence par `ST-CERT-`, suivi de 12 caractères. C'est le numéro affiché sur le certificat |
| Numéro de série imprimé sur le produit | `000000000000` | 12 caractères de l'alphabet Crockford Base32 |

Le numéro de série tolère les confusions de lecture courantes. Nous lisons les
lettres `I` et `L` comme le chiffre `1`, la lettre `O` comme le chiffre `0`, et
nous ignorons la casse. Nous ne résolvons jamais un produit détruit ou retiré.

### Ce que valent `country` et `city` quand vous les omettez

Si vous n'envoyez pas `country`, le pays enregistré est celui du point de vente.
Même règle pour `city`. Renseignez ces deux champs quand la vente n'a pas eu
lieu à l'adresse habituelle du point de vente.

## Requête d'exemple

:::onglets
```bash title="curl"
curl -i -X POST https://api.sealtrust.io/v1/partner/sellout \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "000000000000",
    "retailer_code": "BTQ-EXEMPLE-01",
    "country": "FR",
    "city": "Lyon"
  }'
```
```typescript
const response = await fetch(
  "https://api.sealtrust.io/v1/partner/sellout",
  {
    method: "POST",
    headers: {
      Authorization:
        "Bearer st_test_0000000000000000000000000000000000000000000000",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      identifier: "000000000000",
      retailer_code: "BTQ-EXEMPLE-01",
      country: "FR",
      city: "Lyon",
    }),
  },
);

console.log(response.status);
console.log(response.headers.get("X-RateLimit-Scope"));
console.log(await response.json());
```
```python
import requests

response = requests.post(
    "https://api.sealtrust.io/v1/partner/sellout",
    headers={
        "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000",
        "Content-Type": "application/json",
    },
    json={
        "identifier": "000000000000",
        "retailer_code": "BTQ-EXEMPLE-01",
        "country": "FR",
        "city": "Lyon",
    },
    timeout=30,
)

print(response.status_code)
print(response.headers["X-RateLimit-Scope"])
print(response.json())
```
:::

> [!INFO] Le SDK TypeScript ne couvre pas encore ce point d'entrée
> Le paquet `@sealtrust-io/sdk` expose la frappe en lot, la vérification et les
> abonnements aux notifications. La déclaration de vente s'appelle donc en HTTP
> direct, comme ci-dessus.

## Réponse d'exemple

Code HTTP 200, première déclaration pour ce produit.

```json
{
  "status": "activated",
  "activation_id": 1024,
  "product_id": 4096,
  "product_name": "Sac de voyage Exemple SAS",
  "retailer_code": "BTQ-EXEMPLE-01",
  "source": "sellout_declared",
  "activated_at": "2026-08-20T14:32:07.512430+00:00"
}
```

Code HTTP 200 également si le produit avait déjà été activé, par exemple par un
scan du client final avant votre déclaration.

```json
{
  "status": "already_activated",
  "activation_id": 1024,
  "product_id": 4096,
  "product_name": "Sac de voyage Exemple SAS",
  "retailer_code": "BTQ-EXEMPLE-01",
  "source": "qr",
  "activated_at": "2026-08-19T09:14:55.201884+00:00"
}
```

Les sept champs de la réponse.

| Champ | Type | Description |
| --- | --- | --- |
| `status` | `string` | `activated` si cet appel a créé l'activation, `already_activated` si elle existait déjà. |
| `activation_id` | `integer` | Identifiant interne de l'activation. Servez-vous-en pour rapprocher vos enregistrements des nôtres. Ne l'affichez pas à un tiers. |
| `product_id` | `integer` | Identifiant du produit résolu à partir de `identifier`. |
| `product_name` | `string` ou `null` | Nom du produit tel qu'il est enregistré. |
| `retailer_code` | `string` | Le code de point de vente que vous avez envoyé, après résolution. Sur une réponse `already_activated`, si l'activation portait déjà un autre point de vente, nous gardons ce rattachement d'origine et ce champ ne le reflète pas. |
| `source` | `string` | Ce qui a créé l'activation : `sellout_declared` pour une déclaration par cette route, `nfc` ou `qr` pour un premier scan. |
| `activated_at` | `string` ou `null` | Date et heure de l'activation, au format ISO 8601 avec fuseau. Pour une activation qui existait déjà, c'est la date d'origine, jamais celle de votre appel. |

## Erreurs

| Code | Condition | Que faire |
| --- | --- | --- |
| 401 | En-tête `Authorization` absent. La réponse porte `WWW-Authenticate: Bearer`. | Ajoutez l'en-tête `Authorization: Bearer <votre clef>`. |
| 401 | En-tête présent mais qui ne commence pas par `Bearer ` suivi d'un espace. | Corrigez le format de l'en-tête. |
| 401 | Clef inconnue, ou clef de moins de 40 caractères. | Vérifiez que vous envoyez la clef entière. Elle n'est lisible qu'une fois, à sa création ; si elle est perdue, créez-en une nouvelle depuis la console. |
| 403 | Clef révoquée, ou déjà marquée expirée. Le message nomme l'état. | Créez une nouvelle clef. Une clef révoquée est refusée à chaque appel suivant, et la console ne propose aucune remise en service. |
| 403 | Clef arrivée à sa date d'expiration. Message `API key has expired`. | Créez une nouvelle clef. L'expiration est constatée au premier appel qui suit l'échéance, et elle est définitive. |
| 403 | La clef n'a pas le droit `sellout:write`. Message `Missing required scopes: sellout:write`. | Créez une clef qui porte ce droit. Les droits d'une clef existante ne se modifient pas. |
| 403 | L'offre de votre marque ne comprend pas l'accès API. `detail` vaut `{"code": "FEATURE_NOT_AVAILABLE", "feature": "api_access"}`. | Contactez-nous pour changer d'offre. Réessayer ne changera rien. |
| 404 | Aucun point de vente actif ne porte ce `retailer_code` dans votre marque. Message `Retailer '<code>' not found for this brand`. | Vérifiez le code dans votre console, section Distribution, et vérifiez que le point de vente est actif. |
| 404 | Aucun produit de votre marque ne correspond à `identifier`. Message `Product not found for this identifier`. | Vérifiez l'identifiant et sa forme. Un produit d'une autre marque, détruit ou retiré, répond la même chose. |
| 422 | Corps invalide : champ obligatoire absent, champ inconnu, `country` qui n'est pas deux lettres, ou longueur dépassée. | Le corps de la réponse liste les champs fautifs et le motif de chaque refus. Corrigez et rappelez. |
| 429 | Plafond de débit atteint, par votre clef ou par la somme des clefs de votre marque. | Attendez le nombre de secondes indiqué par l'en-tête `Retry-After`. `X-RateLimit-Scope` vous dit lequel des deux compteurs a refusé. |
| 429 | Quota journalier de la clef épuisé. Message `Quota exceeded. Remaining today: <restant>/<limite>`. | Les en-têtes `X-Quota-Limit` et `X-Quota-Remaining` donnent l'état du compteur. `X-Quota-Reset` donne la date de la dernière remise à zéro, une date passée. La suivante a lieu au passage de minuit en temps universel. Attendez minuit en temps universel, ou utilisez une clef au quota plus large. |
| 500 | Erreur interne pendant le traitement de l'appel. | Réessayez. Si l'erreur persiste, contactez-nous en indiquant l'heure de l'appel et le `retailer_code` employé. |
| 503 | Le service qui compte les appels est momentanément indisponible. Message `Rate limiting temporarily unavailable, please retry shortly`. | Réessayez dans quelques instants. Aucune déclaration n'a été enregistrée. |

### L'ordre des contrôles, et ce qu'il change pour votre quota

Les contrôles s'enchaînent dans cet ordre : plafond de débit, offre de la
marque, existence du point de vente, quota journalier de la clef, puis
résolution du produit.

Une conséquence utile : un `retailer_code` inconnu ne consomme pas votre quota
journalier, alors qu'un `identifier` introuvable le consomme, parce que nous
résolvons le produit après le décompte. Si vous rapprochez des ventes par lots,
validez vos codes de points de vente une fois pour toutes, et attendez-vous à ce
que les identifiants inconnus coûtent une unité chacun.

## Voir aussi

- [`GET /certificate/{identifier}`](/reference/get-certificate/),
  lire le certificat d'authenticité d'un article.
- [Prendre en main la console](/console-prise-en-main/),
  le tour des écrans côté marque, dans l'ordre où vous les utilisez.
- [API partenaire, vue d'ensemble](/api-vue-ensemble/),
  adresse de base, clefs, droits, plafonds d'appels et pagination.
- [Erreurs de l'API](/api-erreurs/),
  reconnaître un code d'erreur et décider s'il faut corriger ou rejouer.
