Aller au contenu

Documentation de l'API

Tout ce que la plateforme expose : transmission des commandes, lecture et réponse aux avis, affichage public, et branchement d'un assistant IA. Dix-neuf points d'entrée, trois canaux, une seule clé.

Un marchand n'a normalement rien à coder. Les modules PrestaShop et WooCommerce font tout ce qui est décrit ici — voir les modules. Cette page s'adresse aux développeurs qui intègrent une plateforme sans module, un ERP, un CRM ou un outil interne.

1. Introduction

La plateforme expose trois canaux distincts. Ils ne partagent ni le même public, ni le même régime d'authentification, ni les mêmes limites. Choisir le bon est la première décision d'une intégration.

Canal Préfixe Pour qui Authentification
API CMS /api/v1/cms/ Modules e-commerce, ERP, CRM, outils internes Clé d'API + signature HMAC en écriture
API publique /api/v1/public/ Widgets d'affichage, JavaScript de boutique Aucune — débit limité, CORS restreint
MCP /api/v1/mcp Assistants IA (Claude, ChatGPT, autres) Même clé, même signature que l'API CMS

Avant d'écrire une ligne de code : vérifiez qu'un module ne suffit pas

Les modules PrestaShop et WooCommerce font l'intégralité de ce que décrit cette page : ils transmettent les commandes au bon moment, posent le script des widgets dans le thème, placent les étoiles sur les fiches produit et le bloc d'avis, et gèrent la signature des requêtes. Le marchand ne colle rien et n'écrit rien.

Télécharger les modules →

Cette documentation s'adresse donc à trois cas : une plateforme pour laquelle nous n'avons pas encore de module, un développement sur mesure, ou le branchement d'un outil tiers (ERP, service client, assistant IA) sur les avis déjà collectés.

Adresses de base

Toutes les URL de cette page sont relatives à l'adresse de l'API. Un module ne doit connaître qu'elle : les autres adresses lui sont renvoyées par GET /api/v1/cms/me, ce qui lui évite de les deviner et permet de les changer sans mettre à jour quoi que ce soit chez les marchands.

UsageAdresse
API (tous canaux)https://louis.guide
Espace marchandhttps://louis.guide/app
Script des widgetshttps://louis.guide/widget/v1/avis.js

Conventions

  • Format — JSON en entrée comme en sortie, encodé en UTF-8. L'en-tête Content-Type: application/json est attendu sur toute requête portant un corps.
  • Nommage — serpent minuscule (external_order_id, experienced_at), la convention dominante des API que consomment les intégrateurs PHP et JavaScript.
  • Dates — ISO 8601 avec fuseau explicite en entrée (2026-08-01T14:22:00+02:00). En sortie, les dates complètes sont au même format ; les dates publiques d'un avis sont réduites au jour (2026-08-01) parce qu'aucun widget n'affiche l'heure.
  • Montants — transmis en chaîne ("129.90") et non en nombre flottant : un centime perdu à l'arrondi sur une commande devient un écart de facturation.
  • Identifiants — les objets que nous créons portent un UUID permanent. Les vôtres (commande, produit, déclinaison) restent les vôtres : nous ne les réécrivons jamais.
  • Erreurs — toujours la même enveloppe { "error": { "code": …, "message": … } }. Le code est stable et destiné à votre programme, le message à l'humain qui débogue. Voir §10.
  • Versionnage — le /v1 du chemin est un contrat. Un champ optionnel peut y être ajouté à tout moment ; aucun champ existant ne sera renommé, supprimé ni rendu obligatoire. Une rupture partirait en /v2, l'ancienne version restant servie — les modules tournent chez les marchands et personne ne peut les mettre à jour à distance. Votre code doit donc ignorer les champs qu'il ne connaît pas plutôt qu'échouer à leur vue.

Obtenir une clé d'API

  1. Créez un compte marchand sur l'espace marchand.
  2. Confirmez l'adresse email, puis ouvrez la section des clés d'API.
  3. Notez le secret : il n'est affiché qu'une fois. Perdu, il ne se retrouve pas — une nouvelle clé se crée, l'ancienne se révoque.

Un module d'installation n'a pas besoin de cette manipulation : il ouvre lui-même une demande de raccordement que le marchand valide d'un clic. Voir §3.

2. Authentification

L'API CMS et le serveur MCP utilisent le même mécanisme : une clé qui dit qui appelle, et une signature qui prouve que l'appelant détient le secret. Ce sont deux choses distinctes.

Méthode HTTPEn-têtes exigésPourquoi
GET, HEAD X-Api-Key Une lecture ne modifie rien : la clé suffit à l'autoriser.
POST, PUT, PATCH, DELETE X-Api-Key, X-Timestamp, X-Signature Une écriture engage le marchand : elle doit être prouvée et non rejouable.

Le secret ne circule jamais

Seule la signature voyage. Cela ferme trois portes qui ne demandent aucune compromission de la boutique : la fuite passive du secret dans les journaux d'un intermédiaire, le rejeu d'une requête interceptée, et l'altération du corps en transit. En revanche, cela ne protège pas d'une boutique dont la base de données est dérobée — contre ce cas, la défense est la rotation des clés. Ne mettez jamais le secret dans une URL : les URL finissent dans les journaux de tous les intermédiaires traversés.

2.1 La signature, pas à pas

Étape 1 — Les trois en-têtes

En-têteContenu
X-Api-Key Identifiant public de la clé, tel qu'affiché dans l'espace marchand.
X-Timestamp Horodatage Unix en secondes, chiffres uniquement. Pas de millisecondes, pas de date ISO.
X-Signature Le préfixe littéral sha256= suivi du HMAC-SHA256 en hexadécimal minuscule. Le préfixe fait partie de la valeur comparée : l'omettre produit un rejet.

Étape 2 — Construire la charge à signer

Quatre morceaux concaténés sans séparateur, dans cet ordre exact :

charge = X-Timestamp
       + MÉTHODE HTTP en majuscules
       + chemin logique de la requête
       + corps brut de la requête
MorceauRègle exacte
Horodatage La chaîne identique à celle envoyée dans X-Timestamp.
Méthode POST, PUT… toujours en majuscules.
Chemin Le chemin sans schéma, sans hôte, sans chaîne de requête, commençant par / — par exemple /api/v1/cms/orders. Si l'API est servie depuis un sous-répertoire, ce préfixe d'installation n'entre pas dans la signature : il vit dans l'adresse de base, pas dans le chemin logique.
Corps La chaîne d'octets exactement telle qu'elle est envoyée. Sérialisez une fois, signez cette chaîne, envoyez cette chaîne. Corps vide → chaîne vide.

Étape 3 — Calculer

X-Signature = "sha256=" + HMAC_SHA256(charge, secret)   // hexadécimal minuscule

Étape 4 — Vérifier votre implémentation sur cet exemple

Ces valeurs sont figées et la signature affichée est réellement celle de ces données : si votre code produit autre chose, le problème est dans votre code, pas dans le nôtre.

secret       : sk_demo_3f9c1a7e5b2d48a6
X-Timestamp  : 1786000000
méthode      : POST
chemin       : /api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response
corps        : {"content":"Merci pour votre retour !"}

Charge à signer (une seule ligne, aucun espace ajouté) :

1786000000POST/api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response{"content":"Merci pour votre retour !"}

Résultat attendu :

X-Signature: sha256=8a9c0de10516226f965465704902e00c666892b483b45b361f4536a6b2ee36e9

Le même calcul en une ligne de shell :

printf '%s' '1786000000POST/api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response{"content":"Merci pour votre retour !"}' \
  | openssl dgst -sha256 -hmac 'sk_demo_3f9c1a7e5b2d48a6' -r

printf et non echo : ce dernier ajoute un saut de ligne final, qui change la signature.

Étape 5 — Un appel complet en curl

SECRET='sk_demo_3f9c1a7e5b2d48a6'
CLE='ak_demo_5c2f81b0'
TS=$(date +%s)
CHEMIN='/api/v1/cms/orders'
CORPS='{"external_order_id":"CMD-1042","experienced_at":"2026-08-01T14:22:00+02:00","customer":{"email":"claire.martin@exemple.fr","first_name":"Claire","country":"FR","locale":"fr"},"source":{"platform":"custom","plugin_version":"1.0.0"}}'

SIG=$(printf '%s' "$TS""POST""$CHEMIN""$CORPS" \
      | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)

curl -X POST 'https://louis.guide/api/v1/cms/orders' \
  -H "X-Api-Key: $CLE" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: sha256=$SIG" \
  -H 'Content-Type: application/json' \
  --data-raw "$CORPS"

--data-raw et non --data : le second interprète certains caractères et peut modifier le corps envoyé, donc invalider la signature.

2.2 Exemple en PHP

Le client minimal, sans dépendance. C'est la même mécanique que celle des modules PrestaShop et WooCommerce, réduite à l'essentiel.

<?php

final class ClientGuideLouis
{
    public function __construct(
        private string $baseUrl,   // ex. 'https://louis.guide'
        private string $apiKey,
        private string $secret,
    ) {
    }

    /**
     * @param array<string, mixed> $donnees
     *
     * @return array{status: int, body: array<string, mixed>}
     */
    public function post(string $chemin, array $donnees): array
    {
        // ON SÉRIALISE UNE SEULE FOIS. La chaîne signée et la chaîne envoyée
        // doivent être le même objet : réencoder juste avant l'envoi produit
        // tôt ou tard une différence d'échappement, donc un 401 inexplicable.
        $corps = json_encode($donnees, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        $timestamp = (string) time();

        // Le chemin LOGIQUE, sans l'adresse de base : c'est lui qui est signé.
        $chemin = '/' . ltrim($chemin, '/');

        $signature = hash_hmac(
            'sha256',
            $timestamp . 'POST' . $chemin . $corps,
            $this->secret,
        );

        $ch = curl_init(rtrim($this->baseUrl, '/') . $chemin);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $corps,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Accept: application/json',
                'X-Api-Key: ' . $this->apiKey,
                'X-Timestamp: ' . $timestamp,
                'X-Signature: sha256=' . $signature,
            ],
        ]);

        $reponse = (string) curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return ['status' => $status, 'body' => json_decode($reponse, true) ?: []];
    }
}

2.3 Les quatre pièges

