{"openapi":"3.1.0","info":{"title":"agentic-commerce-api","version":"0.1.3","description":"API de présentation catalogue et de devis configurable Fulfiller : recherche de produits cotables, espace de configuration, et devis (prix CALCULÉ, non figé) pour une sélection — dont une grille par palier de quantité. Destinée aux agents IA et au chatbot."},"servers":[{"url":"https://www.fulfiller.com/agenticopenapi","description":"Base API"}],"components":{"schemas":{},"parameters":{}},"paths":{"/products/{id}/config":{"get":{"operationId":"getProductConfiguration","summary":"Espace de configuration d’un produit","description":"Renvoie les options configurables d’un produit (quantité, délai, valeurs, taille) et leurs valeurs disponibles. À appeler avant /validate ou /quote pour connaître les options valides.","parameters":[{"schema":{"type":"string","pattern":"^[1-9]\\d*$","example":1001},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Schéma de configuration du produit.","content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"number"},"versionId":{"type":"number"},"configurationId":{"type":"number"},"state":{"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"type":{"type":"string","description":"Type d’option tel que déclaré en amont. SÉLECTIONNABLES via /validate et /quote : QUANTITY, DELIVERY_TIME, LIST_OF_VALUES, SIZE. Déclarés en amont mais NON sélectionnables — l’option est décrite, la sélectionner renvoie une alerte OPTION_TYPE_UNSUPPORTED et le défaut du produit s’applique : LIST_OF_RANGES, QUANTITY_BY_VALUE. Liste OUVERTE : un type ajouté en amont apparaît ici sans changement de code."},"label":{"type":"string"},"unit":{"type":"string"},"allowsCustomInput":{"type":"boolean"},"values":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"info":{"type":"string"},"pictoUrl":{"type":"string"}},"required":["code","label"]}},"quantities":{"type":"array","items":{"type":"object","properties":{"min":{"type":"number"},"max":{"type":"number"},"increment":{"type":"number"}},"required":["min","max"]}},"sizes":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"minWidth":{"type":"number"},"maxWidth":{"type":"number"},"minHeight":{"type":"number"},"maxHeight":{"type":"number"},"increment":{"type":"number"},"surface":{"type":"number"},"info":{"type":"string"},"pictoUrl":{"type":"string"}},"required":["code"]}},"deliveryTimes":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"label":{"type":"string"},"days":{"type":"number"},"daysByQty":{"type":"array","items":{"type":"object","properties":{"qtyMin":{"type":"number"},"days":{"type":"number"}},"required":["qtyMin","days"]}},"cutOff":{"type":"string"}},"required":["code","label"]}},"order":{"type":"number"},"info":{"type":"string"}},"required":["code","type","label","order"]}},"preselections":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"value":{"type":"string"}},"required":["option","value"]}},"presets":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"isDefault":{"type":"boolean"},"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"value":{"type":"string"}},"required":["option","value"]}}},"required":["options"]}},"url":{"type":"string","description":"Page produit publique correspondante. ABSENTE si le lien n’a pas pu être résolu de façon univoque — ce n’est pas une erreur et la configuration reste exploitable."}},"required":["productId","versionId","configurationId","options","preselections"]}}}},"400":{"description":"Requête ou sélection invalide / contexte de prix non supporté.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"Produit introuvable.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/products/{id}/validate":{"post":{"operationId":"validateProductSelection","summary":"Validation/résolution d’une sélection","description":"Résout une sélection tolérante en codes canoniques et vérifie sa commandabilité (sans prix, voir /quote). Renvoie sélection résolue, options activables et alertes : `SELECTION_NOT_ORDERABLE` si non commandable, `QUANTITY_OUT_OF_RANGE` (200, pas 400 comme /quote) si quantité hors plage.","parameters":[{"schema":{"type":"string","pattern":"^[1-9]\\d*$","example":1001},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","maxLength":128},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string","maxLength":128},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string","maxLength":128},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]},"maxItems":50},"preset":{"type":"string","minLength":1,"maxLength":128,"description":"Code ou nom d’un preset de la configuration (`presets` : « MOINS_CHER », « Top vente »…) : ses valeurs servent de base, les `options` données explicitement l’emportent. Un preset inconnu est signalé (SELECTION_UNKNOWN) et ignoré."}},"required":["options"]},"pricingContext":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["PUBLIC"]}},"required":["kind"]},{"type":"object","properties":{"kind":{"type":"string","enum":["CUSTOMER"]},"customerId":{"type":"string"}},"required":["kind","customerId"]},{"type":"object","properties":{"kind":{"type":"string","enum":["RESELLER"]},"resellerId":{"type":"string"}},"required":["kind","resellerId"]},{"type":"object","properties":{"kind":{"type":"string","enum":["B2B"]},"accountId":{"type":"string"}},"required":["kind","accountId"]}],"default":{"kind":"PUBLIC"}}},"required":["selection"]}}}},"responses":{"200":{"description":"Sélection résolue + options activables.","content":{"application/json":{"schema":{"type":"object","properties":{"resolvedSelection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"]},"summary":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","description":"Code de l’option (celui de `resolvedSelection`)."},"label":{"type":"string","description":"Nom de l’option, à citer (ex. « Format »)."},"value":{"type":"string","description":"Valeur COTÉE, en clair : libellé, quantité, ou gabarit et dimensions."},"status":{"type":"string","enum":["AS_REQUESTED","DEFAULT","REPLACED_BY_DEFAULT","ADJUSTED","CHANGED_BY_CONFIGURATOR"],"description":"AS_REQUESTED : la valeur demandée. DEFAULT : option non précisée, valeur par défaut du produit. REPLACED_BY_DEFAULT : la valeur demandée n’a pas été reconnue (inconnue, ambiguë, hors plage ou dimensions hors bornes), la valeur par défaut est cotée à sa place. ADJUSTED : la valeur demandée n’était pas commandable telle quelle, la plus proche est cotée (quantité ou dimensions hors du pas, valeur ambiguë départagée). CHANGED_BY_CONFIGURATOR : le configurateur du produit a remplacé la valeur demandée par une autre (règle produit). Hors AS_REQUESTED et DEFAULT, dites-le à l’utilisateur."},"requested":{"type":"string","description":"Ce qui avait été demandé. Présent seulement quand la valeur cotée en diffère."}},"required":["option","label","value","status"]},"description":"La sélection cotée, EN CLAIR : une ligne par option, avec son nom, sa valeur lisible et son origine (`status`). C’est ce qu’il faut citer pour décrire le devis à l’utilisateur. Toute ligne hors AS_REQUESTED et DEFAULT est une valeur qu’il n’a pas obtenue telle quelle : dites-le, avec `requested`."},"enabledOptions":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"type":{"type":"string","description":"Type d’option tel que déclaré en amont. SÉLECTIONNABLES via /validate et /quote : QUANTITY, DELIVERY_TIME, LIST_OF_VALUES, SIZE. Déclarés en amont mais NON sélectionnables — l’option est décrite, la sélectionner renvoie une alerte OPTION_TYPE_UNSUPPORTED et le défaut du produit s’applique : LIST_OF_RANGES, QUANTITY_BY_VALUE. Liste OUVERTE : un type ajouté en amont apparaît ici sans changement de code."},"enabledValues":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Code de la valeur activable. ABSENT sur une entrée de quantité, qui s’identifie par {min, max, increment} — son absence n’est pas une erreur."},"isEnabled":{"type":"boolean"},"min":{"type":"number"},"max":{"type":"number"},"increment":{"type":"number"}},"required":["isEnabled"]}}},"required":["code","enabledValues"]}},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]}},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["resolvedSelection","summary","enabledOptions","alerts","requiresReview","reviewReasons"]}}}},"400":{"description":"Requête ou sélection invalide / contexte de prix non supporté.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"Produit introuvable.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/products/{id}/quote":{"post":{"operationId":"quoteProduct","summary":"Devis pour une sélection","description":"Devis CALCULÉ par ZeLoom pour une sélection. Prix à annoncer : `devis.finalPriceHt` (promo déduite ; `priceHt` = prix catalogue). 400 `QUANTITY_OUT_OF_RANGE` / `SIZE_OUT_OF_RANGE` : quantité ou taille hors bornes (hors du pas seulement : 200 + alerte d’ajustement).","parameters":[{"schema":{"type":"string","pattern":"^[1-9]\\d*$","example":1001},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","maxLength":128},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string","maxLength":128},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string","maxLength":128},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]},"maxItems":50},"preset":{"type":"string","minLength":1,"maxLength":128,"description":"Code ou nom d’un preset de la configuration (`presets` : « MOINS_CHER », « Top vente »…) : ses valeurs servent de base, les `options` données explicitement l’emportent. Un preset inconnu est signalé (SELECTION_UNKNOWN) et ignoré."}},"required":["options"]},"pricingContext":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["PUBLIC"]}},"required":["kind"]},{"type":"object","properties":{"kind":{"type":"string","enum":["CUSTOMER"]},"customerId":{"type":"string"}},"required":["kind","customerId"]},{"type":"object","properties":{"kind":{"type":"string","enum":["RESELLER"]},"resellerId":{"type":"string"}},"required":["kind","resellerId"]},{"type":"object","properties":{"kind":{"type":"string","enum":["B2B"]},"accountId":{"type":"string"}},"required":["kind","accountId"]}],"default":{"kind":"PUBLIC"}}},"required":["selection"]}}}},"responses":{"200":{"description":"Devis pour la sélection.","content":{"application/json":{"schema":{"type":"object","properties":{"devis":{"type":"object","properties":{"finalPriceHt":{"type":"number","description":"Prix HT À PAYER pour la sélection : promotion déduite s’il y en a une, sinon égal à `priceHt`. C’est LE prix à annoncer. HT, hors TVA, hors port."},"priceHt":{"type":"number","description":"Prix HT CATALOGUE de la sélection, AVANT promotion. Quand `promo` est présent, c’est le prix BARRÉ : le prix à payer est `finalPriceHt`."},"currency":{"type":"string"},"deliveryDays":{"type":"number"},"atelier":{"type":"string"},"promo":{"type":"object","nullable":true,"properties":{"priceHt":{"type":"number","description":"Prix HT remisé : égal à `finalPriceHt`."},"percent":{"type":"number","description":"Remise en pourcentage du prix catalogue."},"name":{"type":"string","description":"Libellé de la promotion ; chaîne vide si l’amont n’en donne pas."},"endsAt":{"type":"string","description":"Fin de la promotion, si connue."}},"required":["priceHt","percent","name"],"description":"Promotion appliquée à ce devis, ou `null`."},"stockAvailable":{"type":"boolean"}},"required":["finalPriceHt","priceHt","currency","stockAvailable"]},"resolvedSelection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"]},"summary":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","description":"Code de l’option (celui de `resolvedSelection`)."},"label":{"type":"string","description":"Nom de l’option, à citer (ex. « Format »)."},"value":{"type":"string","description":"Valeur COTÉE, en clair : libellé, quantité, ou gabarit et dimensions."},"status":{"type":"string","enum":["AS_REQUESTED","DEFAULT","REPLACED_BY_DEFAULT","ADJUSTED","CHANGED_BY_CONFIGURATOR"],"description":"AS_REQUESTED : la valeur demandée. DEFAULT : option non précisée, valeur par défaut du produit. REPLACED_BY_DEFAULT : la valeur demandée n’a pas été reconnue (inconnue, ambiguë, hors plage ou dimensions hors bornes), la valeur par défaut est cotée à sa place. ADJUSTED : la valeur demandée n’était pas commandable telle quelle, la plus proche est cotée (quantité ou dimensions hors du pas, valeur ambiguë départagée). CHANGED_BY_CONFIGURATOR : le configurateur du produit a remplacé la valeur demandée par une autre (règle produit). Hors AS_REQUESTED et DEFAULT, dites-le à l’utilisateur."},"requested":{"type":"string","description":"Ce qui avait été demandé. Présent seulement quand la valeur cotée en diffère."}},"required":["option","label","value","status"]},"description":"La sélection cotée, EN CLAIR : une ligne par option, avec son nom, sa valeur lisible et son origine (`status`). C’est ce qu’il faut citer pour décrire le devis à l’utilisateur. Toute ligne hors AS_REQUESTED et DEFAULT est une valeur qu’il n’a pas obtenue telle quelle : dites-le, avec `requested`."},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]}},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["devis","resolvedSelection","summary","alerts","requiresReview","reviewReasons"]}}}},"400":{"description":"Requête ou sélection invalide / contexte de prix non supporté.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"Produit introuvable.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/products/{id}/quote/pivot":{"post":{"operationId":"quoteProductByQuantity","summary":"Grille de prix par palier de quantité","description":"Grille de devis pour plusieurs paliers de quantité, même règle que `/quote` palier par palier : hors plage → erreur localisée (réponse globale 200) ; hors du pas → coté au palier le plus proche, `quotedQuantity` + alerte `QUANTITY_ADJUSTED`.","parameters":[{"schema":{"type":"string","pattern":"^[1-9]\\d*$","example":1001},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","maxLength":128},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string","maxLength":128},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string","maxLength":128},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]},"maxItems":50},"preset":{"type":"string","minLength":1,"maxLength":128,"description":"Code ou nom d’un preset de la configuration (`presets` : « MOINS_CHER », « Top vente »…) : ses valeurs servent de base, les `options` données explicitement l’emportent. Un preset inconnu est signalé (SELECTION_UNKNOWN) et ignoré."}},"required":["options"]},"pricingContext":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["PUBLIC"]}},"required":["kind"]},{"type":"object","properties":{"kind":{"type":"string","enum":["CUSTOMER"]},"customerId":{"type":"string"}},"required":["kind","customerId"]},{"type":"object","properties":{"kind":{"type":"string","enum":["RESELLER"]},"resellerId":{"type":"string"}},"required":["kind","resellerId"]},{"type":"object","properties":{"kind":{"type":"string","enum":["B2B"]},"accountId":{"type":"string"}},"required":["kind","accountId"]}],"default":{"kind":"PUBLIC"}},"quantities":{"type":"array","items":{"type":"integer","minimum":0,"exclusiveMinimum":true},"minItems":1,"maxItems":20}},"required":["selection","quantities"]}}}},"responses":{"200":{"description":"Grille de cotation par palier de quantité.","content":{"application/json":{"schema":{"type":"object","properties":{"resolvedSelection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"]},"summary":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","description":"Code de l’option (celui de `resolvedSelection`)."},"label":{"type":"string","description":"Nom de l’option, à citer (ex. « Format »)."},"value":{"type":"string","description":"Valeur COTÉE, en clair : libellé, quantité, ou gabarit et dimensions."},"status":{"type":"string","enum":["AS_REQUESTED","DEFAULT","REPLACED_BY_DEFAULT","ADJUSTED","CHANGED_BY_CONFIGURATOR"],"description":"AS_REQUESTED : la valeur demandée. DEFAULT : option non précisée, valeur par défaut du produit. REPLACED_BY_DEFAULT : la valeur demandée n’a pas été reconnue (inconnue, ambiguë, hors plage ou dimensions hors bornes), la valeur par défaut est cotée à sa place. ADJUSTED : la valeur demandée n’était pas commandable telle quelle, la plus proche est cotée (quantité ou dimensions hors du pas, valeur ambiguë départagée). CHANGED_BY_CONFIGURATOR : le configurateur du produit a remplacé la valeur demandée par une autre (règle produit). Hors AS_REQUESTED et DEFAULT, dites-le à l’utilisateur."},"requested":{"type":"string","description":"Ce qui avait été demandé. Présent seulement quand la valeur cotée en diffère."}},"required":["option","label","value","status"]},"description":"La sélection cotée, EN CLAIR : une ligne par option, avec son nom, sa valeur lisible et son origine (`status`). C’est ce qu’il faut citer pour décrire le devis à l’utilisateur. Toute ligne hors AS_REQUESTED et DEFAULT est une valeur qu’il n’a pas obtenue telle quelle : dites-le, avec `requested`. La quantité n’y figure pas : elle est propre à chaque entrée."},"entries":{"type":"array","items":{"type":"object","properties":{"quantity":{"type":"number","description":"Palier DEMANDÉ."},"quotedQuantity":{"type":"number","description":"Quantité RÉELLEMENT cotée quand elle diffère du palier demandé (hors du pas → palier commandable le plus proche, alerte QUANTITY_ADJUSTED ; ou substitution du configurateur, alerte SELECTION_ADJUSTED_UPSTREAM) : le devis porte sur CELLE-CI. Absente sinon."},"devis":{"type":"object","properties":{"finalPriceHt":{"type":"number","description":"Prix HT À PAYER pour la sélection : promotion déduite s’il y en a une, sinon égal à `priceHt`. C’est LE prix à annoncer. HT, hors TVA, hors port."},"priceHt":{"type":"number","description":"Prix HT CATALOGUE de la sélection, AVANT promotion. Quand `promo` est présent, c’est le prix BARRÉ : le prix à payer est `finalPriceHt`."},"currency":{"type":"string"},"deliveryDays":{"type":"number"},"atelier":{"type":"string"},"promo":{"type":"object","nullable":true,"properties":{"priceHt":{"type":"number","description":"Prix HT remisé : égal à `finalPriceHt`."},"percent":{"type":"number","description":"Remise en pourcentage du prix catalogue."},"name":{"type":"string","description":"Libellé de la promotion ; chaîne vide si l’amont n’en donne pas."},"endsAt":{"type":"string","description":"Fin de la promotion, si connue."}},"required":["priceHt","percent","name"],"description":"Promotion appliquée à ce devis, ou `null`."},"stockAvailable":{"type":"boolean"}},"required":["finalPriceHt","priceHt","currency","stockAvailable"]},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]}},"error":{"type":"object","properties":{"code":{"type":"string","description":"Motif du refus de CE palier. QUANTITY_OUT_OF_RANGE : la quantité n’est dans aucune plage déclarée — changez de palier (le message donne les quantités possibles). SELECTION_ADJUSTED_UPSTREAM : le configurateur a écarté la quantité. Les autres valeurs sont des codes d’erreur du domaine (INVALID_SELECTION, PRODUCT_NOT_FOUND…). Un palier hors du pas n’est PAS une erreur : il est coté au palier commandable le plus proche (`quotedQuantity`)."},"message":{"type":"string"}},"required":["code","message"]},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel. Sur une entrée de pivot, une entrée NON COTÉE (`error` présent) est toujours `requiresReview: true`, même sans aucune alerte."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["quantity","alerts","requiresReview","reviewReasons"]}},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel. À la racine : OU des entrées/lignes."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`. À la racine : union DÉDUPLIQUÉE des raisons des entrées/lignes, dans leur ordre d’apparition, tronquée à 10 (aucune autre source ne contribue à la racine)."}},"required":["resolvedSelection","summary","entries","requiresReview","reviewReasons"]}}}},"400":{"description":"Requête ou sélection invalide / contexte de prix non supporté.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"Produit introuvable.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/catalog/search":{"get":{"operationId":"searchCatalog","summary":"Recherche de produits cotables","description":"Recherche plein-texte, renvoie des produits cotables (id+SKU). Top-N borné par `limit`. `startingPrice` = prix d’appel AFFICHÉ par le site, indicatif (ni plancher garanti ni prix ferme) ; base `TOTAL`/`UNIT`/`PER_SQUARE_METER`/`UNKNOWN`. Prix ferme : `/products/{id}/quote`.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200,"example":"carte de visite"},"required":true,"name":"q","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":24,"example":10},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Produits cotables correspondant aux mots-clés (résolus jusqu’à l’id ZeLoom).","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"zeloomProductId":{"type":"number","description":"Identifiant ZeLoom du produit : même valeur que `productId`, son nom dans les routes de cotation."},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"shortDescription":{"type":"string"},"miniDescription":{"type":"string"},"startingPriceHt":{"type":"number","description":"DÉPRÉCIÉ — montant nu, sans sa base de lecture : un lot ici, une unité là, donc NON comparable d’un produit à l’autre. Utilisez `startingPrice`, qui porte `basis`, `label` et la `quantity` de référence. Conservé pour compatibilité ; retiré une fois les consommateurs migrés."},"startingPrice":{"type":"object","properties":{"amount":{"type":"number"},"currency":{"type":"string"},"taxIncluded":{"type":"boolean"},"basis":{"type":"string","enum":["TOTAL","UNIT","PER_SQUARE_METER","UNKNOWN"]},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"label":{"type":"string","description":"Libellé du site, VERBATIM et de format variable (« 3.41€ », « Jusqu’à… ») : pour le citer, utilisez `displayLabel`."},"displayLabel":{"type":"string","description":"Libellé à citer, au format unique « À partir de 1,12 € HT le panneau » : les mots du site, préfixe et montant normalisés."}},"required":["amount","currency","taxIncluded","basis","label","displayLabel"],"description":"Prix d’appel AFFICHÉ par le site : libellé marketing, indicatif, parfois périmé ou propre à une configuration particulière — ni un plancher garanti, ni un prix à annoncer comme ferme. Absent quand le site n’en affiche pas. Seul un devis (`/quote`) est ferme."},"category":{"type":"string"},"alsoListedAs":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"}},"required":["sku","name","url"]},"description":"Autres fiches du site qui vendent CE MÊME produit : même `zeloomProductId`, donc même configuration et mêmes prix. Ne les présentez pas comme des produits distincts. Absent quand la recherche n’en a trouvé aucune."},"productId":{"type":"number","description":"Identifiant à passer aux routes et outils de cotation (même valeur que `zeloomProductId`, son alias historique conservé pour compatibilité)."}},"required":["zeloomProductId","sku","name","url","productId"]}},"count":{"type":"integer"},"relaxedQuery":{"type":"string","description":"Présent quand la requête d’origine n’a rien trouvé : la version simplifiée (mots de contexte, d’intention et de budget retirés) réellement cherchée. Les résultats répondent à CELLE-CI : signalez-le si l’écart compte."},"hint":{"type":"string","description":"Présent seulement quand il n’y a aucun résultat : comment reformuler."}},"required":["results","count"],"example":{"results":[{"productId":77,"zeloomProductId":77,"sku":"CARTE_STANDARD","name":"Carte de visite Standard","url":"https://www.fulfiller.com/carte-de-visite-standard.html","startingPriceHt":16,"startingPrice":{"amount":16,"currency":"EUR","taxIncluded":false,"basis":"TOTAL","quantity":50,"label":"Dès 16€ HT les 50 exemplaires","displayLabel":"À partir de 16 € HT les 50 exemplaires"},"category":"cartes_visite"},{"productId":604,"zeloomProductId":604,"sku":"STYLO_METAL_STANDARD","name":"Stylo métal standard","url":"https://www.fulfiller.com/stylo-metal-standard.html","startingPriceHt":0.72,"startingPrice":{"amount":0.72,"currency":"EUR","taxIncluded":false,"basis":"UNIT","label":"Jusqu'à 0,72€ HT le stylo","displayLabel":"À partir de 0,72 € HT le stylo"},"category":"objets_publicitaires"}],"count":2}}}}},"400":{"description":"Requête de recherche invalide (q manquant / hors bornes).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Source catalogue (storefront / Akeneo) temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/catalog/resolve":{"get":{"operationId":"resolveProduct","summary":"Résolution d’un produit par SKU, URL ou productId","description":"Convertit une identité EXACTE (`sku`, `url` de la page produit, ou `productId`) en identité complète (`productId`, SKU, nom, liens), sans recherche approximative. Fournissez exactement un paramètre. 404 si le produit ne se cote pas ; 400 `PRODUCT_AMBIGUOUS` si un productId désigne plusieurs fiches.","parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_%-]{1,128}$","example":"POSTER_PHOTO_SUR_MESURE"},"required":false,"name":"sku","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":2048,"example":"https://www.fulfiller.com/poster-papier-photo-brillant-premium.html"},"required":false,"name":"url","in":"query"},{"schema":{"type":"string","pattern":"^[1-9]\\d*$","example":"1977"},"required":false,"name":"productId","in":"query"}],"responses":{"200":{"description":"Produit résolu (id ZeLoom + SKU + nom, `url` si le lien a été résolu).","content":{"application/json":{"schema":{"type":"object","properties":{"zeloomProductId":{"type":"number","description":"Identifiant ZeLoom du produit : même valeur que `productId`, son nom dans les routes de cotation."},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"productId":{"type":"number","description":"Identifiant à passer aux routes et outils de cotation (même valeur que `zeloomProductId`, son alias historique conservé pour compatibilité)."},"links":{"type":"object","properties":{"config":{"type":"string","description":"GET — espace de configuration du produit."},"quote":{"type":"string","description":"POST — devis pour une sélection."}},"required":["config","quote"]}},"required":["zeloomProductId","sku","name","productId","links"],"example":{"productId":1977,"zeloomProductId":1977,"sku":"POSTER_PHOTO_SUR_MESURE","name":"Poster papier Photo Brillant Premium","url":"https://www.fulfiller.com/poster-papier-photo-brillant-premium.html","links":{"config":"https://www.fulfiller.com/agenticopenapi/products/1977/config","quote":"https://www.fulfiller.com/agenticopenapi/products/1977/quote"}}}}}},"400":{"description":"Paramètres invalides (aucun / plusieurs / SKU non conforme), `PAGE_URL_NOT_SUPPORTED` (l’URL n’est pas une page produit du storefront), ou `PRODUCT_AMBIGUOUS` (productId porté par plusieurs fiches : le message les cite).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"SKU inconnu du PIM, non cotable, ou page sans SKU déclaré.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Source catalogue (storefront / Akeneo) temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/catalog/product":{"get":{"operationId":"getProductDetails","summary":"Fiche produit (descriptions, points clés, FAQ, prépresse)","description":"Descriptions, points clés, questions fréquentes, contraintes de fichier (fond perdu, résolution, marges) et ressources (visuels, gabarits, consignes). Identifiez le produit par `productId`, `sku` OU `url`. AUCUN prix ici : voir /quote. Champ absent = non renseigné.","parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_%-]{1,128}$","example":"POSTER_PHOTO_SUR_MESURE"},"required":false,"name":"sku","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":2048,"example":"https://www.fulfiller.com/poster-papier-photo-brillant-premium.html"},"required":false,"name":"url","in":"query"},{"schema":{"type":"string","pattern":"^[1-9]\\d*$","example":"1977"},"required":false,"name":"productId","in":"query"}],"responses":{"200":{"description":"Fiche du produit (les champs non renseignés au PIM sont absents).","content":{"application/json":{"schema":{"type":"object","properties":{"zeloomProductId":{"type":"number","description":"Identifiant ZeLoom du produit : même valeur que `productId`, son nom dans les routes de cotation."},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"category":{"type":"string"},"miniDescription":{"type":"string","description":"Accroche courte, texte brut, ≤140 caractères."},"shortDescription":{"type":"string","description":"Résumé commercial, converti du HTML PIM en texte sur une ligne."},"description":{"type":"string","description":"Fiche rédactionnelle longue, convertie du HTML PIM en texte. Les SAUTS DE PARAGRAPHE sont conservés (une ligne vide entre blocs) : sur un texte de plusieurs milliers de caractères, les aplatir sur une seule ligne détruit la structure que l’auteur y a mise. Texte marketing : un prix qui y serait écrit (« Dès 14.5 € ») est souvent PÉRIMÉ, seul `/quote` fait foi."},"keyPoints":{"type":"array","items":{"type":"string"},"description":"Arguments courts en puces, tels que saisis (ex. « De 50 à 50 000 ex »)."},"faq":{"type":"array","items":{"type":"object","properties":{"question":{"type":"string"},"answer":{"type":"string"}},"required":["question","answer"]},"description":"Questions fréquentes du produit. Rare : la plupart des produits n’en portent pas."},"printSpecs":{"type":"object","properties":{"bleed":{"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"required":["amount","unit"],"description":"Fond perdu à prévoir autour du format fini."},"resolution":{"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"required":["amount","unit"],"description":"Résolution minimale du fichier fourni."},"securityMargins":{"type":"object","properties":{"amount":{"type":"number"},"unit":{"type":"string"}},"required":["amount","unit"],"description":"Marge de sécurité intérieure : aucun élément important au-delà."},"colorMode":{"type":"string","description":"Mode colorimétrique attendu pour le fichier."},"proofAvailable":{"type":"boolean","description":"Un BAT (bon à tirer) est proposé sur ce produit."},"mockupRequired":{"type":"boolean","description":"Ce produit exige une maquette avant production."}}},"printedIn":{"type":"string","description":"Lieu de production déclaré, libellé résolu (ex. « Centre-Val de Loire », « Europe »)."},"printedInDetails":{"type":"string","description":"Précisions sur la répartition de la production entre ateliers."},"images":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"url":{"type":"string"},"thumbnailUrl":{"type":"string"}},"required":["url"]},"description":"Visuels produit."},"videos":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"url":{"type":"string"},"thumbnailUrl":{"type":"string"}},"required":["url"]},"description":"Vidéos de présentation (YouTube, Vimeo, fichiers vidéo), sorties de `images`."},"templates":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"url":{"type":"string"},"thumbnailUrl":{"type":"string"}},"required":["url"]},"description":"Gabarits de mise en page téléchargeables (PDF, IDML, ZIP)."},"technicalInstructionsUrl":{"type":"string","description":"Consignes techniques de préparation du fichier (PDF)."},"canvaGuideUrl":{"type":"string","description":"Guide de préparation du fichier sous Canva (PDF)."},"instructionsForUseUrl":{"type":"string","description":"Notice d’utilisation du produit (PDF)."},"productId":{"type":"number","description":"Identifiant à passer aux routes et outils de cotation (même valeur que `zeloomProductId`, son alias historique conservé pour compatibilité)."},"links":{"type":"object","properties":{"config":{"type":"string","description":"GET — espace de configuration du produit."},"quote":{"type":"string","description":"POST — devis pour une sélection."}},"required":["config","quote"]}},"required":["zeloomProductId","sku","name","productId","links"],"example":{"productId":77,"zeloomProductId":77,"sku":"CARTE_STANDARD","name":"Carte de visite Standard","url":"https://www.fulfiller.com/carte-de-visite-standard.html","category":"cartes_visite","miniDescription":"De 50 à 50 000 exemplaires. Livraison dès 24H Chrono.","description":"Impression de carte de visite pas cher\n\nCartes de visite quadri recto sans finition sur papier épais 350g/m2 en 8,5 x 5,5 cm.","keyPoints":["Dès 24H Chrono","De 50 à 50 000 ex","Avec ou sans finitions"],"faq":[{"question":"Quel est le format standard d’une carte de visite ?","answer":"Le format le plus répandu est le 85 x 54 mm."}],"printSpecs":{"bleed":{"amount":3,"unit":"MILLIMETER"},"resolution":{"amount":300,"unit":"DPI"},"securityMargins":{"amount":3,"unit":"MILLIMETER"},"colorMode":"CMJN (quadri)","proofAvailable":true},"printedIn":"Europe","templates":[{"label":"Standard 85 x 55mm","url":"https://www.fulfiller.com/assets/website/gabarits/carte_de_visite_standard/standard_85x55mm.zip"}],"technicalInstructionsUrl":"https://www.fulfiller.com/assets/website/consignes_techniques/CT_carte_de_visite_standard.pdf","links":{"config":"https://www.fulfiller.com/agenticopenapi/products/77/config","quote":"https://www.fulfiller.com/agenticopenapi/products/77/quote"}}}}}},"400":{"description":"Paramètres invalides (aucun / plusieurs / SKU non conforme), `PAGE_URL_NOT_SUPPORTED` (l’URL n’est pas une page produit du storefront), ou `PRODUCT_AMBIGUOUS` (productId porté par plusieurs fiches : le message les cite).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"SKU inconnu du PIM, non cotable, ou page sans SKU déclaré.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Source catalogue (storefront / Akeneo) temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/checkout":{"post":{"operationId":"createCheckout","summary":"Crée un panier","description":"Crée un panier anonyme (id-capacité UUID) avec des lignes initiales optionnelles (0 à 5, cotées via `/products/{id}/quote`). Panier vide = 201 légitime. `Idempotency-Key` optionnel (voir sa description) : absent, panier neuf ; rejeu à même clé/corps ⇒ 200.","parameters":[{"schema":{"type":"string","format":"uuid","description":"OPTIONNEL. Si présent, DOIT être un UUID (capacité anonyme, même rang que l’id de panier). Absent : comportement non idempotent inchangé. Même clé + même corps ⇒ 200 + `Idempotency-Replayed: true` (panier dans son état courant). Même clé, corps différent ⇒ 409 IDEMPOTENCY_KEY_REUSED. Création concurrente ⇒ 409 IDEMPOTENCY_IN_PROGRESS (voir `Retry-After`). IGNORÉ sur `PATCH`/`DELETE`.","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"required":false,"description":"OPTIONNEL. Si présent, DOIT être un UUID (capacité anonyme, même rang que l’id de panier). Absent : comportement non idempotent inchangé. Même clé + même corps ⇒ 200 + `Idempotency-Replayed: true` (panier dans son état courant). Même clé, corps différent ⇒ 409 IDEMPOTENCY_KEY_REUSED. Création concurrente ⇒ 409 IDEMPOTENCY_IN_PROGRESS (voir `Retry-After`). IGNORÉ sur `PATCH`/`DELETE`.","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"example":1001},"sku":{"type":"string","minLength":1,"maxLength":128,"description":"SKU de la fiche. Facultatif : absent, il est résolu depuis `productId` ; si plusieurs fiches vendent ce produit, 400 `PRODUCT_AMBIGUOUS` cite leurs SKU.","example":"CARTE_STANDARD"},"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","maxLength":128},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string","maxLength":128},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string","maxLength":128},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]},"maxItems":50},"preset":{"type":"string","minLength":1,"maxLength":128,"description":"Code ou nom d’un preset de la configuration (`presets` : « MOINS_CHER », « Top vente »…) : ses valeurs servent de base, les `options` données explicitement l’emportent. Un preset inconnu est signalé (SELECTION_UNKNOWN) et ignoré."}},"required":["options"]},"quantity":{"type":"integer","minimum":1,"maximum":1000,"default":1,"description":"Nombre de LOTS identiques au prix unitaire figé — PAS un palier de volume. Borné à 1000. Pour un vrai changement de volume (autre tranche de prix), re-cotez avec une autre QUANTITY dans `selection` puis remplacez la ligne."}},"required":["productId","selection"]},"maxItems":5}}}}}},"responses":{"200":{"description":"Rejeu d’idempotence (point 1) : même `Idempotency-Key` et même corps qu’une création déjà aboutie — le panier renvoyé est dans son état COURANT (voir `Idempotency-Replayed: true`).","headers":{"Idempotency-Replayed":{"schema":{"type":"string","description":"Présent (`true`) UNIQUEMENT sur un REJEU (même `Idempotency-Key` + même corps qu’une création déjà aboutie) : le panier renvoyé est dans son état COURANT, pas une copie figée du 201 d’origine (un `PATCH` entre-temps y est visible)."},"required":false,"description":"Présent (`true`) UNIQUEMENT sur un REJEU (même `Idempotency-Key` + même corps qu’une création déjà aboutie) : le panier renvoyé est dans son état COURANT, pas une copie figée du 201 d’origine (un `PATCH` entre-temps y est visible)."}},"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","description":"Vocabulaire OUVERT (décision n°2) : aujourd’hui seule la valeur \"cart\" existe (panier anonyme, pré-authentification). D’autres valeurs apparaîtront avec la commande (identité, paiement) sans changement cassant — un agent qui branche sur ce champ continue de fonctionner."},"lines":{"type":"array","items":{"type":"object","properties":{"lineId":{"type":"string"},"productId":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string","description":"Page produit publique, FIGÉE à l’ajout comme le prix et le nom. Absente si le lien n’a pas été résolu à ce moment-là ; jamais recalculée à la lecture (un panier reste consultable storefront à terre)."},"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"],"description":"Sélection RÉSOLUE (défauts + ajustements amont), figée à l’ajout."},"quantity":{"type":"integer","description":"Nombre de LOTS identiques au prix unitaire figé — PAS un palier de volume : 2×50 exemplaires n'a pas le même prix qu'1×100. Un vrai changement de volume passe par une re-cotation avec une autre QUANTITY dans `selection`, puis un remplacement de la ligne."},"unitPriceHt":{"type":"number","description":"Prix unitaire HT FIGÉ à l’ajout, promotion déduite (= `devis.finalPriceHt`)."},"lineTotalHt":{"type":"number"},"devis":{"type":"object","properties":{"finalPriceHt":{"type":"number","description":"Prix HT À PAYER pour la sélection : promotion déduite s’il y en a une, sinon égal à `priceHt`. C’est LE prix à annoncer. HT, hors TVA, hors port."},"priceHt":{"type":"number","description":"Prix HT CATALOGUE de la sélection, AVANT promotion. Quand `promo` est présent, c’est le prix BARRÉ : le prix à payer est `finalPriceHt`."},"currency":{"type":"string"},"deliveryDays":{"type":"number"},"atelier":{"type":"string"},"promo":{"type":"object","nullable":true,"properties":{"priceHt":{"type":"number","description":"Prix HT remisé : égal à `finalPriceHt`."},"percent":{"type":"number","description":"Remise en pourcentage du prix catalogue."},"name":{"type":"string","description":"Libellé de la promotion ; chaîne vide si l’amont n’en donne pas."},"endsAt":{"type":"string","description":"Fin de la promotion, si connue."}},"required":["priceHt","percent","name"],"description":"Promotion appliquée à ce devis, ou `null`."},"stockAvailable":{"type":"boolean"}},"required":["priceHt","currency","stockAvailable"]},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]},"description":"Alertes de cotation FIGÉES à l’ajout. Une alerte `severity: \"error\"` signale une configuration non commandable en l'état ; elle n'empêche PAS l'ajout au panier (un panier n'est pas une commande — le jalon commande refusera de commander une ligne en `error`)."},"pricedAt":{"type":"string","description":"Horodatage ISO 8601 de la cotation source de cette ligne (snapshot, jamais re-coté à la lecture) — indique ce qu’il faut re-coter si le panier vieillit."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["lineId","productId","sku","name","selection","quantity","unitPriceHt","lineTotalHt","devis","alerts","pricedAt","requiresReview","reviewReasons"]}},"lineCount":{"type":"integer","description":"Nombre de lignes du panier."},"itemCount":{"type":"integer","description":"Somme des quantités de toutes les lignes."},"currency":{"type":"string"},"totalHt":{"type":"number","description":"Somme des `lineTotalHt` (promo appliquée, arrondi 2 décimales). HT, HORS TVA, HORS PORT — PAS un total de commande. La TVA est une condition pré-prod hors de ce jalon."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"expiresAt":{"type":"string","description":"Horodatage ISO 8601 d’expiration du panier. TTL GLISSANT : reculé à chaque écriture (create/add/remove/update). Passé ce délai, le panier disparaît silencieusement (404 CHECKOUT_NOT_FOUND à la lecture, jamais distingué de « jamais existé »)."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel. À la racine : OU des entrées/lignes."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`. À la racine : union DÉDUPLIQUÉE des raisons des entrées/lignes, dans leur ordre d’apparition, tronquée à 10 (aucune autre source ne contribue à la racine)."}},"required":["id","status","lines","lineCount","itemCount","totalHt","createdAt","updatedAt","expiresAt","requiresReview","reviewReasons"]}}}},"201":{"description":"Panier créé (vide ou avec ses lignes initiales).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","description":"Vocabulaire OUVERT (décision n°2) : aujourd’hui seule la valeur \"cart\" existe (panier anonyme, pré-authentification). D’autres valeurs apparaîtront avec la commande (identité, paiement) sans changement cassant — un agent qui branche sur ce champ continue de fonctionner."},"lines":{"type":"array","items":{"type":"object","properties":{"lineId":{"type":"string"},"productId":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string","description":"Page produit publique, FIGÉE à l’ajout comme le prix et le nom. Absente si le lien n’a pas été résolu à ce moment-là ; jamais recalculée à la lecture (un panier reste consultable storefront à terre)."},"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"],"description":"Sélection RÉSOLUE (défauts + ajustements amont), figée à l’ajout."},"quantity":{"type":"integer","description":"Nombre de LOTS identiques au prix unitaire figé — PAS un palier de volume : 2×50 exemplaires n'a pas le même prix qu'1×100. Un vrai changement de volume passe par une re-cotation avec une autre QUANTITY dans `selection`, puis un remplacement de la ligne."},"unitPriceHt":{"type":"number","description":"Prix unitaire HT FIGÉ à l’ajout, promotion déduite (= `devis.finalPriceHt`)."},"lineTotalHt":{"type":"number"},"devis":{"type":"object","properties":{"finalPriceHt":{"type":"number","description":"Prix HT À PAYER pour la sélection : promotion déduite s’il y en a une, sinon égal à `priceHt`. C’est LE prix à annoncer. HT, hors TVA, hors port."},"priceHt":{"type":"number","description":"Prix HT CATALOGUE de la sélection, AVANT promotion. Quand `promo` est présent, c’est le prix BARRÉ : le prix à payer est `finalPriceHt`."},"currency":{"type":"string"},"deliveryDays":{"type":"number"},"atelier":{"type":"string"},"promo":{"type":"object","nullable":true,"properties":{"priceHt":{"type":"number","description":"Prix HT remisé : égal à `finalPriceHt`."},"percent":{"type":"number","description":"Remise en pourcentage du prix catalogue."},"name":{"type":"string","description":"Libellé de la promotion ; chaîne vide si l’amont n’en donne pas."},"endsAt":{"type":"string","description":"Fin de la promotion, si connue."}},"required":["priceHt","percent","name"],"description":"Promotion appliquée à ce devis, ou `null`."},"stockAvailable":{"type":"boolean"}},"required":["priceHt","currency","stockAvailable"]},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]},"description":"Alertes de cotation FIGÉES à l’ajout. Une alerte `severity: \"error\"` signale une configuration non commandable en l'état ; elle n'empêche PAS l'ajout au panier (un panier n'est pas une commande — le jalon commande refusera de commander une ligne en `error`)."},"pricedAt":{"type":"string","description":"Horodatage ISO 8601 de la cotation source de cette ligne (snapshot, jamais re-coté à la lecture) — indique ce qu’il faut re-coter si le panier vieillit."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["lineId","productId","sku","name","selection","quantity","unitPriceHt","lineTotalHt","devis","alerts","pricedAt","requiresReview","reviewReasons"]}},"lineCount":{"type":"integer","description":"Nombre de lignes du panier."},"itemCount":{"type":"integer","description":"Somme des quantités de toutes les lignes."},"currency":{"type":"string"},"totalHt":{"type":"number","description":"Somme des `lineTotalHt` (promo appliquée, arrondi 2 décimales). HT, HORS TVA, HORS PORT — PAS un total de commande. La TVA est une condition pré-prod hors de ce jalon."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"expiresAt":{"type":"string","description":"Horodatage ISO 8601 d’expiration du panier. TTL GLISSANT : reculé à chaque écriture (create/add/remove/update). Passé ce délai, le panier disparaît silencieusement (404 CHECKOUT_NOT_FOUND à la lecture, jamais distingué de « jamais existé »)."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel. À la racine : OU des entrées/lignes."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`. À la racine : union DÉDUPLIQUÉE des raisons des entrées/lignes, dans leur ordre d’apparition, tronquée à 10 (aucune autre source ne contribue à la racine)."}},"required":["id","status","lines","lineCount","itemCount","totalHt","createdAt","updatedAt","expiresAt","requiresReview","reviewReasons"]}}}},"400":{"description":"Corps invalide, id/sélection invalide, SKU_PRODUCT_MISMATCH, ou en-tête `Idempotency-Key` présent mais non conforme (doit être un UUID).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"PRODUCT_NOT_FOUND : un SKU d’une ligne initiale est inconnu du PIM.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"INVALID_CHECKOUT_OPERATION (devise hétérogène entre les lignes initiales) ; ou, avec `Idempotency-Key` : IDEMPOTENCY_IN_PROGRESS (création concurrente à même clé — voir `Retry-After`) ou IDEMPOTENCY_KEY_REUSED (même clé, corps différent).","headers":{"Retry-After":{"schema":{"type":"string","description":"Présent UNIQUEMENT sur 409 IDEMPOTENCY_IN_PROGRESS. Toujours `1` (secondes) : jamais d’attente ni de polling serveur, un retry immédiat est légitime."},"required":false,"description":"Présent UNIQUEMENT sur 409 IDEMPOTENCY_IN_PROGRESS. Toujours `1` (secondes) : jamais d’attente ni de polling serveur, un retry immédiat est légitime."}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont (ZeLoom, Akeneo) ou Redis temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}},"/checkout/{id}":{"get":{"operationId":"getCheckout","summary":"Lit un panier","description":"Renvoie le panier tel que figé (SNAPSHOT) : aucune re-cotation à la lecture. `pricedAt` par ligne et `expiresAt` sur le panier indiquent l’âge du prix affiché.","parameters":[{"schema":{"type":"string","format":"uuid","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Panier (snapshot, non re-coté à la lecture).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","description":"Vocabulaire OUVERT (décision n°2) : aujourd’hui seule la valeur \"cart\" existe (panier anonyme, pré-authentification). D’autres valeurs apparaîtront avec la commande (identité, paiement) sans changement cassant — un agent qui branche sur ce champ continue de fonctionner."},"lines":{"type":"array","items":{"type":"object","properties":{"lineId":{"type":"string"},"productId":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string","description":"Page produit publique, FIGÉE à l’ajout comme le prix et le nom. Absente si le lien n’a pas été résolu à ce moment-là ; jamais recalculée à la lecture (un panier reste consultable storefront à terre)."},"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"],"description":"Sélection RÉSOLUE (défauts + ajustements amont), figée à l’ajout."},"quantity":{"type":"integer","description":"Nombre de LOTS identiques au prix unitaire figé — PAS un palier de volume : 2×50 exemplaires n'a pas le même prix qu'1×100. Un vrai changement de volume passe par une re-cotation avec une autre QUANTITY dans `selection`, puis un remplacement de la ligne."},"unitPriceHt":{"type":"number","description":"Prix unitaire HT FIGÉ à l’ajout, promotion déduite (= `devis.finalPriceHt`)."},"lineTotalHt":{"type":"number"},"devis":{"type":"object","properties":{"finalPriceHt":{"type":"number","description":"Prix HT À PAYER pour la sélection : promotion déduite s’il y en a une, sinon égal à `priceHt`. C’est LE prix à annoncer. HT, hors TVA, hors port."},"priceHt":{"type":"number","description":"Prix HT CATALOGUE de la sélection, AVANT promotion. Quand `promo` est présent, c’est le prix BARRÉ : le prix à payer est `finalPriceHt`."},"currency":{"type":"string"},"deliveryDays":{"type":"number"},"atelier":{"type":"string"},"promo":{"type":"object","nullable":true,"properties":{"priceHt":{"type":"number","description":"Prix HT remisé : égal à `finalPriceHt`."},"percent":{"type":"number","description":"Remise en pourcentage du prix catalogue."},"name":{"type":"string","description":"Libellé de la promotion ; chaîne vide si l’amont n’en donne pas."},"endsAt":{"type":"string","description":"Fin de la promotion, si connue."}},"required":["priceHt","percent","name"],"description":"Promotion appliquée à ce devis, ou `null`."},"stockAvailable":{"type":"boolean"}},"required":["priceHt","currency","stockAvailable"]},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]},"description":"Alertes de cotation FIGÉES à l’ajout. Une alerte `severity: \"error\"` signale une configuration non commandable en l'état ; elle n'empêche PAS l'ajout au panier (un panier n'est pas une commande — le jalon commande refusera de commander une ligne en `error`)."},"pricedAt":{"type":"string","description":"Horodatage ISO 8601 de la cotation source de cette ligne (snapshot, jamais re-coté à la lecture) — indique ce qu’il faut re-coter si le panier vieillit."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["lineId","productId","sku","name","selection","quantity","unitPriceHt","lineTotalHt","devis","alerts","pricedAt","requiresReview","reviewReasons"]}},"lineCount":{"type":"integer","description":"Nombre de lignes du panier."},"itemCount":{"type":"integer","description":"Somme des quantités de toutes les lignes."},"currency":{"type":"string"},"totalHt":{"type":"number","description":"Somme des `lineTotalHt` (promo appliquée, arrondi 2 décimales). HT, HORS TVA, HORS PORT — PAS un total de commande. La TVA est une condition pré-prod hors de ce jalon."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"expiresAt":{"type":"string","description":"Horodatage ISO 8601 d’expiration du panier. TTL GLISSANT : reculé à chaque écriture (create/add/remove/update). Passé ce délai, le panier disparaît silencieusement (404 CHECKOUT_NOT_FOUND à la lecture, jamais distingué de « jamais existé »)."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel. À la racine : OU des entrées/lignes."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`. À la racine : union DÉDUPLIQUÉE des raisons des entrées/lignes, dans leur ordre d’apparition, tronquée à 10 (aucune autre source ne contribue à la racine)."}},"required":["id","status","lines","lineCount","itemCount","totalHt","createdAt","updatedAt","expiresAt","requiresReview","reviewReasons"]}}}},"400":{"description":"Id de panier non UUID.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"CHECKOUT_NOT_FOUND : panier inconnu OU expiré (jamais distingué).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont (ZeLoom, Akeneo) ou Redis temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}},"patch":{"operationId":"patchCheckout","summary":"Modifie un panier","description":"Applique, DANS CET ORDRE : retraits (`remove`), changements de quantité (`update`, même prix, pas de recotation), puis ajouts (`add`, cotés). Bornes : 5 ajouts, 20 retraits/updates. `Idempotency-Key` IGNORÉ ici (un ajout rejoué duplique la ligne).","parameters":[{"schema":{"type":"string","format":"uuid","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"remove":{"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"maxItems":20},"update":{"type":"array","items":{"type":"object","properties":{"lineId":{"type":"string","minLength":1,"maxLength":64},"quantity":{"type":"integer","minimum":1,"maximum":1000}},"required":["lineId","quantity"]},"maxItems":20},"add":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"integer","minimum":0,"exclusiveMinimum":true,"example":1001},"sku":{"type":"string","minLength":1,"maxLength":128,"description":"SKU de la fiche. Facultatif : absent, il est résolu depuis `productId` ; si plusieurs fiches vendent ce produit, 400 `PRODUCT_AMBIGUOUS` cite leurs SKU.","example":"CARTE_STANDARD"},"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string","maxLength":128},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string","maxLength":128},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string","maxLength":128},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]},"maxItems":50},"preset":{"type":"string","minLength":1,"maxLength":128,"description":"Code ou nom d’un preset de la configuration (`presets` : « MOINS_CHER », « Top vente »…) : ses valeurs servent de base, les `options` données explicitement l’emportent. Un preset inconnu est signalé (SELECTION_UNKNOWN) et ignoré."}},"required":["options"]},"quantity":{"type":"integer","minimum":1,"maximum":1000,"default":1,"description":"Nombre de LOTS identiques au prix unitaire figé — PAS un palier de volume. Borné à 1000. Pour un vrai changement de volume (autre tranche de prix), re-cotez avec une autre QUANTITY dans `selection` puis remplacez la ligne."}},"required":["productId","selection"]},"maxItems":5}}}}}},"responses":{"200":{"description":"Panier après application (remove → update → add).","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","description":"Vocabulaire OUVERT (décision n°2) : aujourd’hui seule la valeur \"cart\" existe (panier anonyme, pré-authentification). D’autres valeurs apparaîtront avec la commande (identité, paiement) sans changement cassant — un agent qui branche sur ce champ continue de fonctionner."},"lines":{"type":"array","items":{"type":"object","properties":{"lineId":{"type":"string"},"productId":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"url":{"type":"string","description":"Page produit publique, FIGÉE à l’ajout comme le prix et le nom. Absente si le lien n’a pas été résolu à ce moment-là ; jamais recalculée à la lecture (un panier reste consultable storefront à terre)."},"selection":{"type":"object","properties":{"options":{"type":"array","items":{"type":"object","properties":{"option":{"type":"string"},"type":{"type":"string","enum":["QUANTITY","DELIVERY_TIME","LIST_OF_VALUES","SIZE"]},"value":{"type":"string"},"quantity":{"type":"integer","minimum":0,"exclusiveMinimum":true},"size":{"type":"object","properties":{"code":{"type":"string"},"width":{"type":"number","minimum":0,"exclusiveMinimum":true},"height":{"type":"number","minimum":0,"exclusiveMinimum":true}},"required":["code"]}},"required":["option","type"]}}},"required":["options"],"description":"Sélection RÉSOLUE (défauts + ajustements amont), figée à l’ajout."},"quantity":{"type":"integer","description":"Nombre de LOTS identiques au prix unitaire figé — PAS un palier de volume : 2×50 exemplaires n'a pas le même prix qu'1×100. Un vrai changement de volume passe par une re-cotation avec une autre QUANTITY dans `selection`, puis un remplacement de la ligne."},"unitPriceHt":{"type":"number","description":"Prix unitaire HT FIGÉ à l’ajout, promotion déduite (= `devis.finalPriceHt`)."},"lineTotalHt":{"type":"number"},"devis":{"type":"object","properties":{"finalPriceHt":{"type":"number","description":"Prix HT À PAYER pour la sélection : promotion déduite s’il y en a une, sinon égal à `priceHt`. C’est LE prix à annoncer. HT, hors TVA, hors port."},"priceHt":{"type":"number","description":"Prix HT CATALOGUE de la sélection, AVANT promotion. Quand `promo` est présent, c’est le prix BARRÉ : le prix à payer est `finalPriceHt`."},"currency":{"type":"string"},"deliveryDays":{"type":"number"},"atelier":{"type":"string"},"promo":{"type":"object","nullable":true,"properties":{"priceHt":{"type":"number","description":"Prix HT remisé : égal à `finalPriceHt`."},"percent":{"type":"number","description":"Remise en pourcentage du prix catalogue."},"name":{"type":"string","description":"Libellé de la promotion ; chaîne vide si l’amont n’en donne pas."},"endsAt":{"type":"string","description":"Fin de la promotion, si connue."}},"required":["priceHt","percent","name"],"description":"Promotion appliquée à ce devis, ou `null`."},"stockAvailable":{"type":"boolean"}},"required":["priceHt","currency","stockAvailable"]},"alerts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’alerte. Résolution locale de la sélection : SELECTION_UNKNOWN (option ou valeur introuvable), SELECTION_AMBIGUOUS (valeur correspondant à plusieurs entrées), SELECTION_DUPLICATE_OPTION, QUANTITY_OUT_OF_RANGE (hors de toute plage — non résolu, défaut amont appliqué), QUANTITY_ADJUSTED (ramenée à la quantité commandable la plus proche du pas), SIZE_ADJUSTED (dimensions arrondies au pas de saisie du gabarit), SIZE_OUT_OF_RANGE (erreur : dimensions hors des bornes du gabarit, que la cotation refuse), OPTION_TYPE_UNSUPPORTED (option décrite mais non sélectionnable), SELECTION_INTERPRETED (info : valeur partielle rattachée à la seule valeur qui la contient). Décision du configurateur amont : SELECTION_ADJUSTED_UPSTREAM et SELECTION_DROPPED_UPSTREAM (il a substitué ou retiré ce qu’on lui a soumis — c’est SA valeur qui est cotée), SELECTION_NOT_ORDERABLE (configuration non commandable en l’état). Tout AUTRE valeur est un passe-plat d’alerte amont et peut être un code d’option propre au produit, pas un genre d’alerte."},"message":{"type":"string","description":"Message lisible, en français, destiné à être montré ou cité tel quel."},"severity":{"type":"string","enum":["info","warning","error"],"description":"info : sans conséquence sur le prix. warning : la sélection cotée DIFFÈRE de la demande — le prix est bon pour ce qui est décrit dans `resolvedSelection`, pas pour la demande initiale. error : la configuration n’est pas commandable en l’état."}},"required":["code","message","severity"]},"description":"Alertes de cotation FIGÉES à l’ajout. Une alerte `severity: \"error\"` signale une configuration non commandable en l'état ; elle n'empêche PAS l'ajout au panier (un panier n'est pas une commande — le jalon commande refusera de commander une ligne en `error`)."},"pricedAt":{"type":"string","description":"Horodatage ISO 8601 de la cotation source de cette ligne (snapshot, jamais re-coté à la lecture) — indique ce qu’il faut re-coter si le panier vieillit."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`."}},"required":["lineId","productId","sku","name","selection","quantity","unitPriceHt","lineTotalHt","devis","alerts","pricedAt","requiresReview","reviewReasons"]}},"lineCount":{"type":"integer","description":"Nombre de lignes du panier."},"itemCount":{"type":"integer","description":"Somme des quantités de toutes les lignes."},"currency":{"type":"string"},"totalHt":{"type":"number","description":"Somme des `lineTotalHt` (promo appliquée, arrondi 2 décimales). HT, HORS TVA, HORS PORT — PAS un total de commande. La TVA est une condition pré-prod hors de ce jalon."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"expiresAt":{"type":"string","description":"Horodatage ISO 8601 d’expiration du panier. TTL GLISSANT : reculé à chaque écriture (create/add/remove/update). Passé ce délai, le panier disparaît silencieusement (404 CHECKOUT_NOT_FOUND à la lecture, jamais distingué de « jamais existé »)."},"requiresReview":{"type":"boolean","description":"`true` signifie que la sélection COTÉE diffère de la sélection DEMANDÉE (doublon, valeur inconnue, quantité ajustée, substitution amont…) : le prix rendu est celui de la sélection cotée. Lisez `alerts` pour le détail. `false` au nominal — champ TOUJOURS présent, jamais optionnel. À la racine : OU des entrées/lignes."},"reviewReasons":{"type":"array","items":{"type":"string"},"description":"Codes déclencheurs de `requiresReview`, DÉDUPLIQUÉS, dans l’ordre d’apparition, max 10. Vide au nominal. Vocabulaire FERMÉ (12 valeurs) : nos propres codes (SELECTION_DUPLICATE_OPTION, SELECTION_UNKNOWN, SELECTION_AMBIGUOUS, QUANTITY_OUT_OF_RANGE, QUANTITY_ADJUSTED, SIZE_ADJUSTED, SIZE_OUT_OF_RANGE, SELECTION_DROPPED_UPSTREAM, SELECTION_ADJUSTED_UPSTREAM, SELECTION_NOT_ORDERABLE) et deux SENTINELLES — UPSTREAM_ALERT (une alerte amont `severity: error` de code hors de ce vocabulaire) et PIVOT_ENTRY_ERROR (une entrée pivot en `error` de code hors de ce vocabulaire) — dont le CODE D’ORIGINE n’est jamais republié ici (non borné, piloté par l’amont) : le détail reste dans `alerts`/`error`. À la racine : union DÉDUPLIQUÉE des raisons des entrées/lignes, dans leur ordre d’apparition, tronquée à 10 (aucune autre source ne contribue à la racine)."}},"required":["id","status","lines","lineCount","itemCount","totalHt","createdAt","updatedAt","expiresAt","requiresReview","reviewReasons"]}}}},"400":{"description":"Corps/id invalide, ou SKU_PRODUCT_MISMATCH sur un ajout.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"CHECKOUT_NOT_FOUND (inconnu, ou expiré entre-temps) ou PRODUCT_NOT_FOUND sur un ajout.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"INVALID_CHECKOUT_OPERATION : lineId inconnu, devise hétérogène, plafond de lignes.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont (ZeLoom, Akeneo) ou Redis temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}},"delete":{"operationId":"deleteCheckout","summary":"Supprime un panier","description":"Supprime DÉFINITIVEMENT un panier : l’UUID n’est jamais réémis. 204 sans corps si le panier existait ; 404 CHECKOUT_NOT_FOUND si inconnu ou expiré (jamais un 204 systématique). Une clé d’idempotence pointant ce panier redevient neuve ensuite. `Idempotency-Key` IGNORÉ ici.","parameters":[{"schema":{"type":"string","format":"uuid","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Panier supprimé (aucun corps)."},"400":{"description":"Id de panier non UUID.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"404":{"description":"CHECKOUT_NOT_FOUND : panier inconnu OU expiré (jamais distingué, décision C2).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Quota par IP dépassé (RATE_LIMITED) — voir l’en-tête Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Service amont (ZeLoom, Akeneo) ou Redis temporairement indisponible.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Genre d’erreur, sur lequel un client peut brancher. Requête : VALIDATION_ERROR (entrée non conforme, voir `details`), INVALID_SELECTION, QUANTITY_OUT_OF_RANGE, PAGE_URL_NOT_SUPPORTED (l’URL n’est pas une page produit du storefront), PRICING_CONTEXT_UNSUPPORTED. Ressource : PRODUCT_NOT_FOUND. Routage : NOT_FOUND (le chemin n’existe pas) et METHOD_NOT_ALLOWED (le chemin existe, pas la méthode — les verbes acceptés sont dans l’en-tête `Allow` ET dans le message) ; ne les confondez pas avec PRODUCT_NOT_FOUND, qui porte sur la ressource et non sur le chemin. Quota : RATE_LIMITED, voir `Retry-After`. Amont : UPSTREAM_UNAVAILABLE — jamais un échec définitif, réessayez."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string","description":"Chemin du champ en cause, notation pointée/crochets (ex. `selection.options[0].quantity`) ; `$body`/`$query`/`$param`/`$header`/`$cookie`/`$form` si le chemin zod est vide (échec sur la cible elle-même, ex. corps JSON illisible). Tronqué à 200 caractères."},"reason":{"type":"string","description":"Code MACHINE sur lequel un agent peut brancher. Pour `error.code = VALIDATION_ERROR` : code d'issue zod verbatim, ou `malformed_json` pour un corps JSON illisible/tronqué."},"message":{"type":"string","description":"Message TECHNIQUE, en anglais, verbatim (message zod) pour un code d’issue zod. Sur `malformed_json`, `message` est PRÉSENT mais FIXE et neutre, en français (pas un message zod, le corps n’a pas pu être parsé). Tronqué à 200 caractères. Jamais la valeur reçue."}},"required":["path","reason"]},"maxItems":10,"description":"Détails structurés (point 3 du brief fix/multiple-fixes), OPTIONNEL et borné à 10 entrées. Présent aujourd'hui sur `VALIDATION_ERROR` uniquement ; absent sur 404/409/429/503."}},"required":["code","message"]}},"required":["error"]}}}}}}}}}