Erreurs de l'API
Lire n'importe quelle réponse d'erreur de l'API SealTrust, reconnaître le code renvoyé, décider s'il faut corriger la requête ou la rejouer, et retrouver un appel précis quand vous nous écrivez.
Sur cette page
- Toutes les erreurs ont la même forme
- Les en-têtes qui accompagnent une réponse
- X-Request-Id, l'identifiant de chaque appel
- Les en-têtes de budget
- Le catalogue, code par code
- 400, votre requête est mal formée
- 401, l'API ne sait pas qui vous êtes
- 403, l'API sait qui vous êtes et refuse
- 404, la ressource n'existe pas pour vous
- 409, l'état actuel interdit cette opération
- 413, l'envoi est trop gros
- 422, l'API refuse une valeur que vous avez envoyée
- 429, vous appelez trop souvent
- 500, la panne est de notre côté
- 503, l'API refuse l'appel sans rien modifier
- Une réponse 200 peut vouloir dire non
- Que rejouer, que ne pas rejouer
- Une gestion d'erreur complète, en trois langages
- Ce qu'il faut retenir
En quittant cette page, vous saurez lire n'importe quelle réponse d'erreur de l'API SealTrust. Vous reconnaîtrez le code renvoyé, vous déciderez s'il faut corriger votre requête ou la rejouer telle quelle, et vous retrouverez un appel précis dans nos journaux quand vous nous écrivez.
Cette page se lit en entier une fois, au moment où vous écrivez votre client. Elle sert ensuite de catalogue.
#Toutes les erreurs ont la même forme
Le corps d'une réponse d'erreur contient un seul champ : detail. Sa valeur
prend trois formes, et vous devez savoir gérer les trois.
Une phrase. C'est la forme la plus courante. Le texte s'adresse à un lecteur humain.
{
"detail": "Invalid API key"
}Un objet qui porte un code. Certains refus liés à votre offre et certaines
confirmations de suppression renvoient un objet. Le champ code est stable. Le champ
message change au fil des versions.
{
"detail": {
"code": "QUOTA_EXCEEDED",
"resource": "products",
"current": 8,
"additional": 5,
"max": 10,
"period": "monthly"
}
}Une liste. Quand la validation du corps ou des paramètres échoue, detail
est une liste. Chaque entrée décrit une valeur refusée.
| Clef de l'entrée | Ce qu'elle contient |
|---|---|
loc | le chemin de la valeur fautive, sous forme de liste, par exemple ["body", "retailer_code"] ou ["query", "page"] |
type | le nom de la règle non respectée, par exemple less_than_equal |
msg | la phrase lisible qui explique le refus |
Selon la règle non respectée, une entrée peut porter des clefs supplémentaires.
Lisez loc et type. Ne comparez jamais msg caractère par caractère.
#Les en-têtes qui accompagnent une réponse
#X-Request-Id, l'identifiant de chaque appel
Chaque réponse de l'API porte un en-tête X-Request-Id, une erreur 500
comprise. Si votre requête en envoie un, c'est le vôtre qui est repris, à
condition qu'il fasse de 1 à 128 caractères pris parmi les lettres sans accent,
les chiffres, le point, le tiret et le souligné. Sinon, ou si vous n'en envoyez
pas, nous en fabriquons un.
Enregistrez cette valeur à côté de chaque appel qui échoue. Quand vous nous écrivez au sujet d'un refus, donnez-la : elle nous mène directement à l'appel concerné.
curl -sS -D - -o /dev/null \
-X POST https://api.sealtrust.io/v1/partner/sellout \
-H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-H "X-Request-Id: exemple-sas-2026-08-20-0001" \
-d '{"identifier": "DEM000000000", "retailer_code": "BTQ-EXEMPLE-01"}'#Les en-têtes de budget
Un appel authentifié par clef d'API porte les compteurs de débit dans sa réponse, y compris quand la route refuse ensuite. Un 400, un 403 dû à votre offre, à votre quota ou à une marque qui ne correspond pas, vous disent donc quel budget de débit il vous reste.
Trois familles de refus ne portent aucun compteur : les 401, les 403 de clef révoquée ou expirée, et les 403 de droit manquant sur la clef. Testez la présence de l'en-tête avant de lire sa valeur.
| En-tête | Ce qu'il vaut |
|---|---|
X-RateLimit-Limit | le plafond d'appels de la fenêtre en cours |
X-RateLimit-Remaining | le nombre d'appels encore acceptés dans cette fenêtre |
X-RateLimit-Reset | l'horodatage Unix, en secondes, de la fin de la fenêtre |
X-RateLimit-Scope | key si le compteur le plus serré est celui de la clef, brand si c'est celui de la marque |
Retry-After | présent sur un refus de débit, le nombre de secondes à attendre, jamais inférieur à 1 |
X-Quota-Limit | le quota journalier de la clef. Présent uniquement sur le refus 429 du quota journalier |
X-Quota-Remaining | ce qu'il en reste aujourd'hui. Présent uniquement sur le refus 429 du quota journalier |
X-Quota-Reset | la date, au format ISO 8601, de la dernière remise à zéro du compteur. Présent uniquement sur le refus 429 du quota journalier. La remise à zéro suivante a lieu au passage de minuit en temps universel |
WWW-Authenticate | vaut Bearer sur les refus 401 dus à un en-tête Authorization absent ou mal formé |
Un appel qui réussit ne vous dit pas combien de quota journalier il vous reste.
Les trois en-têtes X-Quota-* n'existent que sur le refus 429 du quota. Pour
suivre ce compteur avant de le heurter, ouvrez la liste des clefs d'API dans la
console : elle affiche, pour chaque clef, la consommation du jour sur son quota.
#Le catalogue, code par code
#400, votre requête est mal formée
Le serveur a compris la requête et la refuse. Corrigez la requête. La rejouer telle quelle donnera le même résultat.
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
| Le fichier CSV est vide ou n'a pas de ligne d'en-tête | {"error": "CSV vide ou sans en-têtes"} | envoyez un fichier dont la première ligne nomme les colonnes |
| Une colonne obligatoire manque à l'en-tête CSV | {"error": "Colonnes manquantes", "missing": ["category_id"]} | ajoutez les colonnes nommées dans missing |
| Des lignes du CSV sont invalides | un objet dont error vaut CSV invalide et dont rows liste chaque ligne fautive | corrigez chaque ligne signalée, voir juste après ce tableau |
| Des objets du JSON sont invalides | un objet dont error vaut JSON invalide et dont rows liste chaque objet fautif | corrigez chaque objet signalé, voir juste après ce tableau |
| Le corps JSON n'est pas une liste | "Le JSON doit être une liste d'objets" | encadrez vos objets par des crochets |
| Le corps JSON est illisible | "Corps de requete illisible : le JSON envoye est mal forme. Envoyez une liste d'objets produit en application/json, ou un fichier CSV en multipart/form-data." | vérifiez que vous envoyez du JSON valide, ou passez au CSV en multipart |
| Le lot dépasse 500 articles | "Batch trop volumineux : 640 items (max 500)" | découpez en plusieurs appels de 500 articles au maximum |
| Le lot ne contient aucun article | "Batch vide (aucun item)" | n'envoyez pas de lot vide |
| Une catégorie citée n'existe pas | "Catégories introuvables : [77, 91]" | corrigez les category_id nommés |
| Un scan ou une lecture de puce envoyé par votre serveur ne nomme pas le visiteur | une phrase qui commence par "This route counts the visitor, not your server" | envoyez les quatre en-têtes X-SealTrust-Visitor-*, voir Afficher le passeport dans votre page |
| Le fichier gzip envoyé est abîmé | une phrase qui commence par Fichier gzip illisible : et donne le motif technique | recompressez le fichier, ou envoyez-le sans compression |
| La suppression d'un abonnement n'apporte pas de confirmation | un objet dont code vaut CONFIRMATION_REQUIRED, accompagné de message et de what_to_type | ajoutez le paramètre confirm égal à l'adresse exacte de l'abonnement |
| La confirmation de suppression ne correspond pas | un objet dont code vaut CONFIRMATION_MISMATCH, accompagné de message et de what_to_type | recopiez l'adresse telle que la lecture de l'abonnement la renvoie. Rien n'a été supprimé |
| Un GTIN mal formé est passé à une route GS1 | "Invalid GTIN" | vérifiez que le segment ne contient que des chiffres et n'en dépasse pas quatorze |
| Une empreinte d'article mal formée est passée à l'historique | "Invalid UID hash format (must be 0x + 64 hex characters)" | envoyez 0x suivi de 64 caractères hexadécimaux |
Les erreurs de lignes vous sont rendues toutes en même temps. Vous corrigez tout en un passage.
{
"detail": {
"error": "CSV invalide",
"rows": [
{
"line": 2,
"external_ref": "EX-0001",
"error": "row: Value error, brand_code is required"
}
]
}
}Le champ line est le numéro de ligne dans le fichier. La première ligne de
données porte le numéro 2, puisque la ligne 1 est l'en-tête.
En JSON, la forme change : la position s'appelle index et commence à 1, et
external_ref n'est pas repris.
{
"detail": {
"error": "JSON invalide",
"rows": [
{
"index": 1,
"error": "metadata_uri: Field required"
}
]
}
}#401, l'API ne sait pas qui vous êtes
Votre requête n'a pas d'identité valide. Corrigez l'en-tête Authorization.
Les deux premières lignes du tableau portent l'en-tête
WWW-Authenticate: Bearer.
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
L'en-tête Authorization est absent | "Missing Authorization header" | ajoutez Authorization: Bearer <votre clef> |
L'en-tête ne commence pas par Bearer | "Invalid Authorization header format (expected 'Bearer <token>')" | respectez le mot Bearer, un espace, puis la clef |
| La valeur envoyée n'a pas la forme d'une clef | "Invalid API key format" | vérifiez que la clef a été copiée en entier |
| La clef ne correspond à aucune clef connue | "Invalid API key" | la clef est fausse ou a été supprimée. Créez-en une nouvelle depuis la console de votre marque |
Le niveau d'accès end_user au passeport est demandé sans compte connecté | "End-user-tier access requires authentication" | connectez-vous avec n'importe quel compte, ou demandez le niveau public |
| Un niveau d'accès professionnel au passeport est demandé sans compte connecté | "Professional-tier access requires authentication" | connectez-vous, ou demandez le niveau public |
| Le niveau d'accès autorité est demandé sans compte connecté | "Authority-tier access requires authentication" | connectez-vous avec un compte d'autorité de surveillance |
| La signature du visiteur ne correspond pas à la clef, à la méthode ou au chemin appelé | "Invalid visitor signature" | signez avec l'empreinte SHA-256 de votre clef, et le chemin exact appelé sans ses paramètres |
| L'heure de la signature du visiteur est à plus de cinq minutes de la nôtre | une phrase qui nomme X-SealTrust-Visitor-Timestamp | signez chaque appel au moment de l'envoi, et réglez l'horloge de votre serveur |
#403, l'API sait qui vous êtes et refuse
Votre identité est valide. Votre clef, votre offre ou votre périmètre ne couvrent pas cette opération. Rejouer la requête ne change rien.
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
| La clef est révoquée | "API key is revoked" | utilisez une clef active |
| La clef a été marquée expirée | "API key is expired" | créez une nouvelle clef |
| La clef atteint sa date d'expiration pendant cet appel | "API key has expired" | créez une nouvelle clef. La clef bascule en expirée dès ce refus |
| Un droit exigé par la route manque à la clef | "Missing required scopes: mint:batch" | créez une clef qui porte les droits nommés |
| Un droit manque sur une route d'abonnement | "Missing required scope: webhooks:write" | créez une clef qui porte ce droit |
| Une ligne du lot, une vente, un abonnement ou une liste d'abonnements nomme une marque que la clef ne sert pas : ni la sienne, ni une marque cliente de votre contrat revendeur. Un code qui ne nomme aucune marque reçoit la même réponse | "This API key cannot act for this brand." | corrigez le brand_code, lu dans GET /v1/partner/catalog/brands. Pour un lot, le lot entier est refusé |
| Votre offre ne comprend pas la fonction demandée | {"code": "FEATURE_NOT_AVAILABLE", "feature": "api_access"} | changez d'offre, ou n'appelez pas cette route |
| Sur un compte d'essai, le lot ferait dépasser les 10 produits de la période de facturation. Une offre vendue ne rend jamais ce refus pour des pièces | un objet dont code vaut QUOTA_EXCEEDED, détaillé juste après ce tableau | attendez la période suivante, réduisez le lot, ou souscrivez une offre |
| Vous demandez un niveau d'accès au passeport que votre compte ne couvre pas | "This tier is restricted to the product's brand, partners holding the matching accreditation, and market-surveillance authorities" | demandez le niveau qui correspond à votre accréditation |
| Un partenaire du portail n'a aucune accréditation active | "Aucune accréditation active" | demandez à la marque de vous accréditer |
| Un compte non partenaire appelle le portail partenaire | "Partner account required (repairer or recycler)" | utilisez un compte de type réparateur ou recycleur |
| La marque est sur l'offre Conformité, qui ne crée aucune pièce, aucun encodage ni aucun nouveau code d'achat | {"code": "OFFER_EXCLUDES_IDENTITY", "offer": "compliance", "feature": "mint"} | ne rejouez pas. Voir Conformité, ou Conformité + Identité |
L'objet QUOTA_EXCEEDED porte de quoi décider sans nous écrire :
{
"detail": {
"code": "QUOTA_EXCEEDED",
"resource": "products",
"current": 8,
"additional": 5,
"max": 10,
"period": "monthly"
}
}#404, la ressource n'existe pas pour vous
Sur les points d'entrée publics et sur l'API à clef, un objet qui existe hors de votre périmètre répond 404, exactement comme un objet inexistant. Un point de vente d'une autre marque, un abonnement d'une autre marque, un produit d'une autre marque : la réponse est la même que pour un identifiant inventé.
Le portail partenaire suit la même règle. Un produit d'une marque qui ne vous a pas accrédité répond 404, quelle que soit la forme de l'identifiant. Sur le domaine propre d'une marque, le portail ne trouve que les produits de cette marque.
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
| Le code de point de vente est inconnu, inactif, ou d'une autre marque | "Retailer 'BTQ-EXEMPLE-01' not found for this brand" | créez le point de vente, ou activez-le, avant de déclarer la vente |
| L'identifiant de produit ne correspond à rien | "Product not found for this identifier" | vérifiez l'empreinte d'étiquette, l'identifiant de jeton ou le numéro de certificat |
| L'abonnement demandé n'existe pas ou appartient à une autre marque | "Webhook subscription not found" | listez vos abonnements pour retrouver le bon identifiant |
| Le produit demandé n'existe pas | "Product not found" | vérifiez l'identifiant |
| Le produit existe et n'a aucun passeport publié | "No published passport found for this product" | publiez le passeport depuis la console |
| Le lien GS1 ne mène à rien | "Unknown GS1 Digital Link" | vérifiez le GTIN et, s'il y en a un, le numéro de série ou le numéro de lot |
| Aucun certificat n'a été émis pour ce produit | "No certificate found for this product" | émettez le certificat avant de le demander |
| Le produit n'appartient à aucun lot ancré | "No Merkle anchor for this product" | la preuve d'ancrage n'existe pas pour cet article |
| Un partenaire du portail ouvre un produit inexistant, ou le produit d'une marque qui ne l'a pas accrédité | "Produit introuvable" | vérifiez l'identifiant lu sur le produit, puis demandez une accréditation à la marque qui le fabrique |
| Une pièce nommée à l'émission des codes d'achat est inconnue, ou d'une autre marque : la même réponse dans les deux cas | {"code": "PIECE_NOT_FOUND", "pieces": [{"serial": "Y5T2VGGF2NP9"}]} | vérifiez les numéros de série listés et le code de la marque dans l'adresse. Rien n'a été émis |
#409, l'état actuel interdit cette opération
La requête est correcte. Elle entre en conflit avec ce qui existe déjà.
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
| La même clef d'idempotence a déjà servi pour un lot différent | "Idempotency-Key 'exemple-lot-001' was already used with a different request body" | changez de clef d'idempotence pour ce nouveau lot |
| Un appel portant la même clef d'idempotence est en cours de traitement | "A request with this Idempotency-Key is already being processed" | attendez quelques secondes, puis relisez le résultat du premier appel |
| Un scellé NFC est lu sur un article dont la frappe est partie et n'est pas confirmée | "MINT_PENDING: mint submitted, waiting for on-chain confirmation." | réessayez dans quelques minutes, la situation se résout seule |
| Un scellé NFC est lu sur un article jamais frappé | "MINT_NOT_SUBMITTED: this product has not been minted yet." | la frappe n'a pas eu lieu. Réessayer ne changera rien, reprenez la création de l'article |
| Une marque qui a des pièces, ou des lots en cours de création, passe à l'offre Conformité sans le confirmer | {"code": "OFFER_CHANGE_NEEDS_CONFIRMATION", "pieces": 3, "batches_in_flight": 0, "live_purchase_codes": 1, "open_claims": 1, "pending_transfers": 0} | renvoyez la même requête avec confirm_pieces_kept à true |
| La preuve d'ancrage d'un lot ne peut pas être servie en l'état | une phrase qui dit que le lot doit être ancré de nouveau | rejouer ne résout pas ce refus. La preuve ne pourra être servie qu'après un nouvel ancrage du lot, que nous seuls déclenchons. Relevez le X-Request-Id et signalez-le nous |
| Le code d'achat d'une pièce a déjà servi à la revendiquer | {"code": "PURCHASE_CODE_ALREADY_USED", "pieces": [...]} | rien n'a été émis. Retirez ces pièces, ou renvoyez avec "replace_used": true si vous voulez vraiment remplacer ce code |
Les deux messages du scellé NFC commencent par un code en majuscules suivi de deux points. Testez ce préfixe. La phrase qui suit peut être reformulée.
#413, l'envoi est trop gros
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
| Un CSV compressé dépasse environ 19,5 Mio une fois décompressé | "Fichier compresse trop volumineux une fois decompresse (plafond 19 Mio)" | découpez le fichier. Un lot ne dépasse de toute façon pas 500 articles |
#422, l'API refuse une valeur que vous avez envoyée
C'est le code des paramètres et des corps de requête que le serveur sait lire
et refuse. Le champ detail est alors une liste, sauf mention contraire dans
le tableau.
| Condition | Ce que vous devez faire |
|---|---|
| Un champ obligatoire manque au corps d'une requête JSON | ajoutez le champ nommé dans loc |
| Un champ hors contrat est envoyé à la déclaration de vente ou à un abonnement | retirez le champ. Ces corps refusent tout champ non prévu |
L'adresse d'un abonnement ne commence pas par https://, ou sort des bornes de 10 à 2048 caractères | corrigez l'adresse |
| Un type d'événement inconnu est demandé à l'abonnement | reprenez un nom de la liste des événements souscriptibles |
| Un paramètre de pagination sort de ses bornes | ramenez skip à 0 ou plus, et limit entre 1 et 100 |
Une valeur de filtre ou de pagination sort du domaine que la route accepte. detail est alors une phrase | ramenez la valeur dans les bornes documentées de la route |
Une empreinte de puce mal formée est envoyée à la vérification d'originalité. detail est alors une phrase, par exemple "uid_hex doit faire 7 ou 10 octets" | corrigez la valeur nommée dans le message |
Un partenaire du portail déclare un type d'intervention non couvert par ses accréditations. detail est alors une phrase qui liste les types autorisés | déclarez un type figurant dans la liste rendue |
Un code de garde présenté par un partenaire est refusé. detail est alors une phrase | l'intervention n'a pas été enregistrée et le code n'a pas été consommé. Redemandez un code valide |
L'offre demandée n'existe pas. detail est alors un objet dont code vaut UNKNOWN_OFFER | envoyez compliance ou compliance_identity |
Les lignes d'un lot de frappe nomment plusieurs marques. detail est alors un objet dont code vaut ONE_BRAND_PER_BATCH | envoyez un lot par marque |
Le brand_code d'une vente ou d'un abonnement n'est pas un code de dix caractères, ou la requête porte à la fois brand_code et brand_id | envoyez le seul brand_code, tel que GET /v1/partner/catalog/brands le donne |
Un numéro de marque (brand_id, parent_brand_id) ou un code de marque (brand_code) est mal écrit : un nombre décimal comme 5.0, un signe comme +5, un espace, true, ou un code qui n'a pas la forme d'un code public. La règle vaut pour le chemin, les paramètres de l'adresse, le corps et chaque ligne d'un lot JSON. detail est alors un objet dont code vaut BRAND_REF_NOT_WELL_FORMED et dont references dit où se trouve la valeur (path, query, body ou form, et le numéro de la ligne), sans la répéter | écrivez le numéro en chiffres seuls, 5 ou "5", ou envoyez le code public tel que GET /v1/partner/catalog/brands le donne. Rien n'a été créé |
L'adresse du visiteur n'est pas une adresse IP, ou désigne votre propre machine ou votre propre réseau (boucle locale, lien local, réseau privé comme 10.x ou 192.168.x). detail est alors une phrase qui nomme X-SealTrust-Visitor-IP | envoyez l'adresse publique du visiteur, telle que votre serveur l'a reçue |
#429, vous appelez trop souvent
Quatre compteurs différents rendent ce code. Les en-têtes vous disent lequel a refusé.
| Compteur | Comment le reconnaître | Ce que vous devez faire |
|---|---|---|
| Le débit de votre clef | X-RateLimit-Scope: key et Retry-After | attendez le nombre de secondes annoncé, puis rejouez la requête à l'identique |
| Le débit de votre marque, toutes clefs confondues | X-RateLimit-Scope: brand et Retry-After | ralentissez l'ensemble de vos intégrations. Créer des clefs supplémentaires n'augmente pas ce plafond |
| Le quota journalier de votre clef | les en-têtes X-Quota-Limit, X-Quota-Remaining et X-Quota-Reset, et l'absence de Retry-After | attendez la remise à zéro, au passage de minuit en temps universel. Le quota journalier se fixe à la création de la clef et ne se modifie plus. Si le besoin est régulier, créez une nouvelle clef avec un quota plus haut, basculez votre intégration dessus, puis révoquez l'ancienne |
| Le plafond de codes d'achat de la marque, 20 000 par heure | {"code": "PURCHASE_CODE_BUDGET_EXCEEDED"} et Retry-After | attendez, puis rejouez. Rien n'a été émis |
| Le plafond par adresse sur les routes publiques de vérification | "Rate limit exceeded: 30 requests per 60s". Le nombre annoncé est celui du chemin que vous avez appelé | espacez vos lectures. Les lectures publiques (GET) du passeport, du certificat (hors téléchargement du PDF), de /resolve, de la chronologie et de la preuve d'ancrage acceptent 600 appels par minute et par adresse, parce que des centaines de visiteurs d'un salon ou d'un réseau mobile partagent souvent une même adresse. Les actions restent plus serrées : les chemins /qr, /sdm et l'enregistrement d'un scan acceptent 30 appels, /p/, /01/ et le téléchargement d'un certificat en acceptent 60. Chaque famille de chemins a son propre compteur, et le préfixe /v1 ne crée pas un second budget |
La console connectée lit votre session (GET /auth/state) et vos marques
(GET /brands et les chemins en dessous) à chaque chargement de page. Avec une
session valide, ces lectures se comptent par compte et non par adresse, à 600
par minute : plusieurs personnes d'un même bureau ne partagent pas ce compteur.
Sans session valide, le plafond par adresse s'applique comme avant.
Un refus de plafond public porte Retry-After, en secondes. Attendez ce délai
avant de rappeler.
Le refus du débit d'une clef d'API nomme le plafond qui a refusé et sa fenêtre :
{
"detail": "Rate limit exceeded: 120 requests per 60 seconds"
}Quand c'est le plafond de la marque qui refuse, la phrase le dit, elle donne le
plafond de la marque, et elle précise que cette clef reste sous son propre
plafond. L'en-tête X-RateLimit-Scope vaut alors brand.
Le dépassement du quota journalier prend une autre forme :
{
"detail": "Quota exceeded. Remaining today: 0/1000"
}#500, la panne est de notre côté
Votre requête n'a rien de fautif. Ne la corrigez pas. Relevez le
X-Request-Id de la réponse, rejouez une fois après quelques secondes, et
signalez-nous cet identifiant si le refus persiste.
Le corps d'un 500 n'a pas de forme garantie. N'écrivez aucune logique contre son contenu. Branchez-vous sur le statut 500 et sur rien d'autre.
{
"detail": "Internal Server Error"
}Sur une opération qui écrit, comme la frappe d'un lot, considérez le résultat
comme inconnu. Rejouez avec la même valeur d'Idempotency-Key que le premier
appel : soit le premier appel avait abouti et vous recevez sa réponse, soit il
n'avait rien créé et le lot part.
#503, l'API refuse l'appel sans rien modifier
| Condition | detail renvoyé | Ce que vous devez faire |
|---|---|---|
| Le comptage des appels est momentanément indisponible | "Rate limiting temporarily unavailable, please retry shortly" | attendez quelques secondes et rejouez. Rien n'a été lu ni modifié |
| La lecture en chaîne a échoué pendant une vérification | "Error during blockchain verification" | réessayez plus tard. Le produit n'est pas déclaré faux pour autant |
| Les frais du réseau Base dépassent le plafond que nous acceptons de payer. Le plafond vaut aussi pour une marque dont les opérations passent par un coffre multi-signature | commence par "GAS_PRICE_ABOVE_CAP" | rejouez après le délai de l'en-tête Retry-After (cinq minutes). Rien n'a été envoyé sur la chaîne ni écrit chez nous |
#Une réponse 200 peut vouloir dire non
Six points d'entrée répondent 200 sur un cas que vous devez traiter comme un refus. Un client qui ne regarde que le code HTTP les manque tous les six. Pour chacun, le champ à lire est nommé ci-dessous.
La vérification publique d'un QR. GET /qr/verify répond 200 avec
"valid": false dans cinq situations. Le champ message dit laquelle
s'applique. Lisez toujours valid.
Valeur de message | Ce que cela veut dire |
|---|---|
Invalid product: blockchain verification failed. | la vérification en chaîne a échoué. C'est le seul des cinq cas qui déclare le produit non authentique |
Invalid QR code signature. This QR code may be tampered with. | la signature du code est fausse. Le code a été modifié, ou il ne vient pas de nous |
QR code has expired. Please request a new QR code. | le code a plus de 30 jours. Faites produire un nouveau QR |
Mint submitted, waiting for on-chain confirmation. | la frappe de l'article est partie et n'est pas encore confirmée. Réessayez dans quelques minutes |
This product has not been minted yet. | l'article n'a jamais été frappé. Réessayer ne changera rien |
{
"valid": false,
"message": "Invalid QR code signature. This QR code may be tampered with.",
"uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"token_id": null,
"product_name": null,
"brand_name": null,
"image_url": null,
"contract_address": null,
"scan_area": null
}La vérification d'une signature d'originalité.
POST /originality/read-sig/verify répond 200 avec "valid": false quand la
signature lue sur la puce ne se vérifie pas contre la clef publique utilisée. Le
champ error vaut alors signature invalide. Lisez valid.
La vérification d'intégrité d'un passeport.
GET /passport/{identifier}/verify répond 200 même quand une comparaison
échoue. Trois champs portent le résultat.
db_hash_match et ipfs_match valent true, false ou null. false veut
dire que la comparaison a échoué. null veut dire qu'elle n'a pas pu être
faite, ce qui est différent d'un échec.
seal.chain_link_match vaut true ou false, et il est absent du bloc seal
quand la version du passeport n'a pas été scellée, ou quand elle a été scellée
avant l'existence du chaînage. Lisez d'abord seal.sealed et seal.linked :
quand les deux valent true, seal.chain_link_match est présent. C'est le
champ qui dit si une version scellée a été modifiée après publication.
La vérification du justificatif vérifiable.
GET /passport/{identifier}/vc/verify répond 200 avec "verified": false
quand la signature du justificatif ne se vérifie pas contre la clef de la
marque. Le champ error vaut alors verification_failed et
credential_subject vaut null. Lisez verified.
Le suivi d'un lot. GET /v1/partner/mint/batch/status/{job_id} répond 200
avec "status": "unknown" quand le lot est introuvable, et 200 avec
"status": "failed" quand il a échoué. Les valeurs que vous verrez sont
queued, started, finished, failed et unknown. Bouclez sur status,
et arrêtez-vous dès qu'il quitte queued et started. Ne bouclez pas sur
is_finished : la réponse unknown ne porte que job_id et status, et une
boucle qui attend is_finished ne s'arrêterait jamais.
La déclaration de vente. POST /v1/partner/sellout répond 200 avec
"status": "already_activated" quand cet article avait déjà été déclaré vendu.
Rien n'a été créé une seconde fois.
#Que rejouer, que ne pas rejouer
| Code | Rejouer à l'identique ? | Pourquoi |
|---|---|---|
| 400 | non | la requête est fautive, elle le restera |
| 401 | non | il faut d'abord réparer l'en-tête d'autorisation |
| 403 | non | il faut d'abord changer la clef, le droit ou l'offre |
| 404 | non | l'objet n'existe pas dans votre périmètre |
| 409 sur une clef d'idempotence | non | changez de clef, ou lisez le résultat du premier appel |
409 MINT_PENDING | oui, après quelques minutes | la confirmation en chaîne arrive |
409 MINT_NOT_SUBMITTED | non | la frappe n'a jamais eu lieu |
| 409 sur la preuve d'ancrage d'un lot | non | il faut d'abord que le lot soit ancré de nouveau, et cela ne dépend pas de vous |
| 413 | non | il faut réduire l'envoi |
| 422 | non | l'API refuse une valeur que vous avez envoyée |
| 429 de débit | oui, après Retry-After | la fenêtre de débit se libère |
| 429 de quota journalier | non avant la remise à zéro de minuit en temps universel | ce refus ne porte pas de Retry-After. Le compteur ne se libère qu'au changement de jour |
| 500 | une fois, avec la même clef d'idempotence | le résultat de l'appel est inconnu |
| 503 | oui, après quelques secondes | rien n'a été modifié |
Un rejeu automatique se fait avec un délai qui croît, et un nombre d'essais
borné. Rejouez sur 503, sur 500, et sur les 429 qui portent un Retry-After.
Sur un 429 de quota journalier, arrêtez d'appeler jusqu'au changement de jour.
Sur les autres codes 4xx, corrigez la requête avant tout nouvel appel.
#Une gestion d'erreur complète, en trois langages
Ces trois programmes font la même chose : ils déclarent une vente, ils distinguent les familles d'erreur, et ils gardent l'identifiant de requête.
Exécutez ces trois programmes sur votre serveur. L'API partenaire n'accepte pas
d'appel venant d'un navigateur, et une clef d'API n'a rien à faire dans du code
envoyé au navigateur. Depuis un navigateur, la lecture de X-Request-Id et
celle de Retry-After rendraient null : ces deux en-têtes ne sont pas exposés
au code de page.
#!/usr/bin/env bash
set -u
entetes=$(mktemp)
trap 'rm -f "$entetes"' EXIT
reponse=$(curl -sS -w '\n%{http_code}' -D "$entetes" \
-X POST https://api.sealtrust.io/v1/partner/sellout \
-H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{"identifier": "DEM000000000", "retailer_code": "BTQ-EXEMPLE-01"}')
code=$(printf '%s' "$reponse" | tail -n 1)
corps=$(printf '%s' "$reponse" | sed '$d')
requete=$(grep -i '^x-request-id:' "$entetes" | tr -d '\r' | cut -d' ' -f2)
echo "code=$code request_id=$requete"
echo "$corps"
case "$code" in
200) echo "vente enregistrée" ;;
429|503) echo "réessayez plus tard" ;;
5*) echo "panne serveur, signalez $requete" ;;
*) echo "requête à corriger" ;;
esacconst reponse = await fetch("https://api.sealtrust.io/v1/partner/sellout", {
method: "POST",
headers: {
Authorization:
"Bearer st_live_0000000000000000000000000000000000000000000000",
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: "DEM000000000",
retailer_code: "BTQ-EXEMPLE-01",
}),
});
const requestId = reponse.headers.get("X-Request-Id");
const corps = await reponse.json();
if (reponse.ok) {
console.log("vente enregistrée", corps.status, requestId);
} else if (reponse.status === 429 || reponse.status === 503) {
const attente = Number(reponse.headers.get("Retry-After") ?? 5);
console.log("réessayez dans", attente, "secondes", requestId);
} else if (reponse.status >= 500) {
console.log("panne serveur, signalez", requestId);
} else if (Array.isArray(corps.detail)) {
for (const erreur of corps.detail) {
console.log("champ refusé", erreur.loc.join("."), erreur.type);
}
} else if (corps.detail && typeof corps.detail === "object") {
console.log("refus code", corps.detail.code, requestId);
} else {
console.log("refus", corps.detail, requestId);
}import requests
reponse = requests.post(
"https://api.sealtrust.io/v1/partner/sellout",
headers={
"Authorization": "Bearer st_live_0000000000000000000000000000000000000000000000",
"Content-Type": "application/json",
},
json={"identifier": "DEM000000000", "retailer_code": "BTQ-EXEMPLE-01"},
timeout=30,
)
request_id = reponse.headers.get("X-Request-Id")
corps = reponse.json()
if reponse.ok:
print("vente enregistrée", corps["status"], request_id)
elif reponse.status_code in (429, 503):
print("réessayez dans", reponse.headers.get("Retry-After", "5"), "secondes", request_id)
elif reponse.status_code >= 500:
print("panne serveur, signalez", request_id)
else:
detail = corps.get("detail")
if isinstance(detail, list):
for erreur in detail:
print("champ refusé", ".".join(str(p) for p in erreur["loc"]), erreur["type"])
elif isinstance(detail, dict):
print("refus code", detail.get("code"), request_id)
else:
print("refus", detail, request_id)#Ce qu'il faut retenir
- Le corps d'erreur porte toujours
detail, sous trois formes : une phrase, un objet àcode, ou une liste de valeurs refusées. - Branchez votre code sur le statut HTTP et sur
detail.code. Ne comparez jamais les phrases. - Gardez le
X-Request-Idde chaque échec. C'est ce que nous vous demanderons. - Rejouez sur 503, sur 500, et sur les 429 qui portent un
Retry-After. Corrigez sur tout le reste. - Sur une écriture, rejouez avec la même valeur d'
Idempotency-Keyque l'appel dont vous ignorez le sort. - Lisez
valid,verified,statuset les champs de correspondance : six points d'entrée décrivent un refus dans une réponse 200.
Cette page vous a-t-elle été utile ?
Votre réponse ouvre un courriel pré-rempli dans votre messagerie, à destination de contact@sealtrust.io. Vous le relisez avant de l'envoyer.