Quatre causes expliquent la quasi-totalité des signature_mismatch. Elles se ressemblent toutes vues de l'extérieur — d'où l'intérêt de les écarter dans cet ordre.

  1. Le corps a été réencodé après la signature. Le cas le plus fréquent, et le plus difficile à voir : un tableau sérialisé deux fois donne deux chaînes différentes dès qu'il contient un accent ou une barre oblique (JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES, ordre des clés). Signez la chaîne, envoyez cette chaîne, ne la reconstruisez jamais.
  2. Le chemin signé porte un préfixe qu'il ne devrait pas avoir. Le chemin signé est /api/v1/cms/orders, même si l'API est servie depuis https://exemple.fr/plateforme/api/v1/cms/orders. Le préfixe d'installation appartient à l'adresse de base. À l'inverse, ne signez pas non plus l'URL complète avec son schéma et son hôte.
  3. L'horloge du serveur dérive. Tolérance : 300 secondes d'écart, dans un sens comme dans l'autre. Au-delà, la réponse est timestamp_out_of_range et son message indique l'écart mesuré en secondes — l'information exacte à donner à votre hébergeur. Ce cas se manifeste souvent par une intégration qui « fonctionnait hier ».
  4. La méthode ou le préfixe manquent. La méthode entre dans la charge en majuscules, et la valeur de X-Signature commence par sha256=. Un HMAC nu, sans préfixe, est rejeté.

Ce que la chaîne de requête ne fait pas

Les paramètres d'URL (?page=2) n'entrent pas dans la charge signée : seul le chemin y figure. Ils sont sans conséquence en pratique, les endpoints signés étant tous des écritures qui portent leurs paramètres dans le corps — mais une implémentation qui les ajouterait à la charge échouerait.

Rejeu et fenêtre de validité

La charge signée couvre l'horodatage, la méthode, le chemin et le corps. Omettre l'un d'eux ouvrirait une faille : sans le chemin, une signature valide pour POST /orders serait rejouable sur DELETE /orders ; sans l'horodatage, la requête serait rejouable indéfiniment.

La fenêtre de 300 secondes est ce qui borne le rejeu : une requête interceptée ne peut être réémise au-delà. Il n'y a pas de dictionnaire de signatures déjà vues — à l'intérieur de cette fenêtre, une requête identique est donc acceptée deux fois. C'est sans effet sur la transmission des commandes, qui est idempotente par external_order_id : la seconde reçoit la commande déjà enregistrée et n'envoie pas un second email.

Réponses d'authentification

Toutes ces réponses portent le statut 401.

CodeCauseÀ faire
missing_api_key En-tête X-Api-Key absent. Ajouter l'en-tête.
invalid_api_key Clé inconnue, révoquée ou expirée. Le message est volontairement identique dans les trois cas : les distinguer permettrait de tester en masse quels identifiants existent. Vérifier la clé dans l'espace marchand, ou en créer une nouvelle.
missing_signature Écriture sans en-tête X-Signature. Signer la requête (§2.1).
missing_timestamp Écriture sans en-tête X-Timestamp. Ajouter l'horodatage, et le signer.
invalid_timestamp X-Timestamp n'est pas une suite de chiffres — millisecondes, date ISO ou signe. Envoyer un horodatage Unix en secondes.
timestamp_out_of_range Plus de 300 secondes d'écart. Le message donne l'écart exact. Synchroniser l'horloge du serveur (NTP).
signature_mismatch La signature ne correspond pas à la charge attendue. Reprendre les quatre pièges du §2.3, dans l'ordre.
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "timestamp_out_of_range",
    "message": "Horodatage hors tolérance : +412 s d'écart avec notre serveur (maximum 300 s). L'horloge de votre serveur est probablement désynchronisée."
  }
}

3. Raccordement d'une boutique

Ces deux endpoints permettent à un module de récupérer une clé sans que le marchand ait à recopier quoi que ce soit. Le module ouvre une demande, affiche un lien, le marchand valide dans son navigateur, et le module reçoit sa clé et son secret à la lecture suivante.

Ils sont sans authentification, par construction. La sécurité ne repose pas sur une identité mais sur trois choses : la demande n'obtient rien tant qu'un marchand connecté ne l'a pas validée, le jeton de sondage ne quitte jamais le serveur de la boutique, et le secret n'est remis qu'une seule fois. Le pire qu'un appel malveillant puisse produire, c'est une demande en attente que personne n'approuvera — et qui expire en quinze minutes.

POST /api/v1/pairing Sans authentification

Ouvre une demande de raccordement et renvoie le lien de validation à présenter au marchand.

ChampTypeObligatoireDescription
shop_domainchaîneoui Domaine de la boutique, ex. boutique.exemple.fr.
platformchaînenon prestashop, woocommerce, custom… unknown par défaut.
shop_namechaînenon Nom lisible de la boutique, réutilisé à la création du compte.
platform_versionchaînenon Version de la plateforme, ex. 8.1.6.
plugin_versionchaînenon Version du module appelant.
shop_uidchaînenon Identifiant unique tiré une fois à l'installation du module. Fortement recommandé en multiboutique : voir §4.3.
curl -X POST 'https://louis.guide/api/v1/pairing' \
  -H 'Content-Type: application/json' \
  --data-raw '{"shop_domain":"boutique.exemple.fr","platform":"prestashop","shop_name":"Tissu Fiesta","platform_version":"8.1.6","plugin_version":"1.2.0","shop_uid":"ps-7f3a91c4"}'
HTTP/1.1 201 Created

{
  "code": "4K7M-9QR3",
  "poll_token": "pt_8f2c1a7e5b2d48a6c3d9e0f1a2b3c4d5",
  "expires_at": "2026-08-13T15:07:44+00:00",
  "approve_url": "https://louis.guide/app/raccordement/4K7M-9QR3"
}
  • approve_url — à ouvrir dans un nouvel onglet du navigateur du marchand, hors de son back-office. C'est là qu'il se connecte ou crée son compte, puis valide.
  • poll_token — à conserver côté serveur uniquement. Il ne doit jamais apparaître dans une page ni dans une URL : c'est lui qui permettra de récupérer le secret.
  • code — affichable au marchand, pour qu'il vérifie qu'il valide bien la bonne demande.

Codes d'erreur

StatutCodeCause
422invalid_requestshop_domain absent ou inexploitable.
429rate_limitedPlus de 10 ouvertures par heure et par IP.
POST /api/v1/pairing/{code} Sans authentification

Interroge l'état de la demande, et remet la clé une fois — et une seule — que le marchand a validé.

En POST bien que ce soit une lecture, parce que l'appel a un effet de bord : il consomme le secret. En GET, un préchargeur de navigateur ou un antivirus qui suit les liens le consommerait à la place du module.

ChampTypeObligatoireDescription
poll_tokenchaîneoui Le jeton reçu à l'ouverture de la demande.
curl -X POST 'https://louis.guide/api/v1/pairing/4K7M-9QR3' \
  -H 'Content-Type: application/json' \
  --data-raw '{"poll_token":"pt_8f2c1a7e5b2d48a6c3d9e0f1a2b3c4d5"}'

En attente de validation :

HTTP/1.1 200 OK

{ "status": "pending" }

Validé — les identifiants ne sont remis qu'à cet appel :

HTTP/1.1 200 OK

{
  "status": "approved",
  "api_key": "ak_live_5c2f81b0",
  "secret": "sk_live_3f9c1a7e5b2d48a6",
  "merchant": "tissufiesta"
}

Valeurs possibles de status

ValeurSignificationQue faire
pendingLe marchand n'a pas encore tranché.Continuer à sonder.
approvedValidé. La réponse porte les identifiants.Les enregistrer, arrêter le sondage.
rejectedLe marchand a refusé.Arrêter, le lui dire.
expiredQuinze minutes écoulées sans décision.Rouvrir une demande.
consumed Le secret a déjà été remis, et il ne l'est jamais deux fois. Le module a perdu la réponse. Recommencer un raccordement — c'est le comportement sûr.
unknown Code inconnu ou jeton faux. Volontairement indistincts : les séparer ferait de ce point d'entrée un oracle permettant de savoir quelles boutiques se raccordent. Vérifier le couple code / jeton.
rate_limitedTrop de sondages (statut HTTP 429).Espacer les appels.

Enregistrez le secret immédiatement. Il n'est transmis qu'à cette réponse-là. Un module qui échoue à le persister devra faire recommencer tout le raccordement au marchand.

Toujours 200, y compris pour un état d'attente. Le module interroge en boucle : un code HTTP d'erreur sur une situation parfaitement normale ferait remonter des alertes pour rien. Sondez toutes les cinq secondes, la limite étant de 240 appels par quart d'heure et par IP — au-delà, la réponse est { "status": "rate_limited" } avec un statut 429.

4. API CMS (signée)

Le canal des intégrations serveur : modules e-commerce, ERP, CRM, outils internes. Toutes les URL sont préfixées par https://louis.guide.

Lectures : la clé suffit. Écritures : clé + signature. Le détail du calcul est au §2. Les fiches ci-dessous rappellent le régime de chacune par une pastille.

Ce que l'API ne permet pas, et ne permettra pas

Aucun endpoint ne modifie ni ne supprime un avis. Le marchand peut répondre publiquement et signaler pour modération, rien d'autre — exactement ce que permet son espace. Une API plus permissive que l'interface serait une porte dérobée dans la conformité, et c'est la première chose que vérifie un audit.

GET /api/v1/cms/ping Clé d'API

Vérifie qu'une clé fonctionne. C'est le premier appel à écrire, et celui à proposer au marchand sous forme d'un bouton « Tester la connexion » : il vaut mieux qu'il découvre une clé fautive à la configuration qu'à la première commande non transmise.

Sans signature, volontairement. Une lecture ne modifie rien, et surtout : ce point d'entrée doit rester utilisable pour prouver qu'une clé est bonne alors même que l'implémentation HMAC est encore fautive. Le ping passe, l'écriture non : le problème est dans la signature, pas dans la clé.

curl 'https://louis.guide/api/v1/cms/ping' \
  -H 'X-Api-Key: ak_live_5c2f81b0'
HTTP/1.1 200 OK

{
  "status": "ok",
  "merchant": "tissufiesta",
  "api_key": "ak_live_5c2f81b0",
  "server_time": "2026-08-13T14:52:07+00:00"
}

server_time est renvoyé pour une raison précise : comparez-le à l'horloge de votre serveur. Un écart supérieur à 300 secondes fera échouer toutes vos écritures signées (§2.3), et c'est ici qu'on le voit avant d'y perdre une journée.

GET /api/v1/cms/me Clé d'API

État du compte : identité du marchand, capacités de la formule, quota, et adresses de la plateforme.

curl 'https://louis.guide/api/v1/cms/me' \
  -H 'X-Api-Key: ak_live_5c2f81b0'
HTTP/1.1 200 OK

