1. Introduzione
La piattaforma espone tre canali distinti. Non condividono né lo stesso pubblico, né lo stesso regime di autenticazione, né gli stessi limiti. Scegliere quello giusto è la prima decisione di un'integrazione.
| Canale |
Prefisso |
Per chi |
Autenticazione |
| API CMS |
/api/v1/cms/ |
Moduli e-commerce, ERP, CRM, strumenti interni |
Chiave API + firma HMAC in scrittura |
| API pubblica |
/api/v1/public/ |
Widget di visualizzazione, JavaScript del negozio |
Nessuna — frequenza limitata, CORS ristretto |
| MCP |
/api/v1/mcp |
Assistenti IA (Claude, ChatGPT, altri) |
Stessa chiave, stessa firma dell'API CMS |
Prima di scrivere una riga di codice: verificate che un modulo non basti
I moduli PrestaShop e WooCommerce fanno per intero ciò che descrive questa pagina: trasmettono gli ordini al momento giusto, collocano lo script dei widget nel tema, mettono le stelle sulle schede prodotto e il blocco delle recensioni, e si occupano della firma delle richieste. L'esercente non incolla nulla e non scrive nulla.
Scaricare i moduli →
Questa documentazione si rivolge quindi a tre casi: una piattaforma per la quale non abbiamo ancora un modulo, uno sviluppo su misura, o il collegamento di uno strumento di terze parti (ERP, assistenza clienti, assistente IA) alle recensioni già raccolte.
Indirizzi di base
Tutti gli URL di questa pagina sono relativi all'indirizzo dell'API. Un modulo deve conoscere soltanto quello: gli altri indirizzi gli vengono restituiti da GET /api/v1/cms/me, il che gli evita di indovinarli e ci permette di cambiarli senza aggiornare alcunché presso gli esercenti.
| Uso | Indirizzo |
| API (tutti i canali) | https://louis.guide |
| Area esercente | https://louis.guide/app |
| Script dei widget | https://louis.guide/widget/v1/avis.js |
Convenzioni
- Formato — JSON in entrata come in uscita, codificato in UTF-8. L'intestazione
Content-Type: application/json è attesa su ogni richiesta dotata di corpo.
- Denominazione — serpente minuscolo (
external_order_id, experienced_at), la convenzione dominante delle API che consumano gli integratori PHP e JavaScript.
- Date — ISO 8601 con fuso esplicito in entrata (
2026-08-01T14:22:00+02:00). In uscita le date complete usano lo stesso formato; le date pubbliche di una recensione sono ridotte al giorno (2026-08-01) perché nessun widget mostra l'ora.
- Importi — trasmessi come stringa (
"129.90") e mai come numero a virgola mobile: un centesimo perso nell'arrotondamento su un ordine diventa uno scarto di fatturazione.
- Identificatori — gli oggetti che creiamo portano un UUID permanente. I vostri (ordine, prodotto, variante) restano i vostri: non li riscriviamo mai.
-
Errori —
sempre la stessa busta
{ "error": { "code": …, "message": … } }. Il code è stabile ed è destinato al vostro programma, il message all'essere umano che sta cercando l'errore. Vedere §10.
-
Versionamento —
il
/v1 del percorso è un contratto. Un campo facoltativo può esservi aggiunto in qualsiasi momento; nessun campo esistente sarà rinominato, rimosso o reso obbligatorio. Una rottura uscirebbe in /v2, con la versione precedente ancora servita — i moduli girano presso gli esercenti e nessuno può aggiornarli a distanza.
Il vostro codice deve dunque ignorare i campi che non conosce anziché fallire alla loro vista.
Ottenere una chiave API
-
Create un conto esercente su l'area esercente.
- Confermate l'indirizzo email, poi aprite la sezione delle chiavi API.
- Annotate il segreto: viene mostrato una sola volta. Una volta perso non si ritrova — si crea una nuova chiave e si revoca la precedente.
Un modulo di installazione non ha bisogno di questa manovra: apre esso stesso una richiesta di collegamento che l'esercente approva con un clic. Vedere §3.
2. Autenticazione
L'API CMS e il server MCP usano lo stesso meccanismo: una chiave che dice chi chiama, e una firma che prova che il chiamante possiede il segreto. Sono due cose distinte.
| Metodo HTTP | Intestazioni richieste | Perché |
GET, HEAD |
X-Api-Key |
Una lettura non modifica nulla: la chiave basta ad autorizzarla. |
POST, PUT, PATCH, DELETE |
X-Api-Key, X-Timestamp, X-Signature |
Una scrittura impegna l'esercente: deve essere provata e non riproducibile. |
Il segreto non viaggia mai
Viaggia solo la firma. Ciò chiude tre porte che non richiedono alcuna compromissione del negozio: la fuga passiva del segreto nei registri di un intermediario, la riproduzione di una richiesta intercettata e l'alterazione del corpo in transito. Non protegge invece da un negozio la cui banca dati sia stata sottratta — contro questo caso la difesa è la rotazione delle chiavi.
Non mettete mai il segreto in un URL : gli URL finiscono nei registri di tutti gli intermediari attraversati.
2.1 La firma, passo per passo
Passo 1 — Le tre intestazioni
| Intestazione | Contenuto |
X-Api-Key |
Identificatore pubblico della chiave, così come appare nell'area esercente. |
X-Timestamp |
Marca temporale Unix in secondi, solo cifre. Niente millisecondi, niente data ISO. |
X-Signature |
Il prefisso letterale sha256= seguito dall'HMAC-SHA256 in esadecimale minuscolo. Il prefisso fa parte del valore confrontato: ometterlo produce un rifiuto. |
Passo 2 — Costruire il carico da firmare
Quattro pezzi concatenati senza separatore, esattamente in questo ordine:
charge = X-Timestamp
+ MÉTHODE HTTP en majuscules
+ chemin logique de la requête
+ corps brut de la requête
| Pezzo | Regola esatta |
| Marca temporale |
La stringa identica a quella inviata in X-Timestamp. |
| Metodo |
POST, PUT… sempre in maiuscolo. |
| Percorso |
Il percorso senza schema, senza host, senza stringa di query, che inizia con / — per esempio /api/v1/cms/orders. Se l'API è servita da una sottocartella, tale prefisso di installazione non entra nella firma: vive nell'indirizzo di base, non nel percorso logico. |
| Corpo |
La sequenza di byte esattamente come viene inviata. Serializzate una volta, firmate quella stringa, inviate quella stringa. Corpo vuoto → stringa vuota. |
Passo 3 — Calcolare
X-Signature = "sha256=" + HMAC_SHA256(charge, secret) // hexadécimal minuscule
Passo 4 — Verificare la vostra implementazione su questo esempio
Questi valori sono fissi e la firma mostrata è realmente quella di questi dati: se il vostro codice ne produce un'altra, il problema è nel vostro codice, non nel nostro.
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 !"}
Carico da firmare (una sola riga, nessuno spazio aggiunto):
1786000000POST/api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response{"content":"Merci pour votre retour !"}
Risultato atteso:
X-Signature: sha256=8a9c0de10516226f965465704902e00c666892b483b45b361f4536a6b2ee36e9
Lo stesso calcolo in una riga di 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 e non echo: quest'ultimo aggiunge un a capo finale, che cambia la firma.
Passo 5 — Una chiamata completa con 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 e non --data: il secondo interpreta certi caratteri e può modificare il corpo inviato, invalidando quindi la firma.
2.2 Esempio in PHP
Il client minimo, senza dipendenze. È lo stesso meccanismo dei moduli PrestaShop e WooCommerce, ridotto all'essenziale.
<?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 Le quattro trappole
Quattro cause spiegano la quasi totalità dei signature_mismatch. Viste dall'esterno si somigliano tutte — da qui l'utilità di escluderle in quest'ordine.
- Il corpo è stato ricodificato dopo la firma. Il caso più frequente e il più difficile da vedere: un array serializzato due volte dà due stringhe diverse non appena contiene un accento o una barra (
JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES, ordine delle chiavi). Firmate la stringa, inviate quella stringa, non ricostruitela mai.
- Il percorso firmato porta un prefisso che non dovrebbe avere. Il percorso firmato è
/api/v1/cms/orders, anche se l'API è servita da https://esempio.it/piattaforma/api/v1/cms/orders. Il prefisso di installazione appartiene all'indirizzo di base. Al contrario, non firmate nemmeno l'URL completo con schema e host.
-
L'orologio del server è alla deriva.
Tolleranza: 300 secondi di scarto, in un senso come nell'altro. Oltre, la risposta è
timestamp_out_of_range e il suo messaggio indica lo scarto misurato in secondi — esattamente l'informazione da dare al vostro hoster. Questo caso si manifesta spesso come un'integrazione che « ieri funzionava ».
- Mancano il metodo o il prefisso. Il metodo entra nel carico in maiuscolo, e il valore di
X-Signature inizia con sha256=. Un HMAC nudo, senza prefisso, viene rifiutato.
Ciò che la stringa di query non fa
I parametri di URL (?page=2) non entrano nel carico firmato: vi figura solo il percorso. In pratica non hanno conseguenze, dato che gli endpoint firmati sono tutti scritture che portano i loro parametri nel corpo — ma un'implementazione che li aggiungesse al carico fallirebbe.
Riproduzione e finestra di validità
Il carico firmato copre la marca temporale, il metodo, il percorso e il corpo. Ometterne uno aprirebbe una falla: senza il percorso, una firma valida per POST /orders sarebbe riproducibile su DELETE /orders; senza la marca temporale, la richiesta sarebbe riproducibile all'infinito.
La finestra di 300 secondi è ciò che limita la riproduzione : una richiesta intercettata non può essere rispedita oltre. Non esiste un dizionario delle firme già viste — all'interno di quella finestra una richiesta identica è quindi accettata due volte. Ciò non ha effetto sulla trasmissione degli ordini, che è idempotente su external_order_id: la seconda riceve l'ordine già registrato e non invia una seconda email.
Risposte di autenticazione
Tutte queste risposte portano lo stato 401.
| Codice | Causa | Che fare |
missing_api_key |
Intestazione X-Api-Key assente. |
Aggiungere l'intestazione. |
invalid_api_key |
Chiave sconosciuta, revocata o scaduta. Il messaggio è volutamente identico nei tre casi: distinguerli permetterebbe di verificare in massa quali identificatori esistono. |
Verificare la chiave nell'area esercente, o crearne una nuova. |
missing_signature |
Scrittura senza intestazione X-Signature. |
Firmare la richiesta (§2.1). |
missing_timestamp |
Scrittura senza intestazione X-Timestamp. |
Aggiungere la marca temporale e firmarla. |
invalid_timestamp |
X-Timestamp non è una sequenza di cifre — millisecondi, data ISO o un segno. |
Inviare una marca temporale Unix in secondi. |
timestamp_out_of_range |
Più di 300 secondi di scarto. Il messaggio indica lo scarto esatto. |
Sincronizzare l'orologio del server (NTP). |
signature_mismatch |
La firma non corrisponde al carico atteso. |
Ripercorrere le quattro trappole del §2.3, nell'ordine. |
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "timestamp_out_of_range",
"message": "Marca temporale fuori tolleranza: +412 s di scarto rispetto al nostro server (massimo 300 s). L'orologio del vostro server è probabilmente desincronizzato."
}
}
3. Collegare un negozio
Questi due endpoint permettono a un modulo di recuperare una chiave senza che l'esercente debba ricopiare alcunché. Il modulo apre una richiesta, mostra un collegamento, l'esercente approva nel suo browser, e il modulo riceve la sua chiave e il suo segreto alla lettura successiva.
Sono senza autenticazione, per costruzione. La sicurezza non poggia su un'identità ma su tre cose: la richiesta non ottiene nulla finché un esercente connesso non l'ha approvata, il token di interrogazione non lascia mai il server del negozio, e il segreto è consegnato una sola volta. Il peggio che una chiamata malevola possa produrre è una richiesta in sospeso che nessuno approverà — e che scade in un quarto d'ora.
POST
/api/v1/pairing
Senza autenticazione
Apre una richiesta di collegamento e restituisce il collegamento di approvazione da presentare all'esercente.
| Campo | Tipo | Obbligatorio | Descrizione |
shop_domain | stringa | sì |
Dominio del negozio, es. negozio.esempio.it. |
platform | stringa | no |
prestashop, woocommerce, custom… unknown per impostazione predefinita. |
shop_name | stringa | no |
Nome leggibile del negozio, riutilizzato alla creazione del conto. |
platform_version | stringa | no |
Versione della piattaforma, es. 8.1.6. |
plugin_version | stringa | no |
Versione del modulo chiamante. |
shop_uid | stringa | no |
Identificatore unico estratto una volta all'installazione del modulo. Fortemente raccomandato con più negozi: vedere §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 — da aprire in una nuova scheda del browser dell'esercente, fuori dal suo back office. È lì che si connette o crea il suo conto, poi approva.
poll_token — da conservare unicamente lato server. Non deve mai comparire in una pagina né in un URL: è ciò che permetterà di recuperare il segreto.
code — mostrabile all'esercente, perché verifichi di approvare la richiesta giusta.
Codici d'errore
| Stato | Codice | Causa |
| 422 | invalid_request | shop_domain assente o inutilizzabile. |
| 429 | rate_limited | Più di 10 aperture per ora e per IP. |
POST
/api/v1/pairing/{code}
Senza autenticazione
Interroga lo stato della richiesta e consegna la chiave una volta — e una sola — che l'esercente abbia approvato.
In POST benché sia una lettura, perché la chiamata ha un effetto collaterale: consuma il segreto. In GET, un precaricatore del browser o un antivirus che segue i collegamenti lo consumerebbe al posto del modulo.
| Campo | Tipo | Obbligatorio | Descrizione |
poll_token | stringa | sì |
Il token ricevuto all'apertura della richiesta. |
curl -X POST 'https://louis.guide/api/v1/pairing/4K7M-9QR3' \
-H 'Content-Type: application/json' \
--data-raw '{"poll_token":"pt_8f2c1a7e5b2d48a6c3d9e0f1a2b3c4d5"}'
In attesa di approvazione:
HTTP/1.1 200 OK
{ "status": "pending" }
Approvato — le credenziali sono consegnate solo a questa chiamata:
HTTP/1.1 200 OK
{
"status": "approved",
"api_key": "ak_live_5c2f81b0",
"secret": "sk_live_3f9c1a7e5b2d48a6",
"merchant": "tissufiesta"
}
Valori possibili di status
| Valore | Significato | Che fare |
pending | L'esercente non ha ancora deciso. | Continuare a interrogare. |
approved | Approvato. La risposta porta le credenziali. | Registrarle, smettere di interrogare. |
rejected | L'esercente ha rifiutato. | Fermarsi e comunicarglielo. |
expired | Quindici minuti trascorsi senza decisione. | Riaprire una richiesta. |
consumed |
Il segreto è già stato consegnato, e ciò non avviene mai due volte. Il modulo ha perso la risposta. |
Ricominciare un collegamento — è il comportamento sicuro. |
unknown |
Codice sconosciuto oppure token errato. Volutamente indistinti: separarli farebbe di questo endpoint un oracolo capace di rivelare quali negozi si stanno collegando. |
Verificare la coppia codice / token. |
rate_limited | Troppe interrogazioni (stato HTTP 429). | Distanziare le chiamate. |
Registrate subito il segreto. Viene trasmesso solo in quella risposta. Un modulo che non riesce a conservarlo dovrà far ricominciare tutto il collegamento all'esercente.
Sempre 200, anche per uno stato di attesa. Il modulo interroga in ciclo: un codice HTTP d'errore su una situazione perfettamente normale farebbe salire allarmi per nulla. Interrogate ogni cinque secondi; il limite è di 240 chiamate per quarto d'ora e per IP — oltre, la risposta è { "status": "rate_limited" } con stato 429.
4. API CMS (firmata)
Il canale delle integrazioni server: moduli e-commerce, ERP, CRM, strumenti interni. Tutti gli URL sono preceduti da https://louis.guide.
Letture: la chiave basta. Scritture: chiave + firma. Il dettaglio del calcolo è al §2. Le schede qui sotto ricordano il regime di ciascuna con una pastiglia.
Ciò che l'API non permette, e non permetterà
Nessun endpoint modifica né elimina una recensione. L'esercente può rispondere pubblicamente e segnalare per moderazione, nient'altro — esattamente ciò che consente la sua area. Un'API più permissiva dell'interfaccia sarebbe una porta sul retro nella conformità, ed è la prima cosa che verifica un audit.
GET
/api/v1/cms/ping
Chiave API
Verifica che una chiave funzioni. È la prima chiamata da scrivere, e quella da proporre all'esercente sotto forma di pulsante « Testare la connessione »: meglio che scopra una chiave errata alla configurazione anziché al primo ordine non trasmesso.
Senza firma, volutamente. Una lettura non modifica nulla, e soprattutto: questo endpoint deve restare utilizzabile per provare che una chiave sia buona mentre l'implementazione HMAC è ancora errata. Il ping passa, la scrittura no: il problema è nella firma, non nella chiave.
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 è restituito per una ragione precisa: confrontatelo con l'orologio del vostro server. Uno scarto superiore a 300 secondi farà fallire tutte le vostre scritture firmate (§2.3), ed è qui che lo si vede prima di perderci una giornata.
GET
/api/v1/cms/me
Chiave API
Stato del conto: identità dell'esercente, capacità del piano, quota e indirizzi della piattaforma.
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/it/m/tissufiesta"
},
"server_time": "2026-08-13T14:52:07+00:00"
}
Leggete le capacità, non il nome del piano
Il blocco plan espone delle capacità (can_…) oltre al codice del piano. Verificate le prime: un modulo che scrive in duro if (plan === 'pro') smetterà di essere corretto il giorno in cui un piano si aggiunge o cambia nome, presso tutti gli esercenti nello stesso momento e senza che nessuno possa correggerlo.
| Capacità | Ciò che governa |
can_display_product_reviews |
Visualizzazione delle recensioni prodotto — le stelle sulle schede. |
can_use_photos |
Uscita delle foto dei clienti tramite l'API. Sono raccolte già dal piano gratuito ma sono servite solo a pagamento: presso un conto gratuito la galleria risponde con un elenco vuoto, mai con un errore. |
can_use_reviews_api |
Lettura delle recensioni tramite l'API, risposta alle recensioni e accesso MCP. La trasmissione degli ordini non è invece interessata: è inclusa in tutti i piani. |
can_remove_branding |
Rimozione della menzione della piattaforma su widget ed email. |
Il blocco urls evita di indovinare
La vostra integrazione deve conoscere un solo indirizzo: quello dell'API. Gli altri — area esercente, script dei widget, pagina pubblica dell'esercente — sono restituiti qui. Un modulo che li ricompone a partire da una base unica presuppone che tutto viva sullo stesso host, cosa che smette di essere vera non appena un canale passa a un sottodominio, e produce collegamenti morti presso tutti gli esercenti già installati.
quota.remaining merita una visualizzazione nella vostra interfaccia: a zero gli ordini continuano a essere accettati ma non parte più alcun sollecito. Avvisare al 90 % di consumo evita all'esercente di scoprirlo dalle sue statistiche.
POST
/api/v1/cms/orders
Firma richiesta
L'endpoint centrale. Registra un ordine e pianifica la richiesta di recensione. Tutto il resto della piattaforma discende da questa chiamata: senza di essa non c'è né sollecito, né recensione, né voto.
Idempotente su external_order_id
Riemettere lo stesso riferimento restituisce l'ordine già registrato con uno stato 200 anziché 201, senza creare un duplicato e senza inviare una seconda email al cliente. Il campo idempotent della risposta vale allora true. Potete dunque riprovare senza precauzioni dopo un'interruzione di rete o un tempo di attesa superato — è il comportamento da preferire a qualsiasi logica di deduplicazione fatta in casa.
Quando chiamare
Nel momento in cui l'esperienza è vissuta, non ordinata: alla consegna, alla spedizione a seconda del vostro mestiere, o al passaggio allo stato che ne fa le veci. È experienced_at a portare questa data, ed è essa a far partire il termine di sollecito.
Corpo della richiesta
Radice
| Campo | Tipo | Obbl. | Descrizione |
external_order_id | stringa (100) | sì |
Riferimento dell'ordine presso di voi. Chiave di idempotenza e prova d'acquisto conservata cinque anni (AFNOR §6.3). Deve essere stabile nel tempo. |
customer | oggetto | sì |
Identità del cliente da sollecitare — vedere la tabella seguente. |
experienced_at | ISO 8601 | sì |
Data di consegna o di consumo, con fuso esplicito. Vedere il riquadro qui sotto: non è la data dell'ordine. |
source | oggetto | sì |
Contesto tecnico dell'emissione — vedere più sotto. |
items | array (200 max) | no |
Articoli. Senza di essi non sarà chiesta alcuna recensione prodotto — soltanto quella sull'insegna. |
amount | stringa decimale | no |
Importo totale, es. "129.90". Mai un numero a virgola mobile. |
currency | ISO 4217 | no |
"EUR", "CHF"… |
channel | enumerazione | no |
ecommerce_order (predefinito), pos_transaction, qr_scan, manual, csv_import, nfc. |
location_id | stringa (100) | no |
Sede interessata, così come dichiarata dall'esercente. Un valore sconosciuto fa fallire la richiesta con un 422 anziché collegare l'ordine al punto vendita sbagliato. |
solicitation_delay_days | intero 0–365 | no |
Termine proprio di questo ordine, che scavalca l'impostazione del conto. Utile quando uno stesso venditore spedisce un mazzo di fiori da sollecitare domani e un materasso da sollecitare tra un mese. Fuori limite il valore è ignorato e si applica l'impostazione del conto — un valore aberrante non deve far perdere un ordine. |
order_status_id | stringa (20) | no |
Stato dell'ordine presso di voi al momento dell'invio. Puramente diagnostico — non lo interpretiamo — ma è l'unica informazione che permetta di rispondere a « perché quest'ordine non ha innescato nulla ». |
order_status_label | stringa (120) | no |
Etichetta leggibile di questo stato. |
experienced_at: la data di consegna, non quella dell'ordine
Un pacco ordinato il 1º e consegnato il 6 porta il 6. Non è una sottigliezza: due studi randomizzati su oltre 300 000 consumatori (Jung, Ryu, Han & Cho, Journal of Marketing, 2023) stabiliscono che un sollecito inviato prima che il cliente abbia potuto formarsi un'opinione ha un effetto negativo sul tasso di deposito. Ancorare il termine alla data dell'ordine equivale a sollecitare sistematicamente troppo presto, di tutto il tempo di consegna.
È anche una delle tre date mostrate pubblicamente accanto alla recensione (AFNOR §6.3).
customer
| Campo | Tipo | Obbl. | Descrizione |
email | email (255) | sì |
L'unico dato personale in chiaro che accettiamo. Cancellato dopo il termine di deposito; ne sopravvive solo l'impronta. |
country | ISO 3166-1 alpha-2 | no* |
*Fortemente raccomandato. Google calcola i suoi voti esercente per paese e scarta le recensioni il cui paese sia sconosciuto. Questa informazione esiste solo al momento dell'ordine: una volta epurato l'indirizzo, è definitivamente irrecuperabile, senza alcun recupero possibile. |
locale | fr, en, nl, de, it, es | no |
Lingua dell'email di sollecito. In mancanza, la lingua predefinita dell'esercente — sollecitare un cliente neerlandofono in francese fa crollare il tasso di risposta. |
phone | stringa (32) | no |
Cellulare per il sollecito via SMS. Formato internazionale (+33612345678) fortemente raccomandato: è il solo non ambiguo. Un numero nazionale è convertito a partire da country; senza paese noto è scartato senza far fallire l'ordine. Vedere l'avvertenza qui sotto. |
first_name, last_name | stringa (100) | no |
Personalizzazione del sollecito e nome mostrato dell'autore. |
company | stringa (255) | no |
Ragione sociale, per un ordine professionale. |
postal_code, city | stringa | no |
Epurati insieme all'indirizzo email. |
Inviate il numero di cellulare solo se l'esercente ha sottoscritto l'SMS. Senza l'opzione viene ricevuto e conservato senza che parta alcun messaggio: un dato personale raccolto senza finalità, cosa che nessuna delle due parti può giustificare in caso di controllo.
source — obbligatorio
Questo blocco non è statistica. Quando un esercente scrive « le mie recensioni non partono più dall'aggiornamento », la risposta è già lì dentro: versione della piattaforma, versione del modulo, evento scatenante. Renderlo facoltativo equivarrebbe a non averlo mai — gli integratori compilano ciò che è richiesto, non ciò che è suggerito.
| Campo | Tipo | Obbl. | Descrizione |
platform | stringa (50) | sì |
prestashop, woocommerce, shopify, magento, custom… |
platform_version | stringa (30) | no |
Es. 8.1.6. |
plugin_version | stringa (30) | no |
Versione della vostra integrazione. Da incrementare a ogni rilascio. |
trigger | stringa (100) | no |
Evento all'origine dell'invio, es. woocommerce_order_status_completed. Permette di capire perché un ordine parta troppo presto o troppo tardi. |
shop_uid | stringa (80) | no* |
*Decisivo con più negozi. Identificatore estratto una volta all'installazione e conservato. Vedere il riquadro. |
shop_id | stringa (50) | no |
Identificatore del negozio presso la piattaforma. Serve da ripiego quando shop_uid è assente. |
shop_name | stringa (255) | no |
Nome leggibile di questo negozio. Senza di esso, l'esercente scopre nella sua area una sede chiamata « 3 » e deve indovinare quale sia. |
shop_group_id, lang_id | stringa | no |
Conservati per la diagnosi, mai interpretati. lang_id non separa nulla: la lingua della recensione viene da customer.locale. |
Più negozi: shop_id non basta
Vale « 1 » su qualsiasi installazione a negozio singolo. Un esercente che gestisce due siti sotto lo stesso conto — un marchio per dominio, caso corrente — invierebbe quindi « 1 » da entrambi: i due negozi si fonderebbero in una sola sede, le recensioni dell'uno comparirebbero sulla pagina dell'altro, e il nome mantenuto sarebbe quello dell'ultimo ordine ricevuto. Difetto constatato in collaudo su due PrestaShop reali.
shop_uid risolve il problema: estraetelo una volta all'installazione, conservatelo. Sopravvive a un cambio di dominio come a un rinnovo di chiave — i due altri discriminanti a cui si pensa per primi, e che si muovono entrambi.
items[] — facoltativo, 200 articoli al massimo
| Campo | Tipo | Obbl. | Descrizione |
external_product_id | stringa (100) | sì |
Identificatore del prodotto nel vostro catalogo. |
name | stringa (255) | sì |
Nome del prodotto così come mostrato al cliente. |
variant_id | stringa (100) | no* |
*Il campo più importante di questo elenco. Senza di esso, la sedia rossa e la sedia blu condividono la stessa chiave prodotto: le loro recensioni si mescolano e « la gamba si è rotta » non designa più nulla. Corrisponde a id_product_attribute (PrestaShop), alla variazione (WooCommerce), al variant (Shopify). |
variant_label | stringa (255) | no |
Etichetta leggibile: « Colore: rosso, Taglia: L ». |
gtin | da 8 a 14 cifre | no* |
EAN-13 o UPC-A convertito. Chiave di aggregazione tra esercenti, e requisito di Google per mostrare le stelle nei suoi risultati. |
upc, isbn, mpn | stringa | no |
Tenuti separati dal GTIN perché i cataloghi li tengono in colonne distinte. L'ISBN è decisivo per il libro, dove il GTIN è spesso vuoto. |
sku, brand | stringa | no |
Riferimento interno e marca. |
category_id, category_name | stringa | no |
Categoria principale nel vostro catalogo. |
product_url, image_url | URL (500) | no |
Usate nell'email di sollecito: un'immagine di prodotto migliora nettamente il tasso di deposito. |
images | elenco di URL (10 max) | no |
Immagini supplementari. |
description | stringa (5000) | no |
Ricevuta, mai rimostrata tale e quale: è il vostro testo, non quello dell'autore della recensione. Serve a situare il prodotto in moderazione. |
tags | elenco (30 max) | no |
Parole chiave del prodotto, 60 caratteri ciascuna. |
meta_title, meta_description | stringa | no |
Metadati della scheda. |
quantity | intero > 0 | no |
1 per impostazione predefinita. |
unit_price | stringa decimale | no |
Es. "19.90". In stringa, come tutti gli importi. |
Esempio completo
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"
}
}
Ordine registrato:
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
}
Stessa richiesta riprodotta:
HTTP/1.1 200 OK
{ "…": "…", "idempotent": true }
L'indirizzo email non è mai restituito, anche se l'avete appena trasmesso: ogni dato restituito è un dato che può trapelare nei vostri stessi registri.
Valori possibili di status
| Valore | Significato |
pending | Ricevuto, in attesa di pianificazione. |
scheduled | Sollecito programmato. |
solicited | Richiesta di recensione inviata al cliente. |
reviewed | Il cliente ha depositato la sua recensione. |
cancelled | Annullato prima dell'invio. |
expired | Termine di deposito trascorso senza recensione. |
Errori
Questo endpoint è l'unico servito da API Platform: i suoi errori di validazione arrivano quindi sotto forma di elenco di violations, e non nella busta { "error": … } del resto dell'API. Il vostro codice deve accettare entrambe le forme.
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."
}
]
}
| Stato | Causa | Che fare |
| 401 |
Chiave assente, non valida, o firma rifiutata. |
Vedere §2. |
| 422 |
Un campo manca o è malformato — vedere violations. |
Correggere il campo indicato da propertyPath. |
| 422 |
experienced_at è nel futuro (oltre un giorno di margine). |
Verificare il fuso orario del server: è quasi sempre da lì che viene lo scarto. |
| 422 |
experienced_at risale a più di 90 giorni. |
Vedere il riquadro qui sotto. Per riprendere uno storico, contattate l'assistenza. |
| 422 |
Nessuna sede corrisponde a location_id. |
Creare la sede nell'area esercente, oppure omettere il campo. |
| 415 |
Intestazione Content-Type assente o inattesa. |
Inviare Content-Type: application/json. |
Perché gli ordini di più di 90 giorni sono rifiutati
Lo scenario di sinistro è noto: un modulo si installa e spinge tre anni di storico in una volta. Migliaia di inviti partono verso indirizzi scaduti, il tasso di rifiuto esplode — e poiché tutte le email partono dal nostro dominio, è la recapitabilità di tutti gli esercenti a crollare, non solo quella del nuovo arrivato.
Il rifiuto è pronunciato all'ingresso, con un messaggio esplicito, anziché alla pianificazione: l'integratore capisce immediatamente invece di vedere i suoi ordini sparire in silenzio.
GET
/api/v1/cms/reviews
Chiave API
Piano a pagamento
Elenca le recensioni dell'esercente, dalla più recente alla più antica, con la risposta pubblicata e l'eventuale segnalazione di ciascuna. È questo endpoint che permette di riportare le recensioni in un ERP, un CRM o uno strumento di assistenza clienti.
| Parametro | Predefinito | Descrizione |
type | merchant |
merchant per le recensioni sull'insegna, product per le recensioni prodotto. |
status | tutti |
published, pending, awaiting_email, rejected, disputed, withdrawn. Un valore sconosciuto è ignorato — il filtro allora non si applica, anziché restituire un errore. |
page | 1 | Numero di pagina. |
per_page | 25 |
Da 1 a 100. Oltre, il valore è riportato a 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
}
Le tre date, e perché sono tre
experienced_at (l'esperienza vissuta), submitted_at (il deposito) e published_at (la messa in linea) sono tre cose diverse, e l'AFNOR impone di poterle distinguere. Un integratore che le confonde mostra « 3 giorni fa » su un'esperienza vecchia di tre settimane. published_at vale null finché la recensione non è pubblicata.
order_reference riprende il vostro external_order_id: è ciò che collega la recensione all'ordine nel vostro sistema. Vale null per una recensione depositata al di fuori di un sollecito.
Sincronizzazione incrementale
Interrogate con status=published e confrontate published_at con l'ultimo passaggio: riportare tutto lo storico a ogni esecuzione funziona i primi mesi, poi diventa una richiesta di diverse migliaia di righe ogni ora. La paginazione comincia da 1 e il campo total dà il numero di recensioni corrispondenti al filtro, non il numero di pagine.
| Stato | Codice | Causa |
| 402 | plan_required |
Il piano dell'esercente non include l'API recensioni. Le recensioni restano leggibili senza chiave tramite l'API pubblica — non è la stessa cosa: quella serve la visualizzazione pubblica, non l'esportazione. |
POST
/api/v1/cms/reviews/{uuid}/response
Firma richiesta
Piano a pagamento
Pubblica una risposta pubblica a una recensione sull'insegna, o aggiorna quella già esistente. Una recensione porta una sola risposta: riemettere sostituisce il testo.
| Campo | Tipo | Obbl. | Descrizione |
content | stringa | sì |
Testo della risposta. Troncato a 3000 caratteri senza errore — verificate la lunghezza dal vostro lato se il taglio vi disturba. |
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
}
}
Leggete published prima di annunciare alcunché
L'esercente imposta nella sua area se le risposte scritte da un programma partano direttamente o attendano la sua rilettura. Questa impostazione vive sul conto e non è un parametro della richiesta: se il chiamante potesse scegliere da sé se debba essere riletto, la garanzia non varrebbe nulla.
Conseguenza per la vostra interfaccia: una risposta accettata non è necessariamente visibile. published: false significa « salvata in bozza, da approvare nell'area esercente » — ditelo, anziché mostrare un « pubblicato » che sarà smentito dalla pagina pubblica.
201 alla creazione, 200 all'aggiornamento; il campo created riporta la stessa informazione nel corpo. Una risposta già pubblicata lo resta: un aggiornamento non la riporta mai in bozza, il che la farebbe sparire dalla pagina senza che nessuno l'abbia deciso.
| Stato | Codice | Causa |
| 402 | plan_required | Piano senza risposta alle recensioni. |
| 404 | review_not_found |
Identificatore sconosciuto, malformato o appartenente a un altro esercente — i tre casi sono indistinguibili, ed è la compartimentazione a imporlo. |
| 422 | content_required | content assente o vuoto. |
POST
/api/v1/cms/reviews/{uuid}/report
Firma richiesta
Segnala una recensione per moderazione. La recensione passa allo stato « contestata » e il fascicolo entra nella coda d'istruttoria.
| Campo | Tipo | Obbl. | Descrizione |
reason | enumerazione | sì |
Motivo, da scegliere nell'elenco qui sotto. |
detail | stringa | no |
Precisazioni per il moderatore, troncate a 1000 caratteri. È qui che si scrive « ordine n. X, mai consegnato a questo indirizzo » — una segnalazione motivata è istruita più in fretta. |
Motivi ammissibili
| Valore | Quando invocarlo |
inappropriate_content | Ingiuria, discorso d'odio, contenuto illecito. |
spam_or_advertising | Pubblicità, collegamento commerciale, contenuto automatizzato. |
off_topic | Senza rapporto con l'esperienza vissuta — il corriere, il tempo. |
conflict_of_interest | Concorrente, ex dipendente, recensione retribuita. |
personal_data_disclosure | La recensione espone dati personali. |
Un voto basso non è un motivo
Nessun motivo permette di contestare una recensione a causa del suo voto, e non è una dimenticanza: è il divieto che fa la differenza tra una piattaforma di recensioni e una vetrina. Una segnalazione mal motivata è respinta, e la recensione resta in linea.
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 vale sempre true, e il campo esiste perché nessuna interfaccia sia concepita presupponendo il contrario: la recensione resta pubblica per tutta l'istruttoria. Toglierla su semplice segnalazione equivarrebbe a lasciare che l'esercente svaluti ciò che gli dispiace — Google lo vieta esplicitamente, l'AFNOR anche. La risposta è un 202: la domanda è registrata, non decisa.
| Stato | Codice | Causa |
| 404 | review_not_found | Identificatore sconosciuto, malformato, o fuori dal vostro conto. |
| 409 | already_reported | Una segnalazione è già aperta su questa recensione. |
| 422 | invalid_reason |
Motivo assente o fuori elenco. Il messaggio ricorda i valori ammessi. |
5. API pubblica
Sola lettura, senza autenticazione, su /api/v1/public/. È ciò che consumano i widget, e ciò che può consumare qualsiasi visualizzazione su misura.
Lo {slug} dei percorsi è l'identificatore pubblico dell'esercente — quello della sua pagina pubblica, visibile in urls.profile restituito da /cms/me.
Ciò che protegge un'API senza chiave
Non c'è identità da verificare: questo codice gira presso i visitatori di un negozio, nessun segreto può viverci. La protezione poggia dunque su tre altre cose, che occorre conoscere prima di integrare.
-
Da qui non esce alcun dato sensibile. Né email, né impronta di email, né riferimento d'ordine, né identificatore interno. Un widget mostra recensioni pubbliche; tutto ciò che esce da questo canale è leggibile da chiunque.
-
Frequenza limitata a 60 richieste al minuto e per indirizzo IP, su finestra scorrevole. Una scheda prodotto fa due chiamate: ciò lascia 30 caricamenti al minuto dallo stesso indirizzo — ampio per un visitatore, stretto per un aspiratore di contenuti. Vedere §9.
-
CORS ristretto ai domini dichiarati dell'esercente. Un jolly
* autorizzerebbe qualsiasi sito — concorrente, comparatore, contraffattore — a mostrare le recensioni di qualunque esercente come se fossero le proprie.
CORS: cosa dichiarare perché il browser accetti la risposta
L'intestazione Access-Control-Allow-Origin è posta solo se l'origine chiamante corrisponde a un dominio collegato all'esercente indicato nell'URL. I sottodomini sono accettati: un dominio dichiarato esempio.it autorizza www.esempio.it e negozio.esempio.it.
Sintomo tipico di un dominio non dichiarato: la richiesta parte, il server risponde 200, e il browser blocca la lettura in console. Il rimedio sta nell'area esercente, non nel codice.
Una chiamata da server a server non è interessata : senza intestazione Origin, non c'è controllo CORS. Quel caso è coperto dalla limitazione di frequenza. Il CORS protegge il browser da un altro sito, mai il dato stesso.
Cache
Tutte le risposte sono pubbliche e messe in cache: 60 secondi per le recensioni e i voti, 300 secondi per le impostazioni di visualizzazione. È ciò che permette di assorbire il traffico di un negozio in promozione senza dimensionare per il picco. Non costruite una visualizzazione che presupponga la comparsa istantanea di una recensione pubblicata.
GET
/api/v1/public/merchants/{slug}/score
Senza autenticazione
Voto globale del negozio e ripartizione per voto.
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 è restituito esplicitamente anziché sottinteso: un integratore che programma « su 10 » perché il suo precedente fornitore lo era produce una visualizzazione falsa che nessuno rilegge. count conta soltanto le recensioni pubblicamente visibili.
404 merchant_not_found se lo slug è sconosciuto.
GET
/api/v1/public/merchants/{slug}/reviews
Senza autenticazione
Recensioni sull'insegna, dalla più recente alla più antica.
| Parametro | Predefinito | Descrizione |
page | 1 | Numero di pagina. |
per_page | 10 |
Da 1 a 50. Oltre, riportato a 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'ordine cronologico è imposto, non scelto
Non esiste un parametro di ordinamento, e non ne esisterà: l'AFNOR (§6.3) esige l'ordine cronologico inverso come visualizzazione predefinita. Proporre « i meglio votati per primi » come ordinamento iniziale sarebbe una presentazione orientata. Un ordinamento in JavaScript sulla pagina ricevuta rientra nella vostra responsabilità, non nella nostra.
Ciò che la vostra visualizzazione deve riprendere
-
Due date come minimo — quella dell'esperienza e quella della pubblicazione. È un obbligo di visualizzazione, e solo l'API può fornirvele. Le date pubbliche sono ridotte al giorno (
2026-08-06): nessun widget mostra l'ora.
verified_purchase — la recensione è collegata a un ordine reale. È ciò che distingue una recensione raccolta da una depositata spontaneamente.
-
disputed — la recensione è contestata e la sua istruttoria è in corso. Resta visualizzata (vedere §4.6); segnalatela anziché nasconderla.
reply — la risposta dell'esercente fa parte della recensione per il lettore. published_at vi porta la data dell'ultima modifica quando ve n'è stata una: mostrare la data d'origine sotto un testo riscritto trarrebbe in inganno.
photos — vuoto presso un esercente il cui piano non le serve. Le recensioni restano complete, mancano solo le immagini.
Su questo canale non esiste un campo total: una pagina vuota significa che non c'è più nulla da caricare. È ciò che fa il pulsante « vedi altro » del widget.
GET
/api/v1/public/products/{slug}/{productId}/score
Senza autenticazione
Voto di un prodotto. {productId} è il vostro identificatore di catalogo, quello trasmesso in external_product_id — non lo riscriviamo mai. Ricordate di codificarlo se contiene caratteri riservati.
| Parametro | Predefinito | Descrizione |
variant | — |
Restringe il voto a una variante. Se assente, il voto riguarda tutte le varianti insieme — che è il comportamento giusto finché il visitatore non ha scelto la sua taglia. |
with_variants | false |
Aggiunge il dettaglio per variante. Costa una richiesta in più: non attivatelo sulla scheda prodotto, che è la pagina più vista del negozio. |
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 vale null quando with_variants non è richiesto — è un'assenza di calcolo, non un'assenza di varianti.
GET
/api/v1/public/products/{slug}/{productId}/reviews
Senza autenticazione
Recensioni di un prodotto. Stessa struttura di risposta e stessi parametri di paginazione delle recensioni sull'insegna, più il filtro variant — utile quando un selettore di taglia vuole mostrare solo le recensioni della variante scelta.
curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/reviews?variant=REF-42-ROUGE-L&per_page=5'
Presso un esercente il cui piano non includa la visualizzazione delle recensioni prodotto, prevedete una visualizzazione che si riduca in modo pulito anziché una cornice vuota: il widget, dal canto suo, sparisce dalla pagina.
GET
/api/v1/public/merchants/{slug}/photos
Senza autenticazione
Foto dei clienti approvate, senza il testo delle recensioni. È ciò che alimenta un carosello: senza questo endpoint bisognerebbe caricare cinquanta recensioni complete — testo, date, voti — per conservarne solo le immagini, su una scheda prodotto che carica già il tema dell'esercente.
| Parametro | Predefinito | Descrizione |
produit | — |
Restringe a un prodotto. Se assente, restituisce le foto di tutto il negozio — il che alimenta un carosello di pagina iniziale. |
limite | 24 |
Da 1 a 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
}
]
}
Gli URL sono assoluti: questo JSON è letto da JavaScript eseguito sul dominio del negozio, dove un URL relativo punterebbe al negozio stesso. Servitevi di width e height per riservare lo spazio prima del caricamento — altrimenti la scheda prodotto salterà sotto gli occhi del visitatore.
Presso un esercente il cui piano non serve le foto, la risposta è { "photos": [] } con stato 200, mai un errore: il carosello sparisce in modo pulito anziché mostrare una cornice fallita.
GET
/api/v1/public/merchants/{slug}/display
Senza autenticazione
Impostazioni di visualizzazione decise dall'esercente nella sua area. È ciò che permette di collocare i tag una volta per tutte in un tema, poi di accendere, spegnere o spostare un elemento senza toccare il codice del negozio.
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/it/m/tissufiesta"
}
| Impostazione | Predefinito | Significato |
badge_flottant | false |
Pastiglia di voto fissata in un angolo dello schermo. Spenta per impostazione predefinita: si sovrappone alla pagina dell'esercente, e nulla deve comparire da lui senza che l'abbia chiesto. |
badge_cote | droite | droite oppure gauche. |
badge_decalage | 16 | Scostamento in pixel rispetto al bordo. |
seuil_avis | 1 |
Numero di recensioni sotto il quale la visualizzazione sparisce. Vedere §6: mostrare « nessuna recensione » è peggio che non mostrare nulla. |
etoiles_fiche | true | Stelle sulla scheda prodotto. |
etoiles_vignettes | true | Stelle sulle miniature degli elenchi. |
onglet_avis | true | Scheda « Recensioni » della pagina prodotto. |
bloc_accueil | true | Blocco di recensioni in pagina iniziale. |
display è sempre completo, valori predefiniti inclusi: il vostro codice non deve conoscere i nostri valori predefiniti né ricopiarli — il giorno in cui uno cambierà, li seguirà.
accent_color vale null quando l'esercente non ha scelto un colore oppure quando il suo piano non lo consente più. Prevedete sempre un colore di ripiego dal vostro lato: è ciò che fa il widget, la cui tinta esiste solo se dichiarata.
GET
/api/v1/public/health
Senza autenticazione
Disponibilità del servizio. Da interrogare con una sonda o con il controllo di salute di un modulo.
HTTP/1.1 200 OK
{ "status": "ok" }
Volutamente minimo: nessun accesso alla banca dati, nessuna dipendenza esterna. Una lentezza della banca dati non deve scatenare un falso allarme di indisponibilità — e viceversa, questo endpoint non dice nulla sullo stato della banca dati. Per verificare che una chiave funzioni, è /cms/ping che va chiamato.
6. Widget di visualizzazione
Quattro elementi HTML da collocare in un tema. Un solo script da caricare, nessuna dipendenza, nessuna configurazione: l'indirizzo dell'API è dedotto dall'URL dello script stesso.
<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>
Lo script è caricato una volta per pagina, dove si vuole; gli elementi possono essere collocati prima di esso. È servito sotto /widget/v1/: un'evoluzione incompatibile uscirebbe in /v2/ e questo resterebbe servito tale e quale — vive in temi che nessuno aggiornerà.
I quattro elementi
| Elemento | Ciò che mostra | Dove collocarlo |
<avis-score> |
Voto medio, stelle, numero di recensioni. |
Scheda prodotto, intestazione del negozio, pagina « chi siamo ». |
<avis-liste> |
Recensioni paginate, con foto e risposte dell'esercente. |
Scheda « Recensioni » di una pagina prodotto, pagina dedicata. |
<avis-carrousel> |
Solo le foto dei clienti, cliccabili. |
Scheda prodotto, pagina iniziale. |
<avis-flottant> |
Pastiglia di voto fissata in un angolo, cliccabile. |
Il modello comune, una sola volta per tutto il sito. |
Attributi
| Attributo | Elementi | Predefinito | Ruolo |
marchand | tutti | — |
Obbligatorio. Identificatore pubblico dell'esercente, quello della sua pagina pubblica. |
produit |
score, elenco, carosello | — |
Il vostro identificatore di catalogo (external_product_id). Se assente, l'elemento riguarda tutto il negozio. |
langue | tutti | lang della pagina |
Lingua delle etichette. In mancanza, l'attributo lang del documento — che il tema già compila — poi il francese. Solo fr e en sono realmente tradotti; qualsiasi altro valore ripiega sul francese anziché mostrare etichette tradotte a metà. |
mini | score | 1 |
Numero di recensioni sotto il quale l'elemento scompare. A 3, una scheda che ne ha soltanto due non mostra nulla anziché un voto fondato su quasi nulla. |
par-page | liste | 5 |
Recensioni caricate per volta; un pulsante « Vedi altro » carica il resto. |
max | carrousel | 12 |
Numero di foto, 50 al massimo. |
Esempio completo su una scheda prodotto
<!-- 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 che non mostra nulla non è per forza guasto
Tre situazioni fanno scomparire un elemento, e ogni volta è voluto: nessuna recensione (o meno della soglia), nessuna foto per il carosello, e qualsiasi errore di rete o di server.
Mostrare « Nessuna recensione per il momento » su una scheda prodotto è peggio che non mostrare nulla: il visitatore ne conclude che nessuno ha ordinato. E una fascia d'errore sul negozio di un esercente perché la nostra API tossisce sarebbe indifendibile — l'elemento si ritira dall'impaginazione, la scheda prodotto resta intatta.
Conseguenza pratica per l'integratore: non costruite un'impaginazione che riservi un'altezza fissa a un widget. Può non occupare nulla del tutto.
Ciò che l'esercente governa senza di voi
Gli elementi leggono /display al caricamento. Due impostazioni vengono da lì anziché da un attributo, ed è deliberato: l'esercente può collocare i suoi tag una volta per tutte, poi cambiare idea dalla sua area senza riaprire il tema.
-
La pastiglia fluttuante — accesa o spenta, a destra o a sinistra, con il suo scostamento.
<avis-flottant> collocato nel modello non mostra nulla finché l'esercente non l'ha attivata. È spenta per impostazione predefinita: nulla deve comparire da lui senza che l'abbia chiesto.
-
Il colore del marchio — applicato alle superfici che possono riceverlo. Le stelle mantengono il loro ambra, così come il verde di « Acquisto verificato » e l'ambra di « Contestata »: quei colori portano un senso, non decorano, e ridipingerli renderebbe il voto illeggibile presso un esercente il cui marchio sia giallo pallido o bianco.
Le impostazioni sono richieste una sola volta per pagina, anche con quattro elementi: la richiesta in corso è condivisa. È ciò che evita al widget di essere lo script che rallenta la scheda prodotto — rimprovero fondato che si può muovere alla maggior parte dei moduli di recensioni.
Isolamento rispetto al tema
Ogni elemento rende il suo contenuto in uno shadow DOM: il CSS del tema non deborda sul widget, e quello del widget non deborda sul negozio. Nessuno dei due sarebbe accettabile nell'altro senso.
Corollario da conoscere prima di provare: le vostre regole CSS non raggiungeranno l'interno dei widget. L'unica personalizzazione prevista è il colore del marchio, impostato nell'area esercente. Una visualizzazione realmente su misura passa dall'API pubblica — è esattamente per questo che è documentata.
Prima di incollare alcunché
Su PrestaShop e WooCommerce il modulo colloca da sé questi tag, nel punto giusto del tema. L'incollaggio manuale si rivolge alle altre piattaforme e ai temi su misura — vedere i moduli.
7. Server MCP
MCP (Model Context Protocol) espone le stesse capacità dell'API, in una forma che un assistente IA può scoprire da solo. Là dove uno sviluppatore legge una documentazione, scrive l'autenticazione e interpreta il JSON, l'assistente chiede l'elenco degli strumenti, ne legge le descrizioni e li chiama.
Concretamente: l'esercente collega il suo assistente a questo server, poi scrive « quali recensioni non hanno ancora risposta? » oppure « rispondi a questa scusandoti per il ritardo ». Nessuno ha scritto codice di integrazione.
| Valore |
| Indirizzo | https://louis.guide/api/v1/mcp |
| Trasporto | JSON-RPC 2.0 su HTTP, in POST |
| Versione del protocollo | 2024-11-05 |
| Server annunciato | avis-clients, versione 1.0.0 |
| Capacità | tools — né risorse, né prompt |
| Autenticazione |
Token bearer (§7.1) o chiave API + firma HMAC (§7.2) |
| Piano | A pagamento — altrimenti errore JSON-RPC -32001 |
7.1 Collegare un assistente: il token
È la via normale, e la sola che non richieda nulla di installato. L'esercente crea un token nella sua area — Impostazioni · Raccolta, sezione « Collegare un assistente » — poi lo incolla nella configurazione del suo assistente insieme all'indirizzo del server.
POST https://louis.guide/api/v1/mcp
Authorization: Bearer mcp_live_…
Content-Type: application/json
Forma usuale dei file di configurazione di un client MCP:
{
"mcpServers": {
"avis-clients": {
"url": "https://louis.guide/api/v1/mcp",
"headers": { "Authorization": "Bearer mcp_live_…" }
}
}
}
Verifica in un solo comando, prima di collegare alcunché:
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"}'
Ciò che il token può e ciò che non può
| Proprietà | Comportamento |
| Portata |
Unicamente /api/v1/mcp. Presentato sull'API CMS, non viene nemmeno esaminato: né trasmissione di ordini, né esportazione di recensioni, né collegamento. |
| Scrittura |
Vietata per impostazione predefinita. L'esercente spunta esplicitamente « autorizzare la redazione di risposte » alla creazione. Senza di essa lo strumento repondre_a_un_avis non compare nemmeno in tools/list — l'assistente non lo proporrà dunque. |
| Durata di vita |
Un anno, poi cessa di valere. Si ricrea in dieci secondi. |
| Revoca |
Immediata e definitiva, token per token, senza toccare le chiavi API né i moduli dell'esercente. |
| Conservazione |
Mostrato una sola volta. Ne conserviamo soltanto un'impronta: nessuno può rivisualizzarlo, noi compresi. |
| Numero |
Tre token validi al massimo per conto. |
Un token bearer viaggia: trattatelo come una password
A differenza del segreto HMAC, parte a ogni richiesta e vive nella configurazione di un servizio che non controlliamo. È il prezzo del collegamento diretto, ed è per questo che è compartimentato, in scadenza, revocabile e muto in scrittura per impostazione predefinita. Non mettetelo mai in un URL né in un deposito di codice: gli URL finiscono nei registri di tutti gli intermediari attraversati.
I tentativi sono limitati a 20 fallimenti per quarto d'ora e per indirizzo IP — oltre, la risposta è un 429.
7.2 Alternativa: chiave API e firma HMAC
Lo stesso endpoint accetta l'autenticazione descritta al §2: chiave API e firma HMAC. Ha un vantaggio reale — il segreto non lascia mai il server dell'esercente — e un inconveniente che la riserva agli integratori: nessun client MCP sa ricalcolare un HMAC a ogni chiamata, si limitano a porre intestazioni fisse.
Serve dunque un ponte: un piccolo programma avviato dall'assistente, che riceve il JSON-RPC sul suo ingresso standard, lo firma, lo invia e restituisce la risposta. Node.js 18 o più recente, nessuna dipendenza. Salvatelo come 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');
}
}
});
Dichiarazione lato client MCP (forma usuale dei file di configurazione):
{
"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"
}
}
}
}
Il segreto non esce dalla macchina. Serve a firmare localmente; ciò che parte in rete è la firma. Un percorso assoluto è indispensabile: l'assistente non avvia il programma dalla cartella in cui l'avete scritto.
Per verificare il ponte prima di collegare alcunché, inviategli una riga a mano. Deve tornare un elenco di strumenti:
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 Metodi
| Metodo | Effetto |
initialize |
Annuncia la versione del protocollo, le capacità e l'identità del server. |
tools/list | Catalogo degli strumenti e dei loro schemi di ingresso. |
tools/call | Esegue uno strumento — params.name e params.arguments. |
notifications/initialized, ping | Confermati con un risultato vuoto. |
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 I tre strumenti
Due leggono, uno scrive. La linea di separazione non è tecnica: leggere recensioni non presenta alcun rischio, mentre pubblicare una risposta fa parlare l'esercente in pubblico su una pagina che ospitiamo noi — una formulazione infelice su una recensione delicata, ed è una schermata che circola.
Per questo tools/list restituisce soltanto due strumenti quando il chiamante presenta un token in sola lettura. Non scrivete dunque l'elenco in duro: chiedetelo, e annunciate all'esercente solo ciò che contiene.
strumento
lister_avis
Recensioni pubblicate sull'insegna, dalla più recente alla più antica. Lo strumento che l'assistente chiama per « mostrami i clienti scontenti » o « che cosa non ha ancora risposta? ».
| Argomento | Tipo | Predefinito | Effetto |
note_max | intero 1–5 | — |
Restituisce solo le recensioni il cui voto sia minore o uguale. |
sans_reponse | booleano | false |
Scarta le recensioni cui è già stata pubblicata una risposta. |
limite | intero 1–50 | 20 |
Numero di recensioni lette. |
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lister_avis",
"arguments": { "note_max": 3, "sans_reponse": true, "limite": 10 }
}
}
Il risultato è un blocco di testo contenente JSON — è la forma che il protocollo prevede per un risultato strutturato, e quella che gli assistenti sanno leggere:
{
"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 filtra dopo il limite, non prima. Chiedere 20 recensioni senza risposta legge le ultime 20 recensioni pubblicate poi ne toglie quelle già trattate: il risultato può contarne molte meno, e total lo dice. Alzate limite per allargare la finestra di lettura.
Qui risalgono soltanto le recensioni pubblicate: né quelle in attesa, né le rifiutate, né le ritirate. Per quelle c'è GET /cms/reviews con il suo filtro status.
strumento
resume_reputation
Visione d'insieme, senza argomento. Ciò che l'assistente chiama per « come va la mia reputazione? ».
{
"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 conta le recensioni sull'insegna; avis_produit conta separatamente quelle che riguardano un articolo. Sommarle darebbe un totale che non corrisponde ad alcun voto mostrato.
strumento
repondre_a_un_avis
Scrittura pubblica
| Argomento | Tipo | Obbl. | Effetto |
avis_id | stringa | sì | Identificatore della recensione, così come restituito da lister_avis. |
contenu | stringa | sì |
Testo della risposta, troncato a 3000 caratteri. |
{
"avis_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"publiee": false,
"message": "Risposta salvata in bozza. L'esercente deve approvarla nella sua area prima che compaia."
}
Pubblicata o in bozza: l'ha deciso l'esercente, non la chiamata
L'esercente imposta nella sua area se le risposte redatte da un assistente partano direttamente o attendano la sua rilettura. Nessuno dei due comportamenti è giusto in assoluto: chi riceve due recensioni a settimana vuole rileggere, chi ne riceve duecento vuole che partano.
Questa impostazione non è un parametro della richiesta, ed è essenziale: se l'assistente potesse scegliere da sé se debba essere riletto, la garanzia non varrebbe più nulla. Il campo publiee e il campo message dicono ciò che è realmente accaduto — un assistente deve riferirlo tale e quale all'esercente.
Una recensione già dotata di risposta vede la propria risposta sostituita. Una risposta già pubblica lo resta: una riscrittura non la rimanda mai in bozza, il che la farebbe sparire dalla pagina senza decisione di nessuno.
Ciò che un assistente deve sapere prima di scrivere
-
Rispondere nella lingua della recensione — il campo
langue serve a questo. Una risposta in francese sotto una recensione in neerlandese dice al lettore che non è stata letta.
-
Non promettere mai un gesto commerciale che non si possa onorare: rimborso, sostituzione, sconto. Questa risposta è pubblica e opponibile all'esercente.
-
Nessuno strumento modifica né elimina una recensione, e non ce ne saranno. Un assistente cui si chieda di « far togliere » una recensione può soltanto segnalarla, con un motivo ammissibile (§4.6) — il voto non lo è.
7.5 Errori
Sempre uno stato HTTP 200, anche in caso di errore: in JSON-RPC l'errore viaggia nel corpo. Un 4xx farebbe credere al client che il trasporto sia fallito, e la maggior parte riproverebbe anziché mostrare il messaggio.
Unica eccezione: l'autenticazione, rifiutata prima di raggiungere lo strato JSON-RPC. Risponde con la consueta busta d'errore dell'API.
| Stato | Codice | Causa |
| 401 | invalid_mcp_token |
Token sconosciuto, revocato o scaduto — indistinguibili di proposito. L'esercente ne ricrea uno nella sua area. |
| 401 | codici di §2 |
Via HMAC: chiave assente, firma o marca temporale rifiutate. |
| 429 | too_many_attempts |
Più di 20 fallimenti di autenticazione in quindici minuti dallo stesso indirizzo. Attendete anziché riprovare in ciclo. |
| Codice | Significato | Che fare |
-32001 |
Il piano dell'esercente non include l'accesso MCP. |
Passare a un piano a pagamento; le recensioni restano leggibili pubblicamente. |
-32601 | Metodo JSON-RPC sconosciuto. | Verificare method. |
-32602 | Strumento sconosciuto. | Chiamare tools/list, non scrivere i nomi in duro. |
-32603 |
Errore del ponte locale — rete, segreto assente. |
Questo codice viene dal ponte qui sopra, non dal server. |
Gli errori di merito di uno strumento non sono errori JSON-RPC: la risposta resta un risultato, con isError: true e un oggetto { "erreur": "…" } nel testo. È il caso di una recensione introvabile, di un contenuto vuoto, o di una risposta tentata con un token in sola lettura. L'assistente può così spiegarlo all'esercente anziché annunciare un guasto.
8. Webhook in entrata
Non esiste alcun webhook in uscita
La piattaforma non vi chiama: non emette alcuna notifica verso il vostro server alla pubblicazione di una recensione, di una risposta o di una decisione di moderazione. Per seguire l'attività, interrogate GET /api/v1/cms/reviews al vostro ritmo, filtrando su status=published e confrontando published_at con il vostro ultimo passaggio.
Un passaggio ogni ora va bene per la quasi totalità degli usi: le recensioni non arrivano al secondo, e il ritmo di pubblicazione di un negozio si conta in unità al giorno. Interrogare ogni minuto non farà comparire nulla più in fretta.
I due endpoint qui sotto esistono per chiamanti precisi — il nostro operatore SMS e il nostro fornitore di pagamenti. Nessun integratore deve chiamarli, e nessuno può farlo: sono entrambi chiusi da un segreto che non viene distribuito.
POST
/api/v1/stripe/webhook
Firma Stripe
Riceve gli eventi di abbonamento: checkout.session.completed, customer.subscription.created, .updated e .deleted. È ciò che fa passare un conto al piano a pagamento, e quindi ciò che apre l'API recensioni e l'accesso MCP.
La firma del carico utile è l'unica cosa che protegge questa rotta: senza di essa chiunque potrebbe inviare « abbonamento attivo » e offrirsi il piano a pagamento con una sola richiesta curl. È verificata prima di ogni lettura del contenuto, e un segreto assente fa fallire la richiesta anziché lasciarla passare.
Gli eventi non trattati sono confermati con un 200 ({ "ignored": … }): Stripe considera ogni risposta non-2xx come un fallimento e ripete per tre giorni, a intervalli crescenti. Rispondere 404 a un tipo di evento che non ci serve provocherebbe migliaia di rinvii inutili, poi la disattivazione dell'endpoint dal loro lato. Al contrario, un vero fallimento di trattamento risponde sì 500 — lì vogliamo che Stripe ripeta anziché lasciare in piano gratuito un esercente che ha pagato.
POST
/api/v1/sms/inbound/{token}
Token condiviso
Riceve gli SMS in entrata, cioè gli « STOP ». L'operatore tratta la parola chiave dal suo lato e smette di consegnare — ma senza questo endpoint noi non ne sapremmo nulla: continueremmo a inviargli messaggi fatturati e mai ricevuti, l'opposizione sparirebbe il giorno di un cambio di operatore, e non potremmo provare di averla onorata mentre l'onere della prova spetta a noi.
Il token viaggia nel percorso, il che è più debole di una firma — ma è ciò che le interfacce degli operatori francesi sanno configurare. Da qui il fatto che questo endpoint non possa fare altro che aggiungere un'opposizione: il peggio che una chiamata fraudolenta produca è impedire l'invio di SMS a un numero. Fastidioso, mai pericoloso, e reversibile dal back office.
La parola chiave è cercata come prima parola del messaggio, non ovunque al suo interno: chi scrive « deve smetterla, quel negozio fa schifo » non chiede di essere cancellato, e cancellarlo d'ufficio gli toglierebbe il canale attraverso cui viene legittimamente contattato. L'opposizione è registrata per tutti gli esercenti: il messaggio in entrata non dice di quale negozio si tratti — la persona risponde al numero di invio — e indovinare sarebbe insieme falso e pericoloso.
Taglia soltanto il canale SMS. L'email continua a partire: è quella che porta il collegamento di gestione della recensione e le menzioni obbligatorie, e un'opposizione espressa su un canale non vale per l'altro.
9. Limiti di frequenza
I limiti sono calcolati su una finestra scorrevole: nessun contatore che si azzera all'ora tonda, quindi nessuna raffica possibile a inizio periodo.
| Canale | Limite | Chiave | Perché questa cifra |
API pubblica /api/v1/public/ |
60 / minuto |
Indirizzo IP |
Una scheda prodotto fa due chiamate: ciò lascia 30 caricamenti al minuto dallo stesso indirizzo. Ampio per un visitatore, stretto per un aspiratore di contenuti. |
Disponibilità /public/health |
nessuno |
— |
Escluso di proposito: è interrogato di continuo dal monitoraggio, e limitarlo farebbe salire falsi allarmi di indisponibilità. |
Apertura di collegamento POST /pairing |
10 / ora |
Indirizzo IP |
Ogni chiamata crea una riga in banca dati senza alcuna autenticazione. Dieci bastano ampiamente a un integratore che ricomincia. |
Interrogazione POST /pairing/{code} |
240 / 15 minuti |
Indirizzo IP |
Generoso di proposito: il modulo interroga ogni cinque secondi mentre l'esercente crea il conto, conferma l'indirizzo e approva. |
| Autenticazione MCP tramite token |
20 fallimenti / 15 minuti |
Indirizzo IP |
Conta solo i fallimenti: un collegamento che funziona non lo tocca mai. Ferma la scansione di token trovati altrove ed evita che un client mal configurato anneghi i registri. |
| Deposito di una recensione |
10 / minuto |
Token dell'invito |
Per token e non per IP: più clienti di una stessa impresa condividono spesso un solo indirizzo in uscita, e limitarli insieme punirebbe depositi legittimi. |
| Segnalazione pubblica di una recensione |
5 / ora |
Indirizzo IP |
Aperta a ogni lettore (obbligo DSA), quindi a ogni robot. Ogni invio crea una riga nella coda di moderazione. |
L'API CMS non è limitata, il che non autorizza tutto
Oggi nessun limite di frequenza è applicato agli endpoint firmati (/api/v1/cms/ e MCP): sono autenticati, e il volume reale è delimitato dalla quota di solleciti dell'esercente. Gestite comunque il 429 — un limite potrà essere aggiunto, e un'integrazione che non sa leggerlo si romperà il giorno in cui comparirà.
In pratica: trasmettete gli ordini man mano anziché in lotti notturni da diverse migliaia, e interrogate le recensioni ogni ora anziché ogni minuto (§8). Un volume anomalo è visibile dal nostro lato e provoca un contatto, non un'interruzione silenziosa.
Ciò che restituisce un superamento
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Remaining: 0
{
"error": {
"code": "rate_limit_exceeded",
"message": "Troppe richieste. Riprovate tra qualche istante."
}
}
Retry-After indica il numero di secondi da attendere. Rispettatelo: riprovare subito non fa che consumare la finestra successiva. Un ritardo esponenziale, con tetto a un minuto, basta per tutti i casi qui descritti.
L'interrogazione di collegamento fa eccezione e risponde { "status": "rate_limited" }: è lo stesso evento, espresso nel vocabolario di un endpoint che il modulo interroga in ciclo.
10. Codici d'errore comuni
Due formati, e uno solo da gestire nella maggior parte dei casi
Ovunque nell'API, un errore porta la stessa busta:
{
"error": {
"code": "invalid_api_key",
"message": "Chiave API sconosciuta, revocata o scaduta."
}
}
Il code è stabile ed è destinato al vostro programma; il message è destinato all'essere umano che cerca il guasto e può essere riformulato senza preavviso. Non costruite mai la vostra logica sul testo del messaggio.
Una sola eccezione : POST /cms/orders, servito da uno strato diverso, restituisce i suoi errori di validazione come elenco di violations. Un client robusto legge quindi error.code se esiste, e ripiega su violations altrimenti.
Stati HTTP
| Stato | Significato | Riprovare? |
| 200 | Riuscito. In JSON-RPC l'eventuale errore è nel corpo. | — |
| 201 | Creato — ordine registrato, risposta pubblicata. | — |
| 202 | Accettato ma non deciso: la segnalazione entra in coda. | — |
| 400 | Richiesta illeggibile. | No, correggete. |
| 401 | Chiave assente, non valida, o firma rifiutata. | No, salvo orologio da risincronizzare. |
| 402 | Il piano dell'esercente non include questa funzione. | No. |
| 404 | Risorsa sconosciuta — o fuori dal vostro conto. | No. |
| 409 | Conflitto: l'azione è già stata compiuta. | No, è uno stato, non un guasto. |
| 415 | Content-Type assente o inatteso. | No, inviate JSON. |
| 422 | Richiesta ben formata ma rifiutata: campo mancante, valore fuori limite. | No, correggete. |
| 429 | Frequenza superata. | Sì, dopo Retry-After. |
| 5xx | Incidente dal nostro lato. | Sì, con attesa crescente. |
Riepilogo dei codici
| Codice | Stato | Dove | Causa e rimedio |
missing_api_key | 401 | CMS, MCP |
Intestazione X-Api-Key assente. |
invalid_api_key | 401 | CMS, MCP |
Chiave sconosciuta, revocata o scaduta — le tre cose sono volutamente indistinguibili. Verificatela nell'area esercente. |
missing_signature, missing_timestamp |
401 | CMS, MCP |
Scrittura non firmata. Vedere §2.1. |
invalid_timestamp | 401 | CMS, MCP |
X-Timestamp non è una marca temporale Unix in secondi — il più delle volte millisecondi o una data ISO. |
timestamp_out_of_range | 401 | CMS, MCP |
Più di 300 s di scarto. Il messaggio indica lo scarto esatto: sincronizzate l'orologio (NTP). |
signature_mismatch | 401 | CMS, MCP |
Ripercorrete le quattro trappole del §2.3, nell'ordine. |
invalid_mcp_token | 401 | MCP |
Token bearer sconosciuto, revocato o scaduto — indistinguibili. L'esercente ne crea uno nuovo dalla sua area (§7.1). |
too_many_attempts | 429 | MCP |
Troppi fallimenti di autenticazione dallo stesso indirizzo. |
plan_required | 402 | Recensioni, risposta |
Funzione inclusa a partire dal piano a pagamento. La visualizzazione pubblica delle recensioni resta gratuita. |
merchant_not_found | 404 | API pubblica |
Identificatore pubblico sconosciuto. Verificate lo slug, non il nome commerciale. |
review_not_found | 404 | Risposta, segnalazione |
Identificatore sconosciuto, malformato o appartenente a un altro esercente: la compartimentazione impone di non distinguerli. |
already_reported | 409 | Segnalazione |
Su questa recensione è già aperto un fascicolo. |
content_required | 422 | Risposta |
content assente o vuoto dopo la pulizia. |
invalid_reason | 422 | Segnalazione |
Motivo fuori elenco. Un voto basso non è un motivo ammissibile (§4.6). |
invalid_request | 422 | Collegamento |
shop_domain assente o inutilizzabile. |
rate_limit_exceeded | 429 | API pubblica |
Vedere §9 e l'intestazione Retry-After. |
-32001 | 200 | MCP |
Piano senza accesso MCP (un errore JSON-RPC, non HTTP). |
-32601, -32602 | 200 | MCP |
Metodo o strumento sconosciuto. Passate da tools/list. |
Tre sintomi, e da dove cominciare
| Sintomo | Causa più frequente |
| « Ieri funzionava tutto, oggi è tutto 401. » |
L'orologio del server è andato alla deriva. GET /cms/ping restituisce server_time: confrontatelo con il vostro prima di cercare altrove. |
| « Il ping passa, ma tutte le mie scritture falliscono. » |
La chiave è buona, la firma no — è esattamente ciò che quella divisione di regime permette di concludere. Il corpo è quasi sempre stato ricodificato dopo essere stato firmato (§2.3). |
| « Il widget non mostra nulla, ma l'API risponde 200 in console. » |
Dominio non dichiarato lato esercente: il browser blocca la lettura in mancanza di intestazione CORS. Oppure, semplicemente, non ci sono ancora recensioni: un widget vuoto si ritira dalla pagina (§6). |
Se nulla di tutto ciò corrisponde
Scriveteci dall'area esercente allegando tre cose: il percorso chiamato, la marca temporale della richiesta e il codice d'errore ricevuto. Con questi tre elementi la richiesta si ritrova nei registri; senza, l'unica risposta possibile è chiedervi di fornirli.
↑ Tornare all'inizio