{
  "merchant": {
    "uuid": "0f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
    "slug": "tissufiesta",
    "display_name": "Tissu Fiesta",
    "country": "FR",
    "default_locale": "fr",
    "timezone": "Europe/Paris",
    "status": "active"
  },
  "plan": {
    "code": "pro",
    "can_display_product_reviews": true,
    "can_use_photos": true,
    "can_use_reviews_api": true,
    "can_remove_branding": true
  },
  "quota": {
    "year": 2026,
    "allowance": 5000,
    "used": 1284,
    "remaining": 3716,
    "resets_at": "2027-01-01T00:00:00+00:00"
  },
  "urls": {
    "api": "https://louis.guide",
    "dashboard": "https://louis.guide/app",
    "widget": "https://louis.guide/widget/v1/avis.js",
    "profile": "https://louis.guide/m/tissufiesta"
  },
  "server_time": "2026-08-13T14:52:07+00:00"
}

Lisez les capacités, pas le nom de la formule

Le bloc plan expose des capacités (can_…) en plus du code de formule. Testez les premières : un module qui code en dur if (plan === 'pro') cessera d'être juste le jour où une formule s'ajoute ou change de nom, chez tous les marchands en même temps et sans qu'aucun ne puisse le corriger.

CapacitéCe qu'elle commande
can_display_product_reviews Affichage des avis produit — les étoiles sur les fiches.
can_use_photos Sortie des photos clients par l'API. Elles sont collectées dès la formule gratuite mais ne sont servies qu'en payant : chez un compte gratuit, la galerie répond une liste vide, jamais une erreur.
can_use_reviews_api Lecture des avis par l'API, réponse aux avis, et accès MCP. La transmission des commandes, elle, n'est pas concernée : elle est incluse dans toutes les formules.
can_remove_branding Retrait de la mention de la plateforme sur les widgets et emails.

Le bloc urls évite de deviner

Votre intégration ne doit connaître qu'une seule adresse : celle de l'API. Les autres — espace marchand, script des widgets, page publique du marchand — sont renvoyées ici. Un module qui les recompose à partir d'une base unique suppose que tout vit sur le même hôte, ce qui cesse d'être vrai dès qu'un canal passe sur un sous-domaine, et produit des liens morts chez tous les marchands déjà installés.

quota.remaining mérite un affichage dans votre interface : à zéro, les commandes continuent d'être acceptées mais plus aucune sollicitation ne part. Prévenir à 90 % de consommation évite au marchand de le découvrir sur ses statistiques.

POST /api/v1/cms/orders Signature exigée

L'endpoint central. Il enregistre une commande et planifie la demande d'avis. Tout le reste de la plateforme découle de cet appel : sans lui, il n'y a ni sollicitation, ni avis, ni note.

Idempotent par external_order_id

Réémettre la même référence renvoie la commande déjà enregistrée avec un statut 200 au lieu de 201, sans créer de doublon et sans envoyer un second email au client. Le champ idempotent de la réponse vaut alors true. Vous pouvez donc réessayer sans précaution après une coupure réseau ou un délai d'attente dépassé — c'est le comportement à préférer à toute logique de déduplication maison.

Quand appeler

Au moment où l'expérience est vécue, pas commandée : à la livraison, à l'expédition selon votre métier, ou au passage au statut qui en tient lieu. C'est experienced_at qui porte cette date, et c'est elle qui fait démarrer le délai de sollicitation.

Corps de la requête

Racine

ChampTypeOblig.Description
external_order_idchaîne (100)oui Référence de la commande chez vous. Clé d'idempotence et preuve d'achat conservée cinq ans (AFNOR §6.3). Doit être stable dans le temps.
customerobjetoui Identité du client à solliciter — voir le tableau suivant.
experienced_atISO 8601oui Date de livraison ou de consommation, avec fuseau explicite. Voir l'encart ci-dessous : ce n'est pas la date de commande.
sourceobjetoui Contexte technique de l'émission — voir plus bas.
itemstableau (200 max)non Articles. Sans eux, aucun avis produit ne sera demandé — seul l'avis sur l'enseigne.
amountchaîne décimalenon Montant total, ex. "129.90". Jamais un flottant.
currencyISO 4217non "EUR", "CHF"…
channelénumérationnon ecommerce_order (défaut), pos_transaction, qr_scan, manual, csv_import, nfc.
location_idchaîne (100)non Établissement concerné, tel que déclaré par le marchand. Une valeur inconnue fait échouer la requête en 422 plutôt que de rattacher la commande au mauvais point de vente.
solicitation_delay_daysentier 0–365non Délai propre à cette commande, qui surcharge le réglage du compte. Utile quand un même vendeur expédie un bouquet à solliciter demain et un matelas à solliciter dans un mois. Hors bornes, la valeur est ignorée et le réglage du compte s'applique — une valeur aberrante ne doit pas faire perdre une commande.
order_status_idchaîne (20)non Statut de la commande chez vous au moment de l'envoi. Purement diagnostic — nous ne l'interprétons pas — mais c'est la seule information qui permette de répondre à « pourquoi cette commande n'a rien déclenché ».
order_status_labelchaîne (120)non Libellé lisible de ce statut.

experienced_at : la date de livraison, pas celle de la commande

Un colis commandé le 1er et livré le 6 porte le 6. Ce n'est pas une subtilité : deux essais randomisés portant sur plus de 300 000 consommateurs (Jung, Ryu, Han & Cho, Journal of Marketing, 2023) établissent qu'une sollicitation envoyée avant que le client ait pu se forger un avis a un effet négatif sur le taux de dépôt. Ancrer le délai sur la date de commande revient à solliciter systématiquement trop tôt, de tout le délai de livraison.

C'est aussi l'une des trois dates affichées publiquement à côté de l'avis (AFNOR §6.3).

customer

ChampTypeOblig.Description
emailemail (255)oui Seule donnée personnelle en clair que nous acceptons. Effacée après le délai de dépôt ; seule son empreinte subsiste.
countryISO 3166-1 alpha-2non* *Fortement recommandé. Google calcule ses notes marchand par pays et écarte les avis dont le pays est inconnu. Cette information n'existe qu'au moment de la commande : une fois l'adresse purgée, elle est définitivement irrécupérable, sans rattrapage possible.
localefr, en, nl, de, it, esnon Langue de l'email de sollicitation. À défaut, la langue par défaut du marchand — solliciter un client néerlandophone en français fait chuter le taux de réponse.
phonechaîne (32)non Mobile pour la sollicitation par SMS. Format international (+33612345678) fortement recommandé : c'est le seul non ambigu. Un numéro national est converti à partir de country ; sans pays connu, il est écarté sans faire échouer la commande. Voir l'avertissement ci-dessous.
first_name, last_namechaîne (100)non Personnalisation de la sollicitation et nom affiché de l'auteur.
companychaîne (255)non Raison sociale, pour une commande professionnelle.
postal_code, citychaînenon Purgés en même temps que l'adresse email.

N'envoyez le numéro de mobile que si le marchand a souscrit le SMS. Sans l'option, il est reçu et conservé sans qu'aucun message ne parte : une donnée personnelle collectée sans finalité, ce qu'aucune des deux parties ne peut justifier en cas de contrôle.

source — obligatoire

Ce bloc n'est pas de la statistique. Quand un marchand écrit « mes avis ne partent plus depuis la mise à jour », la réponse est déjà dedans : version de la plateforme, version du module, événement déclencheur. Le rendre optionnel reviendrait à ne l'avoir jamais — les intégrateurs remplissent ce qui est exigé, pas ce qui est suggéré.

ChampTypeOblig.Description
platformchaîne (50)oui prestashop, woocommerce, shopify, magento, custom…
platform_versionchaîne (30)non Ex. 8.1.6.
plugin_versionchaîne (30)non Version de votre intégration. À incrémenter à chaque livraison.
triggerchaîne (100)non Événement à l'origine de l'envoi, ex. woocommerce_order_status_completed. Permet de comprendre pourquoi une commande part trop tôt ou trop tard.
shop_uidchaîne (80)non* *Décisif en multiboutique. Identifiant tiré une fois à l'installation et conservé. Voir l'encart.
shop_idchaîne (50)non Identifiant de boutique chez la plateforme. Sert de repli quand shop_uid est absent.
shop_namechaîne (255)non Nom lisible de cette boutique. Sans lui, le marchand découvre dans son espace un établissement appelé « 3 » et doit deviner lequel c'est.
shop_group_id, lang_idchaînenon Conservés pour le diagnostic, jamais interprétés. lang_id ne sépare rien : la langue de l'avis vient de customer.locale.

Multiboutique : shop_id ne suffit pas

Il vaut « 1 » sur toute installation mono-boutique. Un marchand qui exploite deux sites sous le même compte — une marque par domaine, cas courant — enverrait donc « 1 » depuis les deux : les deux boutiques se fondraient en un seul établissement, les avis de l'une s'afficheraient sur la page de l'autre, et le nom retenu serait celui de la dernière commande reçue. Défaut constaté en recette sur deux PrestaShop réels.

shop_uid règle le problème : tirez-le une fois à l'installation, conservez-le. Il survit à un changement de domaine comme à un renouvellement de clé — les deux autres discriminants auxquels on pense d'abord, et qui bougent l'un comme l'autre.

items[] — facultatif, 200 articles au maximum

ChampTypeOblig.Description
external_product_idchaîne (100)oui Identifiant du produit dans votre catalogue.
namechaîne (255)oui Nom du produit tel affiché au client.
variant_idchaîne (100)non* *Le champ le plus important de cette liste. Sans lui, la chaise rouge et la chaise bleue partagent la même clé produit : leurs avis se mélangent et « le pied s'est cassé » ne désigne plus rien. Correspond à id_product_attribute (PrestaShop), à la variation (WooCommerce), au variant (Shopify).
variant_labelchaîne (255)non Libellé lisible : « Couleur : rouge, Taille : L ».
gtin8 à 14 chiffresnon* EAN-13 ou UPC-A converti. Clé d'agrégation entre marchands, et exigence de Google pour afficher les étoiles dans ses résultats.
upc, isbn, mpnchaînenon Tenus séparément du GTIN parce que les catalogues les tiennent dans des colonnes distinctes. L'ISBN est décisif sur le livre, où le GTIN est souvent vide.
sku, brandchaînenon Référence interne et marque.
category_id, category_namechaînenon Catégorie principale dans votre catalogue.
product_url, image_urlURL (500)non Utilisées dans l'email de sollicitation : un visuel de produit améliore nettement le taux de dépôt.
imagesliste d'URL (10 max)non Visuels supplémentaires.
descriptionchaîne (5000)non Reçue, jamais réaffichée telle quelle : c'est votre texte, pas celui de l'auteur de l'avis. Elle sert à situer le produit en modération.
tagsliste (30 max)non Mots-clés du produit, 60 caractères chacun.
meta_title, meta_descriptionchaînenon Métadonnées de la fiche.
quantityentier > 0non 1 par défaut.
unit_pricechaîne décimalenon Ex. "19.90". En chaîne, comme tous les montants.

Exemple complet

POST /api/v1/cms/orders
X-Api-Key: ak_live_5c2f81b0
X-Timestamp: 1786000000
X-Signature: sha256=…
Content-Type: application/json

{
  "external_order_id": "CMD-1042",
  "experienced_at": "2026-08-06T09:15:00+02:00",
  "amount": "129.90",
  "currency": "EUR",
  "order_status_id": "5",
  "order_status_label": "Livré",
  "customer": {
    "email": "claire.martin@exemple.fr",
    "first_name": "Claire",
    "last_name": "Martin",
    "locale": "fr",
    "country": "FR",
    "postal_code": "69003",
    "city": "Lyon"
  },
  "items": [
    {
      "external_product_id": "REF-42",
      "variant_id": "REF-42-ROUGE-L",
      "variant_label": "Couleur : rouge, Taille : L",
      "name": "Nappe en lin lavé",
      "sku": "NAP-LIN-42",
      "gtin": "3401579874120",
      "brand": "Tissu Fiesta",
      "quantity": 2,
      "unit_price": "64.95",
      "product_url": "https://boutique.exemple.fr/nappe-lin-42",
      "image_url": "https://boutique.exemple.fr/img/nappe-42.jpg"
    }
  ],
  "source": {
    "platform": "prestashop",
    "platform_version": "8.1.6",
    "plugin_version": "1.2.0",
    "trigger": "actionOrderHistoryAddAfter",
    "shop_uid": "ps-7f3a91c4",
    "shop_id": "1",
    "shop_name": "Tissu Fiesta"
  }
}

Commande enregistrée :

HTTP/1.1 201 Created

{
  "uuid": "3b8e1f42-6c7d-4e9a-91b2-5d0c3a7f4e18",
  "external_order_id": "CMD-1042",
  "status": "pending",
  "channel": "ecommerce_order",
  "experienced_at": "2026-08-06T07:15:00+00:00",
  "imported_at": "2026-08-06T09:41:22+00:00",
  "items_count": 1,
  "idempotent": false
}

Même requête rejouée :

HTTP/1.1 200 OK

{ "…": "…", "idempotent": true }

L'adresse email n'est jamais renvoyée, même si vous venez de la transmettre : toute donnée renvoyée est une donnée qui peut fuiter dans vos propres journaux.

Valeurs possibles de status

ValeurSignification
pendingReçue, en attente de planification.
scheduledSollicitation programmée.
solicitedDemande d'avis envoyée au client.
reviewedLe client a déposé son avis.
cancelledAnnulée avant envoi.
expiredDélai de dépôt écoulé sans avis.

Erreurs

Cet endpoint est le seul servi par API Platform : ses erreurs de validation arrivent donc sous forme de liste de violations, et non dans l'enveloppe { "error": … } du reste de l'API. Votre code doit accepter les deux formes.

HTTP/1.1 422 Unprocessable Content

{
  "status": 422,
  "detail": "customer.email: \"claire.martin\" n'est pas une adresse email valide.",
  "violations": [
    {
      "propertyPath": "customer.email",
      "message": "\"claire.martin\" n'est pas une adresse email valide."
    }
  ]
}
StatutCauseÀ faire
401 Clé absente, invalide, ou signature refusée. Voir §2.
422 Un champ manque ou est mal formé — voir violations. Corriger le champ désigné par propertyPath.
422 experienced_at est dans le futur (au-delà d'un jour de marge). Vérifier le fuseau horaire du serveur : c'est presque toujours de là que vient l'écart.
422 experienced_at remonte à plus de 90 jours. Voir l'encart ci-dessous. Pour reprendre un historique, contactez le support.
422 Aucun établissement ne correspond à location_id. Créer l'établissement dans l'espace marchand, ou omettre le champ.
415 En-tête Content-Type absent ou inattendu. Envoyer Content-Type: application/json.

Pourquoi les commandes de plus de 90 jours sont refusées

Le scénario de sinistre est connu : un module s'installe et pousse trois ans d'historique d'un coup. Des milliers d'invitations partent vers des adresses périmées, le taux de rejet explose — et comme tous les emails partent de notre domaine, c'est la délivrabilité de tous les marchands qui s'effondre, pas seulement celle du nouveau venu.

Le refus est prononcé à l'entrée, avec un message explicite, plutôt qu'à la planification : l'intégrateur comprend immédiatement au lieu de voir ses commandes disparaître en silence.

GET /api/v1/cms/reviews Clé d'API Formule payante

Liste les avis du marchand, du plus récent au plus ancien, avec la réponse publiée et le signalement éventuel de chacun. C'est cet endpoint qui permet de rapatrier les avis dans un ERP, un CRM ou un outil de service client.

ParamètreDéfautDescription
typemerchant merchant pour les avis sur l'enseigne, product pour les avis produit.
statustous published, pending, awaiting_email, rejected, disputed, withdrawn. Une valeur inconnue est ignorée — le filtre ne s'applique alors pas, plutôt que de renvoyer une erreur.
page1Numéro de page.
per_page25 De 1 à 100. Au-delà, la valeur est ramenée à 100.
curl 'https://louis.guide/api/v1/cms/reviews?status=published&per_page=50' \
  -H 'X-Api-Key: ak_live_5c2f81b0'
HTTP/1.1 200 OK

{
  "reviews": [
    {
      "id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
      "score": 4,
      "title": "Livraison rapide",
      "comment": "Nappe conforme, le lin est épais. Un pli à la livraison.",
      "author": "Claire M.",
      "language": "fr",
      "country": "FR",
      "status": "published",
      "verified_purchase": true,
      "experienced_at": "2026-08-06T07:15:00+00:00",
      "submitted_at": "2026-08-13T18:02:41+00:00",
      "published_at": "2026-08-13T18:04:10+00:00",
      "order_reference": "CMD-1042",
      "response": {
        "content": "Merci Claire, nous notons pour l'emballage.",
        "created_at": "2026-08-14T08:11:00+00:00",
        "updated_at": null
      },
      "report": null
    }
  ],
  "page": 1,
  "per_page": 50,
  "total": 318
}

Les trois dates, et pourquoi elles sont trois

experienced_at (l'expérience vécue), submitted_at (le dépôt) et published_at (la mise en ligne) sont trois choses différentes, et l'AFNOR impose de pouvoir les distinguer. Un intégrateur qui les confond affiche « il y a 3 jours » sur une expérience vieille de trois semaines. published_at vaut null tant que l'avis n'est pas publié.

order_reference reprend votre external_order_id : c'est lui qui rattache l'avis à la commande dans votre système. Il vaut null pour un avis déposé hors sollicitation.

Synchronisation incrémentale

Interrogez avec status=published et comparez published_at au dernier passage : rapatrier tout l'historique à chaque exécution fonctionne les premiers mois, puis devient une requête de plusieurs milliers de lignes toutes les heures. La pagination commence à 1 et le champ total donne le nombre d'avis correspondant au filtre, pas le nombre de pages.

StatutCodeCause
402plan_required La formule du marchand n'inclut pas l'API avis. Les avis restent lisibles sans clé par l'API publique — ce n'est pas la même chose : celle-ci sert l'affichage public, pas l'export.
POST /api/v1/cms/reviews/{uuid}/response Signature exigée Formule payante

Publie une réponse publique à un avis sur l'enseigne, ou met à jour celle qui existe déjà. Un avis ne porte qu'une seule réponse : réémettre remplace le texte.

ChampTypeOblig.Description
contentchaîneoui Texte de la réponse. Tronqué à 3000 caractères sans erreur — vérifiez la longueur de votre côté si la coupe vous gêne.
curl -X POST 'https://louis.guide/api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response' \
  -H 'X-Api-Key: ak_live_5c2f81b0' \
  -H 'X-Timestamp: 1786000000' \
  -H 'X-Signature: sha256=…' \
  -H 'Content-Type: application/json' \
  --data-raw '{"content":"Merci Claire, nous notons pour l’emballage."}'
HTTP/1.1 201 Created

{
  "review_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  "response": {
    "content": "Merci Claire, nous notons pour l'emballage.",
    "created": true,
    "status": "published",
    "published": true
  }
}

Lisez published avant d'annoncer quoi que ce soit

Le marchand règle dans son espace si les réponses écrites par un programme partent directement ou attendent sa relecture. Ce réglage vit sur le compte et n'est pas un paramètre de la requête : si l'appelant pouvait choisir lui-même s'il doit être relu, la garantie ne vaudrait rien.

Conséquence pour votre interface : une réponse acceptée n'est pas forcément visible. published: false signifie « enregistrée en brouillon, à valider dans l'espace marchand » — dites-le, plutôt que d'afficher un « publié » qui sera démenti par la page publique.

201 à la création, 200 à la mise à jour ; le champ created reprend la même information dans le corps. Une réponse déjà publiée le reste : une mise à jour ne la repasse jamais en brouillon, ce qui la ferait disparaître de la page sans que personne ne l'ait décidé.

StatutCodeCause
402plan_requiredFormule sans réponse aux avis.
404review_not_found Identifiant inconnu, mal formé, ou appartenant à un autre marchand — les trois cas sont indistinguables, et c'est le cloisonnement qui l'impose.
422content_requiredcontent absent ou vide.
POST /api/v1/cms/reviews/{uuid}/report Signature exigée

Signale un avis pour modération. L'avis passe au statut « contesté » et le dossier entre dans la file d'instruction.

ChampTypeOblig.Description
reasonénumérationoui Motif, à choisir dans la liste ci-dessous.
detailchaînenon Précisions pour le modérateur, tronquées à 1000 caractères. C'est ici qu'on écrit « commande n° X, jamais livrée à cette adresse » — un signalement motivé est instruit plus vite.

Motifs recevables

ValeurQuand l'invoquer
inappropriate_contentInjure, propos haineux, contenu illicite.
spam_or_advertisingPublicité, lien commercial, contenu automatisé.
off_topicSans rapport avec l'expérience vécue — le transporteur, la météo.
conflict_of_interestConcurrent, ancien salarié, avis rémunéré.
personal_data_disclosureL'avis expose des données personnelles.

Une note basse n'est pas un motif

Aucun motif ne permet de contester un avis en raison de sa note, et ce n'est pas un oubli : c'est l'interdit qui fait la différence entre une plateforme d'avis et une vitrine. Un signalement mal motivé est rejeté, et l'avis reste en ligne.

HTTP/1.1 202 Accepted

{
  "review_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  "report": {
    "reason": "off_topic",
    "status": "pending",
    "review_remains_visible": true
  }
}

review_remains_visible vaut toujours true, et le champ existe pour qu'aucune interface ne soit conçue en supposant le contraire : l'avis reste public pendant toute l'instruction. Le retirer sur simple signalement reviendrait à laisser le marchand décoter ce qui lui déplaît — Google l'interdit explicitement, l'AFNOR aussi. La réponse est un 202 : la demande est enregistrée, pas tranchée.

StatutCodeCause
404review_not_foundIdentifiant inconnu, mal formé, ou hors de votre compte.
409already_reportedUn signalement est déjà ouvert sur cet avis.
422invalid_reason Motif absent ou hors liste. Le message rappelle les valeurs admises.

5. API publique

Lecture seule, sans authentification, sur /api/v1/public/. C'est ce que consomment les widgets, et ce que peut consommer n'importe quel affichage sur mesure.

Le {slug} des chemins est l'identifiant public du marchand — celui de sa page publique, visible dans urls.profile renvoyé par /cms/me.

Ce qui protège une API sans clé

Il n'y a pas d'identité à vérifier : ce code s'exécute chez les visiteurs d'une boutique, aucun secret ne peut y vivre. La protection tient donc à trois autres choses, qu'il faut connaître avant d'intégrer.

  • Aucune donnée sensible ne sort d'ici. Ni email, ni empreinte d'email, ni référence de commande, ni identifiant interne. Un widget affiche des avis publics ; tout ce qui sort par ce canal est lisible par n'importe qui.
  • Débit limité à 60 requêtes par minute et par adresse IP, en fenêtre glissante. Une fiche produit fait deux appels (note + avis) : cela laisse 30 chargements par minute depuis une même adresse — large pour un visiteur, étroit pour un aspirateur de contenu. Voir §9.
  • CORS restreint aux domaines déclarés du marchand. Un joker * autoriserait n'importe quel site — concurrent, comparateur, contrefacteur — à afficher les avis de n'importe quel marchand comme s'ils étaient les siens.

CORS : ce qu'il faut déclarer pour que le navigateur accepte la réponse

L'en-tête Access-Control-Allow-Origin n'est posé que si l'origine appelante correspond à un domaine raccordé au marchand désigné dans l'URL. Les sous-domaines sont acceptés : un domaine déclaré exemple.fr autorise www.exemple.fr et boutique.exemple.fr.

Symptôme typique d'un domaine non déclaré : la requête part, le serveur répond 200, et le navigateur bloque la lecture en console. Le remède est dans l'espace marchand, pas dans le code.

Un appel serveur à serveur n'est pas concerné : sans en-tête Origin, il n'y a pas de contrôle CORS. C'est la limitation de débit qui couvre ce cas. Le CORS protège le navigateur d'un autre site, jamais la donnée elle-même.

Cache

Toutes les réponses sont publiques et mises en cache : 60 secondes pour les avis et les notes, 300 secondes pour les réglages d'affichage. C'est ce qui permet d'absorber le trafic d'une boutique en promotion sans dimensionner pour le pic. Ne construisez pas un affichage qui suppose l'apparition instantanée d'un avis publié.

GET /api/v1/public/merchants/{slug}/score Sans authentification

Note globale de la boutique et répartition par note.

curl 'https://louis.guide/api/v1/public/merchants/tissufiesta/score'
HTTP/1.1 200 OK
Cache-Control: public, max-age=60

{
  "merchant": { "slug": "tissufiesta", "name": "Tissu Fiesta" },
  "score": {
    "average": 4.6,
    "count": 318,
    "distribution": { "1": 4, "2": 6, "3": 18, "4": 91, "5": 199 },
    "scale": { "min": 1, "max": 5 }
  }
}

scale est renvoyé explicitement plutôt que sous-entendu : un intégrateur qui code « sur 10 » parce que son précédent prestataire l'était produit un affichage faux que personne ne relit. count ne compte que les avis publiquement visibles.

404 merchant_not_found si le slug est inconnu.

GET /api/v1/public/merchants/{slug}/reviews Sans authentification

Avis sur l'enseigne, du plus récent au plus ancien.

ParamètreDéfautDescription
page1Numéro de page.
per_page10 De 1 à 50. Au-delà, ramené à 50.
HTTP/1.1 200 OK

{
  "reviews": [
    {
      "id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
      "author": "Claire M.",
      "score": 4,
      "title": "Livraison rapide",
      "comment": "Nappe conforme, le lin est épais. Un pli à la livraison.",
      "language": "fr",
      "verified_purchase": true,
      "experienced_at": "2026-08-06",
      "submitted_at": "2026-08-13",
      "published_at": "2026-08-13",
      "disputed": false,
      "photos": [
        {
          "id": "6d2a1f80-9b3c-4d5e-8f70-1a2b3c4d5e6f",
          "url": "https://louis.guide/photos/6d2a1f80-9b3c-4d5e-8f70-1a2b3c4d5e6f",
          "thumbnail_url": "https://louis.guide/photos/6d2a1f80-9b3c-4d5e-8f70-1a2b3c4d5e6f/thumb",
          "width": 1600,
          "height": 1200
        }
      ],
      "reply": {
        "content": "Merci Claire, nous notons pour l'emballage.",
        "published_at": "2026-08-14"
      }
    }
  ],
  "page": 1,
  "per_page": 10
}

L'ordre chronologique est imposé, pas choisi

Il n'y a pas de paramètre de tri, et il n'y en aura pas : l'AFNOR (§6.3) exige l'ordre chronologique inverse comme affichage par défaut. Proposer « les mieux notés d'abord » comme tri initial serait une présentation orientée. Un tri en JavaScript sur la page reçue relève de votre responsabilité, pas de la nôtre.

Ce que votre affichage doit reprendre

  • Deux dates au minimum — celle de l'expérience et celle de la publication. C'est une obligation d'affichage, et seule l'API peut vous les fournir. Les dates publiques sont réduites au jour (2026-08-06) : aucun widget n'affiche l'heure.
  • verified_purchase — l'avis est rattaché à une commande réelle. C'est ce qui distingue un avis collecté d'un avis déposé spontanément.
  • disputed — l'avis est contesté et son instruction est en cours. Il reste affiché (voir §4.6) ; signalez-le plutôt que de le masquer.
  • reply — la réponse du marchand fait partie de l'avis pour le lecteur. published_at y porte la date de dernière modification quand il y en a eu une : afficher la date d'origine sous un texte réécrit induirait en erreur.
  • photos — vide chez un marchand dont la formule ne les sert pas. Les avis restent complets, seules les images manquent.

Il n'y a pas de champ total sur ce canal : une page vide signifie qu'il n'y a plus rien à charger. C'est ce que fait le bouton « voir plus » du widget.

GET /api/v1/public/products/{slug}/{productId}/score Sans authentification

Note d'un produit. {productId} est votre identifiant de catalogue, celui transmis en external_product_id — nous ne le réécrivons jamais. Pensez à l'encoder s'il contient des caractères réservés.

ParamètreDéfautDescription
variant— Restreint la note à une déclinaison. Absent, la note porte sur toutes les variantes confondues — ce qui est le bon comportement tant que le visiteur n'a pas choisi sa taille.
with_variantsfalse Ajoute le détail par déclinaison. Coûte une requête de plus : ne l'activez pas sur la fiche produit, qui est la page la plus vue de la boutique.
curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/score?with_variants=true'
HTTP/1.1 200 OK

{
  "product": { "external_id": "REF-42", "variant_id": null },
  "score": {
    "average": 4.4,
    "count": 27,
    "distribution": { "1": 0, "2": 1, "3": 3, "4": 7, "5": 16 },
    "scale": { "min": 1, "max": 5 }
  },
  "variants": [
    { "variant_id": "REF-42-ROUGE-L", "average": 4.8, "count": 12 },
    { "variant_id": "REF-42-BLEU-M", "average": 4.1, "count": 15 }
  ]
}

variants vaut null quand with_variants n'est pas demandé — c'est une absence de calcul, pas une absence de déclinaisons.

GET /api/v1/public/products/{slug}/{productId}/reviews Sans authentification

Avis d'un produit. Même structure de réponse et mêmes paramètres de pagination que les avis d'enseigne, avec en plus le filtre variant — utile quand un sélecteur de taille veut n'afficher que les avis de la variante choisie.

curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/reviews?variant=REF-42-ROUGE-L&per_page=5'

Chez un marchand dont la formule n'inclut pas l'affichage des avis produit, prévoyez un affichage qui se réduit proprement plutôt qu'un cadre vide : le widget, lui, disparaît de la page.

GET /api/v1/public/merchants/{slug}/photos Sans authentification

Photos clients approuvées, sans le texte des avis. C'est ce qui alimente un carrousel : sans cet endpoint, il faudrait charger cinquante avis complets — texte, dates, notes — pour n'en garder que les images, sur une fiche produit qui charge déjà le thème du marchand.

ParamètreDéfautDescription
produit— Restreint à un produit. Absent, renvoie les photos de toute la boutique — ce qui alimente un carrousel de page d'accueil.
limite24 De 1 à 50.
HTTP/1.1 200 OK

{
  "photos": [
    {
      "id": "6d2a1f80-9b3c-4d5e-8f70-1a2b3c4d5e6f",
      "url": "https://louis.guide/photos/6d2a1f80-9b3c-4d5e-8f70-1a2b3c4d5e6f",
      "thumbnail_url": "https://louis.guide/photos/6d2a1f80-9b3c-4d5e-8f70-1a2b3c4d5e6f/thumb",
      "width": 1600,
      "height": 1200
    }
  ]
}

Les URL sont absolues : ce JSON est lu par du JavaScript exécuté sur le domaine de la boutique, où une URL relative pointerait vers la boutique elle-même. Servez-vous de width et height pour réserver la place avant chargement — sans quoi la fiche produit sautera sous les yeux du visiteur.

Chez un marchand dont la formule ne sert pas les photos, la réponse est { "photos": [] } avec un statut 200, jamais une erreur : le carrousel disparaît proprement au lieu d'afficher un cadre en échec.

GET /api/v1/public/merchants/{slug}/display Sans authentification

Réglages d'affichage décidés par le marchand dans son espace. C'est ce qui permet de poser les balises une fois pour toutes dans un thème, puis d'allumer, éteindre ou déplacer un élément sans retoucher le code de la boutique.

HTTP/1.1 200 OK
Cache-Control: public, max-age=300

{
  "merchant": {
    "slug": "tissufiesta",
    "name": "Tissu Fiesta",
    "accent_color": "#7c3aed"
  },
  "display": {
    "badge_flottant": false,
    "badge_cote": "droite",
    "badge_decalage": 16,
    "seuil_avis": 1,
    "etoiles_fiche": true,
    "etoiles_vignettes": true,
    "onglet_avis": true,
    "bloc_accueil": true
  },
  "profile_url": "https://louis.guide/m/tissufiesta"
}
RéglageDéfautSignification
badge_flottantfalse Pastille de note fixée dans un coin de l'écran. Éteinte par défaut : elle se superpose à la page du marchand, et rien ne doit apparaître chez lui sans qu'il l'ait demandé.
badge_cotedroitedroite ou gauche.
badge_decalage16Décalage en pixels par rapport au bord.
seuil_avis1 Nombre d'avis en dessous duquel l'affichage disparaît. Voir §6 : afficher « aucun avis » est pire que ne rien afficher.
etoiles_fichetrueÉtoiles sur la fiche produit.
etoiles_vignettestrueÉtoiles sur les vignettes de listing.
onglet_avistrueOnglet « Avis » de la fiche produit.
bloc_accueiltrueBloc d'avis en page d'accueil.

display est toujours complet, défauts inclus : votre code n'a pas à connaître nos valeurs par défaut, ni à les recopier — le jour où l'une change, il suit.

accent_color vaut null quand le marchand n'a pas choisi de couleur ou quand sa formule ne l'autorise plus. Prévoyez toujours une couleur de repli de votre côté : c'est ce que fait le widget, dont la teinte n'existe que si elle est déclarée.

GET /api/v1/public/health Sans authentification

Disponibilité du service. À interroger par une sonde ou par le contrôle de santé d'un module.

HTTP/1.1 200 OK

{ "status": "ok" }

Volontairement minimal : aucun accès base, aucune dépendance externe. Une lenteur de la base ne doit pas déclencher une fausse alerte d'indisponibilité — et inversement, ce point d'entrée ne dit rien de l'état de la base. Pour vérifier qu'une clé fonctionne, c'est /cms/ping qu'il faut appeler.

6. Widgets d'affichage

Quatre éléments HTML à poser dans un thème. Un seul script à charger, aucune dépendance, aucune configuration : l'adresse de l'API est déduite de l'URL du script lui-même.

<script src="https://louis.guide/widget/v1/avis.js" async></script>

<avis-score marchand="tissufiesta"></avis-score>
<avis-liste marchand="tissufiesta" par-page="5"></avis-liste>

Le script est chargé une fois par page, où l'on veut ; les éléments peuvent être posés avant lui. Il est servi sous /widget/v1/ : une évolution incompatible partira en /v2/ et celui-ci restera servi tel quel — il vit dans des thèmes que personne ne mettra à jour.

Les quatre éléments

ÉlémentCe qu'il afficheOù le poser
<avis-score> Note moyenne, étoiles, nombre d'avis. Fiche produit, en-tête de boutique, page « à propos ».
<avis-liste> Avis paginés, avec photos et réponses du marchand. Onglet « Avis » d'une fiche produit, page dédiée.
<avis-carrousel> Photos clients seules, cliquables. Fiche produit, page d'accueil.
<avis-flottant> Pastille de note fixée dans un coin, cliquable. Le gabarit commun, une seule fois pour tout le site.

Attributs

AttributÉlémentsDéfautRôle
marchandtous— Obligatoire. Identifiant public du marchand, celui de sa page publique.
produit score, liste, carrousel— Votre identifiant de catalogue (external_product_id). Absent, l'élément porte sur toute la boutique.
languetouslang de la page Langue des libellés. À défaut, l'attribut lang du document — que le thème renseigne déjà — puis le français. Seuls fr et en sont réellement traduits ; toute autre valeur retombe sur le français plutôt que d'afficher des libellés à moitié traduits.
miniscore1 Nombre d'avis en dessous duquel l'élément disparaît. À 3, une fiche qui n'a que deux avis n'affiche rien plutôt qu'une note fondée sur presque rien.
par-pageliste5 Avis chargés à la fois ; un bouton « Voir plus » charge la suite.
maxcarrousel12 Nombre de photos, 50 au maximum.

Exemple complet sur une fiche produit

<!-- Sous le titre du produit -->
<avis-score marchand="tissufiesta" produit="REF-42" mini="3"></avis-score>

<!-- Photos des acheteurs, sous la galerie du catalogue -->
<avis-carrousel marchand="tissufiesta" produit="REF-42" max="8"></avis-carrousel>

<!-- Dans l'onglet « Avis » -->
<avis-liste marchand="tissufiesta" produit="REF-42" par-page="10"></avis-liste>

<!-- Une seule fois, dans le gabarit commun -->
<avis-flottant marchand="tissufiesta"></avis-flottant>

Un widget qui n'affiche rien n'est pas forcément en panne

Trois situations font disparaître un élément, et c'est à chaque fois voulu : aucun avis (ou moins que le seuil), aucune photo pour le carrousel, et toute erreur réseau ou serveur.

Afficher « Aucun avis pour le moment » sur une fiche produit est pire que ne rien afficher : le visiteur en conclut que personne n'a commandé. Et un bandeau d'erreur sur la boutique d'un marchand parce que notre API tousse serait indéfendable — l'élément se retire de la mise en page, la fiche produit reste intacte.

Conséquence pratique pour l'intégrateur : ne construisez pas de mise en page qui réserve une hauteur fixe pour un widget. Il peut ne rien occuper du tout.

Ce que le marchand pilote sans vous

Les éléments lisent /display au chargement. Deux réglages leur viennent de là plutôt que d'un attribut, et c'est délibéré : le marchand peut poser ses balises une fois pour toutes, puis changer d'avis depuis son espace sans rouvrir son thème.

  • La pastille flottante — allumée ou éteinte, à droite ou à gauche, avec son décalage. <avis-flottant> posé dans le gabarit n'affiche rien tant que le marchand ne l'a pas activée. Elle est éteinte par défaut : rien ne doit apparaître chez lui sans qu'il l'ait demandé.
  • La couleur de marque — appliquée aux surfaces qui peuvent l'être. Les étoiles gardent leur ambre, le vert d'« Achat vérifié » et l'ambre de « Contesté » aussi : ces couleurs portent un sens, elles ne décorent pas, et les repeindre rendrait la note illisible chez un marchand dont la marque est jaune pâle ou blanche.

Les réglages ne sont demandés qu'une fois par page, même avec quatre éléments : la requête en vol est partagée. C'est ce qui évite au widget d'être le script qui ralentit la fiche produit — reproche fondé qu'on peut faire à la plupart des modules d'avis.

Isolation vis-à-vis du thème

Chaque élément rend son contenu dans un shadow DOM : le CSS du thème ne déborde pas sur le widget, et celui du widget ne déborde pas sur la boutique. Aucun des deux ne serait acceptable dans l'autre sens.

Corollaire à connaître avant d'essayer : vos règles CSS n'atteindront pas l'intérieur des widgets. La seule personnalisation prévue est la couleur de marque, réglée dans l'espace marchand. Un affichage réellement sur mesure passe par l'API publique — c'est exactement ce pour quoi elle est documentée.

Avant de coller quoi que ce soit

Sous PrestaShop et WooCommerce, le module pose ces balises lui-même, au bon endroit du thème. Le collage manuel s'adresse aux autres plateformes et aux thèmes sur mesure — voir les modules.

7. Serveur MCP

MCP (Model Context Protocol) expose les mêmes capacités que l'API, sous une forme qu'un assistant IA peut découvrir seul. Là où un développeur lit une documentation, écrit l'authentification et interprète le JSON, l'assistant demande la liste des outils, lit leurs descriptions et les appelle.

Concrètement : le marchand branche son assistant sur ce serveur, puis écrit « quels avis n'ont pas encore de réponse ? » ou « réponds à celui-ci en t'excusant pour le délai ». Personne n'a écrit de code d'intégration.

Valeur
Adressehttps://louis.guide/api/v1/mcp
TransportJSON-RPC 2.0 sur HTTP, en POST
Version du protocole2024-11-05
Serveur annoncéavis-clients, version 1.0.0
Capacitéstools — ni ressources, ni invites
Authentification Jeton porteur (§7.1) ou clé d'API + signature HMAC (§7.2)
FormulePayante — sinon erreur JSON-RPC -32001

7.1 Brancher un assistant : le jeton

C'est la voie normale, et la seule qui ne demande rien d'installé. Le marchand crée un jeton dans son espace — Réglages · Collecte, section « Brancher un assistant » —, puis le colle dans la configuration de son assistant avec l'adresse du serveur.

POST https://louis.guide/api/v1/mcp
Authorization: Bearer mcp_live_…
Content-Type: application/json

Forme usuelle des fichiers de configuration d'un client MCP :

{
  "mcpServers": {
    "avis-clients": {
      "url": "https://louis.guide/api/v1/mcp",
      "headers": { "Authorization": "Bearer mcp_live_…" }
    }
  }
}

Vérification en une commande, avant de brancher quoi que ce soit :

curl -X POST 'https://louis.guide/api/v1/mcp' \
  -H 'Authorization: Bearer mcp_live_…' \
  -H 'Content-Type: application/json' \
  --data-raw '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Ce que le jeton peut, et ce qu'il ne peut pas

PropriétéComportement
Portée Uniquement /api/v1/mcp. Présenté sur l'API CMS, il n'est même pas examiné : ni transmission de commandes, ni export d'avis, ni raccordement.
Écriture Interdite par défaut. Le marchand coche explicitement « autoriser la rédaction de réponses » à la création. Sans elle, l'outil repondre_a_un_avis n'apparaît même pas dans tools/list — l'assistant ne le proposera donc pas.
Durée de vie Un an, puis il cesse de valoir. Il se recrée en dix secondes.
Révocation Immédiate et définitive, jeton par jeton, sans toucher aux clés d'API ni aux modules du marchand.
Conservation Affiché une seule fois. Nous n'en gardons qu'une empreinte : personne ne peut le réafficher, nous compris.
Nombre Trois jetons valides au maximum par compte.

Un jeton porteur voyage : traitez-le comme un mot de passe

Contrairement au secret HMAC, il part à chaque requête et vit dans la configuration d'un service que nous ne contrôlons pas. C'est le prix du branchement direct, et c'est pourquoi il est cloisonné, expirant, révocable et muet en écriture par défaut. Ne le mettez jamais dans une URL ni dans un dépôt de code : les URL finissent dans les journaux de tous les intermédiaires traversés.

Les tentatives sont bornées à 20 échecs par quart d'heure et par adresse IP — au-delà, la réponse est un 429.

7.2 Alternative : clé d'API et signature HMAC

Le même point d'entrée accepte l'authentification décrite au §2 : clé d'API et signature HMAC. Elle a un avantage réel — le secret ne quitte jamais le serveur du marchand — et un inconvénient qui la réserve aux intégrateurs : aucun client MCP ne sait recalculer un HMAC à chaque appel, ils ne posent que des en-têtes fixes.

Il faut donc un pont : un petit programme lancé par l'assistant, qui reçoit le JSON-RPC sur son entrée standard, le signe, l'envoie et rend la réponse. Node.js 18 ou plus récent, aucune dépendance. Enregistrez-le sous pont-mcp.js.

#!/usr/bin/env node
'use strict';

// Pont MCP : entrée standard (JSON-RPC) → API signée → sortie standard.
const crypto = require('crypto');
const readline = require('readline');

const BASE = process.env.LG_API_URL;
const CLE = process.env.LG_API_KEY;
const SECRET = process.env.LG_API_SECRET;
const CHEMIN = '/api/v1/mcp';

readline.createInterface({ input: process.stdin }).on('line', async (ligne) => {
  const corps = ligne.trim();
  if (!corps) return;

  let requete;
  try { requete = JSON.parse(corps); } catch (e) { return; }

  // Une notification n'a pas d'identifiant, et n'attend AUCUNE réponse :
  // en écrire une romprait le protocole côté client.
  const attendUneReponse = requete.id !== undefined && requete.id !== null;

  const ts = Math.floor(Date.now() / 1000).toString();

  // On signe la chaîne qu'on envoie, pas un objet réencodé : réencoder
  // produirait tôt ou tard un échappement différent, donc un 401 inexplicable.
  const signature = crypto.createHmac('sha256', SECRET)
    .update(ts + 'POST' + CHEMIN + corps)
    .digest('hex');

  try {
    const reponse = await fetch(BASE + CHEMIN, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': CLE,
        'X-Timestamp': ts,
        'X-Signature': 'sha256=' + signature,
      },
      body: corps,
    });

    const texte = await reponse.text();
    if (attendUneReponse) process.stdout.write(texte + '\n');
  } catch (erreur) {
    if (attendUneReponse) {
      process.stdout.write(JSON.stringify({
        jsonrpc: '2.0',
        id: requete.id,
        error: { code: -32603, message: 'Pont MCP : ' + erreur.message },
      }) + '\n');
    }
  }
});

Déclaration côté client MCP (forme usuelle des fichiers de configuration) :

{
  "mcpServers": {
    "avis-clients": {
      "command": "node",
      "args": ["/chemin/absolu/vers/pont-mcp.js"],
      "env": {
        "LG_API_URL": "https://louis.guide",
        "LG_API_KEY": "ak_live_5c2f81b0",
        "LG_API_SECRET": "sk_live_3f9c1a7e5b2d48a6"
      }
    }
  }
}

Le secret ne sort pas de la machine. Il sert à signer localement ; ce qui part sur le réseau, c'est la signature. Un chemin absolu est indispensable : l'assistant ne lance pas le programme depuis le dossier où vous l'avez écrit.

Pour vérifier le pont avant de brancher quoi que ce soit, envoyez-lui une ligne à la main. Une liste d'outils doit revenir :

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  | LG_API_URL='https://louis.guide' LG_API_KEY='ak_live_…' LG_API_SECRET='sk_live_…' node pont-mcp.js

7.3 Méthodes

MéthodeEffet
initialize Annonce la version du protocole, les capacités et l'identité du serveur.
tools/listCatalogue des outils et de leurs schémas d'entrée.
tools/callExécute un outil — params.name et params.arguments.
notifications/initialized, pingAcquittés par un résultat vide.
POST /api/v1/mcp

{"jsonrpc":"2.0","id":1,"method":"initialize"}
HTTP/1.1 200 OK

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "avis-clients", "version": "1.0.0" }
  }
}

7.4 Les trois outils

Deux lisent, un écrit. La ligne de partage n'est pas technique : lire des avis ne présente aucun risque, tandis que publier une réponse fait parler le marchand en public sur une page que nous hébergeons — une formulation malheureuse sur un avis sensible, et c'est une capture d'écran qui circule.

C'est pourquoi tools/list ne renvoie que deux outils lorsque l'appelant présente un jeton en lecture seule. Ne codez donc pas la liste en dur : demandez-la, et n'annoncez au marchand que ce qu'elle contient.

outil lister_avis

Avis publiés sur l'enseigne, du plus récent au plus ancien. L'outil que l'assistant appelle pour « montre-moi les clients mécontents » ou « qu'est-ce qui n'a pas encore de réponse ? ».

ArgumentTypeDéfautEffet
note_maxentier 1–5— Ne remonte que les avis dont la note est inférieure ou égale.
sans_reponsebooléenfalse Écarte les avis auxquels une réponse a déjà été publiée.
limiteentier 1–5020 Nombre d'avis lus.
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "lister_avis",
    "arguments": { "note_max": 3, "sans_reponse": true, "limite": 10 }
  }
}

Le résultat est un bloc de texte contenant du JSON — c'est la forme que le protocole prévoit pour un résultat structuré, et celle que les assistants savent lire :

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"avis\":[{\"id\":\"9f1c2b3d-…\",\"note\":2,\"titre\":\"Colis abîmé\",\"commentaire\":\"…\",\"langue\":\"fr\",\"auteur\":\"Claire M.\",\"publie_le\":\"2026-08-13\",\"deja_repondu\":false}],\"total\":1}"
      }
    ],
    "isError": false
  }
}

sans_reponse filtre après la limite, pas avant. Demander 20 avis sans réponse lit les 20 derniers avis publiés puis en retire ceux qui ont déjà été traités : le résultat peut en compter beaucoup moins, et total le dit. Relevez limite pour élargir la fenêtre de lecture.

Ne remontent ici que les avis publiés : ni les avis en attente, ni les rejetés, ni les retirés. Pour ceux-là, c'est GET /cms/reviews et son filtre status.

outil resume_reputation

Vue d'ensemble, sans argument. Ce que l'assistant appelle pour « comment va ma réputation ? ».

{
  "boutique": "Tissu Fiesta",
  "note_moyenne": 4.6,
  "avis_publies": 318,
  "repartition": { "1": 4, "2": 6, "3": 18, "4": 91, "5": 199 },
  "avis_produit": 127
}

avis_publies compte les avis sur l'enseigne ; avis_produit compte séparément ceux qui portent sur un article. Les additionner donnerait un total qui ne correspond à aucune note affichée.

outil repondre_a_un_avis Écriture publique
ArgumentTypeOblig.Effet
avis_idchaîneouiIdentifiant de l'avis, tel que renvoyé par lister_avis.
contenuchaîneoui Texte de la réponse, tronqué à 3000 caractères.
{
  "avis_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  "publiee": false,
  "message": "Réponse enregistrée en brouillon. Le marchand doit la valider dans son espace avant qu'elle ne paraisse."
}

Publiée ou en brouillon : c'est le marchand qui l'a décidé, pas l'appel

Le marchand règle dans son espace si les réponses rédigées par un assistant partent directement ou attendent sa relecture. Aucun des deux comportements n'est bon dans l'absolu : celui qui reçoit deux avis par semaine veut relire, celui qui en reçoit deux cents veut que ça parte.

Ce réglage n'est pas un paramètre de la requête, et c'est essentiel : si l'assistant pouvait choisir lui-même s'il doit être relu, la garantie ne vaudrait plus rien. Le champ publiee et le champ message disent ce qui s'est réellement passé — un assistant doit l'annoncer tel quel au marchand.

Un avis déjà répondu voit sa réponse remplacée. Une réponse déjà publique le reste : une réécriture ne la renvoie jamais en brouillon, ce qui la ferait disparaître de la page sans décision de personne.

Ce qu'un assistant doit savoir avant de rédiger

  • Répondre dans la langue de l'avis — le champ langue est là pour ça. Une réponse en français sous un avis néerlandais dit au lecteur qu'il n'a pas été lu.
  • Ne jamais promettre un geste commercial qu'on ne peut pas engager : remboursement, renvoi, remise. Cette réponse est publique et opposable au marchand.
  • Aucun outil ne modifie ni ne supprime un avis, et il n'y en aura pas. Un assistant à qui l'on demande de « faire retirer » un avis ne peut que le signaler, avec un motif recevable (§4.6) — la note n'en est pas un.

7.5 Erreurs

Toujours un statut HTTP 200, y compris en cas d'erreur : en JSON-RPC, l'erreur voyage dans le corps. Un 4xx ferait croire au client que le transport a échoué, et la plupart réessaieraient au lieu d'afficher le message.

Seule exception : l'authentification, refusée avant d'atteindre la couche JSON-RPC. Elle répond avec l'enveloppe d'erreur habituelle de l'API.

StatutCodeCause
401invalid_mcp_token Jeton inconnu, révoqué ou expiré — indistinguables à dessein. Le marchand en recrée un dans son espace.
401codes de §2 Voie HMAC : clé absente, signature ou horodatage refusés.
429too_many_attempts Plus de 20 échecs d'authentification en quinze minutes depuis la même adresse. Attendez plutôt que de réessayer en boucle.
CodeSignificationÀ faire
-32001 La formule du marchand n'inclut pas l'accès MCP. Passer en formule payante ; les avis restent lisibles publiquement.
-32601Méthode JSON-RPC inconnue.Vérifier method.
-32602Outil inconnu.Appeler tools/list, ne pas coder les noms en dur.
-32603 Erreur du pont local — réseau, secret absent. Ce code vient du pont ci-dessus, pas du serveur.

Les erreurs métier d'un outil ne sont pas des erreurs JSON-RPC : la réponse reste un résultat, avec isError: true et un objet { "erreur": "…" } dans le texte. C'est le cas d'un avis introuvable, d'un contenu vide, ou d'une réponse tentée avec un jeton en lecture seule. L'assistant peut ainsi l'expliquer au marchand au lieu d'annoncer une panne.

8. Webhooks entrants

Il n'y a pas de webhook sortant

La plateforme ne vous appelle pas : elle n'émet aucune notification vers votre serveur à la publication d'un avis, d'une réponse ou d'une décision de modération. Pour suivre l'activité, interrogez GET /api/v1/cms/reviews à votre rythme, en filtrant sur status=published et en comparant published_at à votre dernier passage.

Un passage toutes les heures convient à la quasi-totalité des usages : les avis n'arrivent pas à la seconde, et le rythme de publication d'une boutique se compte en unités par jour. Interroger toutes les minutes ne fera rien apparaître plus vite.

Les deux points d'entrée ci-dessous existent pour des appelants précis — notre opérateur SMS et notre prestataire de paiement. Aucun intégrateur n'a à les appeler, et aucun ne peut le faire : ils sont l'un et l'autre fermés par un secret qui n'est pas distribué.

POST /api/v1/stripe/webhook Signature Stripe

Reçoit les événements d'abonnement : checkout.session.completed, customer.subscription.created, .updated et .deleted. C'est ce qui fait basculer un compte en formule payante, et donc ce qui ouvre l'API avis et l'accès MCP.

La signature de la charge utile est la seule chose qui protège cette route : sans elle, n'importe qui pourrait poster « abonnement actif » et s'offrir la formule payante d'une requête curl. Elle est vérifiée avant toute lecture du contenu, et un secret absent fait échouer la requête plutôt que de la laisser passer.

Les événements non traités sont acquittés par un 200 ({ "ignored": … }) : Stripe considère toute réponse non-2xx comme un échec et rejoue pendant trois jours, avec un intervalle croissant. Répondre 404 à un type d'événement dont nous n'avons pas l'usage provoquerait des milliers de renvois inutiles, puis la désactivation du point de terminaison de leur côté. À l'inverse, un échec réel de traitement répond bien 500 — là, on veut que Stripe rejoue plutôt que de laisser un marchand qui a payé en formule gratuite.

POST /api/v1/sms/inbound/{token} Jeton partagé

Reçoit les SMS entrants, c'est-à-dire les « STOP ». L'opérateur traite le mot-clé de son côté et cesse d'acheminer — mais sans ce point d'entrée, nous n'en saurions rien : nous continuerions à lui envoyer des messages facturés et jamais reçus, l'opposition disparaîtrait le jour d'un changement d'opérateur, et nous ne pourrions pas prouver l'avoir honorée alors que la charge de la preuve nous incombe.

Le jeton voyage dans le chemin, ce qui est plus faible qu'une signature — mais c'est ce que les interfaces des opérateurs français savent configurer. D'où le fait que ce point d'entrée ne puisse rien faire d'autre qu'ajouter une opposition : le pire qu'un appel frauduleux produise, c'est empêcher l'envoi de SMS à un numéro. Gênant, jamais dangereux, et réversible depuis le back-office.

Le mot-clé est cherché en premier mot du message, pas n'importe où dedans : quelqu'un qui écrit « il faut que ça s'arrête, ce magasin est nul » ne demande pas à se désinscrire, et le désinscrire d'office lui retirerait le canal par lequel on le sollicite légitimement. L'opposition est enregistrée pour tous les marchands : le message entrant ne dit pas de quelle boutique il s'agit — la personne répond au numéro d'envoi — et deviner serait à la fois faux et dangereux.

Elle ne coupe que le canal SMS. L'email continue de partir : c'est lui qui porte le lien de gestion de l'avis et les mentions obligatoires, et une opposition exprimée sur un canal ne vaut pas pour l'autre.

9. Limites de débit

Les limites sont calculées en fenêtre glissante : pas de compteur qui se remet à zéro à l'heure ronde, donc pas de rafale possible en début de période.

CanalLimiteCléPourquoi ce chiffre
API publique /api/v1/public/ 60 / minute Adresse IP Une fiche produit fait deux appels : cela laisse 30 chargements par minute depuis une même adresse. Large pour un visiteur, étroit pour un aspirateur de contenu.
Disponibilité /public/health aucune — Exclu volontairement : il est interrogé en continu par la supervision, et le brider ferait remonter de fausses alertes d'indisponibilité.
Ouverture de raccordement POST /pairing 10 / heure Adresse IP Chaque appel crée une ligne en base sans aucune authentification. Dix suffisent largement à un intégrateur qui recommence.
Sondage POST /pairing/{code} 240 / 15 minutes Adresse IP Généreux à dessein : le module interroge toutes les cinq secondes pendant que le marchand crée son compte, confirme son adresse et valide.
Authentification MCP par jeton 20 échecs / 15 minutes Adresse IP Ne compte que les échecs : un branchement qui fonctionne n'y touche jamais. Arrête le balayage de jetons trouvés ailleurs, et évite qu'un client mal configuré ne noie les journaux.
Dépôt d'un avis 10 / minute Jeton de l'invitation Par jeton et non par IP : plusieurs clients d'une même entreprise partagent souvent une adresse de sortie, et les brider ensemble punirait des dépôts légitimes.
Signalement public d'un avis 5 / heure Adresse IP Ouvert à tout lecteur (obligation DSA), donc à tout robot. Chaque envoi crée une ligne dans la file de modération.

L'API CMS n'est pas bridée, ce qui n'autorise pas tout

Aucune limite de débit n'est appliquée aujourd'hui aux endpoints signés (/api/v1/cms/ et MCP) : ils sont authentifiés, et le volume réel est borné par le quota de sollicitations du marchand. Traitez quand même le 429 — une limite pourra être ajoutée, et une intégration qui ne sait pas la lire tombera en panne le jour où elle apparaîtra.

En pratique : transmettez les commandes au fil de l'eau plutôt qu'en lot nocturne de plusieurs milliers, et interrogez les avis à l'heure plutôt qu'à la minute (§8). Un volume anormal est visible de notre côté et déclenche une prise de contact, pas une coupure silencieuse.

Ce que renvoie un dépassement

HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Remaining: 0

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Trop de requêtes. Réessayez dans quelques instants."
  }
}

Retry-After donne le nombre de secondes à attendre. Respectez-le : réessayer immédiatement ne fait que consommer la fenêtre suivante. Une temporisation exponentielle, plafonnée à une minute, suffit à tous les cas décrits ici.

Le sondage de raccordement fait exception et répond { "status": "rate_limited" } : c'est le même événement, exprimé dans le vocabulaire d'un endpoint que le module interroge en boucle.

10. Codes d'erreur communs

Deux formats, et un seul à traiter dans la plupart des cas

Partout dans l'API, une erreur porte la même enveloppe :

{
  "error": {
    "code": "invalid_api_key",
    "message": "Clé d'API inconnue, révoquée ou expirée."
  }
}

Le code est stable et destiné à votre programme ; le message est destiné à l'humain qui débogue et peut être reformulé sans préavis. Ne construisez jamais votre logique sur le texte du message.

Une seule exception : POST /cms/orders, servi par une couche différente, renvoie ses erreurs de validation sous forme de liste de violations. Un client robuste lit donc error.code s'il existe, et se rabat sur violations sinon.

Statuts HTTP

StatutSensFaut-il réessayer ?
200Succès. En JSON-RPC, l'erreur éventuelle est dans le corps.—
201Créé — commande enregistrée, réponse publiée.—
202Accepté mais non tranché : le signalement entre en file.—
400Requête illisible.Non, corrigez.
401Clé absente, invalide, ou signature refusée.Non, sauf horloge à resynchroniser.
402La formule du marchand n'inclut pas cette fonction.Non.
404Ressource inconnue — ou hors de votre compte.Non.
409Conflit : l'action a déjà été faite.Non, c'est un état, pas une panne.
415Content-Type absent ou inattendu.Non, envoyez du JSON.
422Requête bien formée mais refusée : champ manquant, valeur hors bornes.Non, corrigez.
429Débit dépassé.Oui, après Retry-After.
5xxIncident de notre côté.Oui, avec temporisation croissante.

Récapitulatif des codes

CodeStatutOùCause et remède
missing_api_key401CMS, MCP En-tête X-Api-Key absent.
invalid_api_key401CMS, MCP Clé inconnue, révoquée ou expirée — les trois sont volontairement indistinguables. Vérifiez-la dans l'espace marchand.
missing_signature, missing_timestamp 401CMS, MCP Écriture non signée. Voir §2.1.
invalid_timestamp401CMS, MCP X-Timestamp n'est pas un horodatage Unix en secondes — millisecondes ou date ISO, le plus souvent.
timestamp_out_of_range401CMS, MCP Plus de 300 s d'écart. Le message donne l'écart exact : synchronisez l'horloge (NTP).
signature_mismatch401CMS, MCP Reprenez les quatre pièges du §2.3, dans l'ordre.
invalid_mcp_token401MCP Jeton porteur inconnu, révoqué ou expiré — indistinguables. Le marchand en recrée un depuis son espace (§7.1).
too_many_attempts429MCP Trop d'échecs d'authentification depuis la même adresse.
plan_required402Avis, réponse Fonction incluse à partir de la formule payante. L'affichage public des avis, lui, reste gratuit.
merchant_not_found404API publique Identifiant public inconnu. Vérifiez le slug, pas le nom commercial.
review_not_found404Réponse, signalement Identifiant inconnu, mal formé, ou appartenant à un autre marchand : le cloisonnement impose de ne pas les distinguer.
already_reported409Signalement Un dossier est déjà ouvert sur cet avis.
content_required422Réponse content absent ou vide après nettoyage.
invalid_reason422Signalement Motif hors liste. Une note basse n'est pas un motif recevable (§4.6).
invalid_request422Raccordement shop_domain absent ou inexploitable.
rate_limit_exceeded429API publique Voir §9 et l'en-tête Retry-After.
-32001200MCP Formule sans accès MCP (erreur JSON-RPC, pas HTTP).
-32601, -32602200MCP Méthode ou outil inconnu. Passez par tools/list.

Trois symptômes, et par où commencer

SymptômeCause la plus fréquente
« Tout marchait hier, tout est en 401 aujourd'hui. » L'horloge du serveur a dérivé. GET /cms/ping renvoie server_time : comparez-le à la vôtre avant de chercher ailleurs.
« Le ping passe, mais toutes mes écritures échouent. » La clé est bonne, la signature non — c'est justement ce que ce partage de régime permet de conclure. Le corps a presque toujours été réencodé après avoir été signé (§2.3).
« Le widget n'affiche rien, mais l'API répond 200 en console. » Domaine non déclaré côté marchand : le navigateur bloque la lecture faute d'en-tête CORS. Ou, tout simplement, il n'y a pas encore d'avis : un widget vide se retire de la page (§6).

Si rien de tout cela ne colle

Écrivez-nous depuis l'espace marchand en joignant trois choses : le chemin appelé, l'horodatage de la requête, et le code d'erreur reçu. Avec ces trois éléments, la requête se retrouve dans les journaux ; sans eux, la seule réponse possible est de vous demander de les fournir.

↑ Revenir au début