1. Inleiding
Het platform biedt drie afzonderlijke kanalen. Ze delen noch hetzelfde publiek, noch hetzelfde authenticatieregime, noch dezelfde limieten. Het juiste kiezen is de eerste beslissing van een integratie.
| Kanaal |
Voorvoegsel |
Voor wie |
Authenticatie |
| API CMS |
/api/v1/cms/ |
E-commercemodules, ERP, CRM, interne hulpmiddelen |
API-sleutel + HMAC-handtekening bij schrijven |
| Publieke API |
/api/v1/public/ |
Weergavewidgets, JavaScript van de webshop |
Geen — snelheidsbeperkt, beperkte CORS |
| MCP |
/api/v1/mcp |
AI-assistenten (Claude, ChatGPT, andere) |
Dezelfde sleutel, dezelfde handtekening als de CMS-API |
Voordat u één regel code schrijft: controleer of een module niet volstaat
De modules PrestaShop en WooCommerce doen alles wat deze pagina beschrijft: ze geven bestellingen op het juiste moment door, plaatsen het widgetscript in het thema, zetten sterren op productpagina's en het reviewblok, en regelen de ondertekening van verzoeken. De handelaar plakt niets en schrijft niets.
Modules downloaden →
Deze documentatie richt zich dus op drie gevallen: een platform waarvoor wij nog geen module hebben, maatwerkontwikkeling, of het aansluiten van een extern hulpmiddel (ERP, klantenservice, AI-assistent) op reeds verzamelde reviews.
Basisadressen
Alle URL's op deze pagina zijn relatief aan het adres van de API. Een module hoeft alleen dat te kennen: de overige adressen worden door GET /api/v1/cms/me teruggegeven, wat raden overbodig maakt en ons toelaat ze te wijzigen zonder iets bij de handelaren bij te werken.
| Gebruik | Adres |
| API (alle kanalen) | https://louis.guide |
| Handelaarsomgeving | https://louis.guide/app |
| Widgetscript | https://louis.guide/widget/v1/avis.js |
Conventies
- Formaat — JSON zowel in als uit, in UTF-8. De header
Content-Type: application/json wordt verwacht bij elk verzoek met een body.
- Naamgeving — kleine slangennotatie (
external_order_id, experienced_at), de gangbare conventie van de API's die PHP- en JavaScript-integrators gebruiken.
- Datums — ISO 8601 met expliciete tijdzone bij invoer (
2026-08-01T14:22:00+02:00). Bij uitvoer hebben volledige datums hetzelfde formaat; de publieke datums van een review worden teruggebracht tot de dag (2026-08-01) omdat geen enkele widget het uur toont.
- Bedragen — als tekenreeks doorgegeven (
"129.90") en nooit als drijvendekommagetal: één cent die bij afronding op een bestelling verloren gaat, wordt een factuurverschil.
- Identificatoren — de objecten die wij aanmaken dragen een blijvende UUID. De uwe (bestelling, product, variant) blijven de uwe: wij herschrijven ze nooit.
-
Fouten —
altijd dezelfde envelop
{ "error": { "code": …, "message": … } }. De code is stabiel en bedoeld voor uw programma, het message voor de mens die foutzoekt. Zie §10.
-
Versiebeheer —
de
/v1 in het pad is een contract. Er kan op elk moment een optioneel veld bij komen; geen bestaand veld wordt hernoemd, verwijderd of verplicht gemaakt. Een breuk zou als /v2 verschijnen, terwijl de oude versie bediend blijft — modules draaien bij de handelaren en niemand kan ze op afstand bijwerken.
Uw code moet dus velden negeren die hij niet kent in plaats van er bij aanblik op te falen.
Een API-sleutel verkrijgen
-
Maak een handelaarsaccount aan op de handelaarsomgeving.
- Bevestig het e-mailadres en open vervolgens de sectie met API-sleutels.
- Noteer het geheim: het wordt slechts eenmaal getoond. Eenmaal kwijt is het onherstelbaar — u maakt een nieuwe sleutel aan en trekt de oude in.
Een installatiemodule heeft deze handeling niet nodig: hij opent zelf een koppelingsverzoek dat de handelaar met één klik goedkeurt. Zie §3.
2. Authenticatie
De CMS-API en de MCP-server gebruiken hetzelfde mechanisme: een sleutel die zegt wie belt, en een handtekening die bewijst dat de beller het geheim bezit. Dat zijn twee verschillende dingen.
| HTTP-methode | Vereiste headers | Waarom |
GET, HEAD |
X-Api-Key |
Een leesactie wijzigt niets: de sleutel volstaat om ze toe te staan. |
POST, PUT, PATCH, DELETE |
X-Api-Key, X-Timestamp, X-Signature |
Een schrijfactie bindt de handelaar: ze moet bewezen zijn en niet herspeelbaar. |
Het geheim reist nooit mee
Alleen de handtekening reist. Dat sluit drie deuren die geen enkele inbraak in de webshop vereisen: het passieve lekken van het geheim in de logs van een tussenpersoon, het herspelen van een onderschept verzoek, en het wijzigen van de body onderweg. Het beschermt echter niet tegen een webshop waarvan de database gestolen is — daartegen is sleutelrotatie de verdediging.
Zet het geheim nooit in een URL: URL's belanden in de logs van elke tussenpersoon die ze passeren.
2.1 Ondertekenen, stap voor stap
Stap 1 — De drie headers
| Header | Inhoud |
X-Api-Key |
Publieke identificator van de sleutel, zoals getoond in de handelaarsomgeving. |
X-Timestamp |
Unix-tijdstempel in seconden, uitsluitend cijfers. Geen milliseconden, geen ISO-datum. |
X-Signature |
Het letterlijke voorvoegsel sha256= gevolgd door de HMAC-SHA256 in kleine hexadecimalen. Het voorvoegsel maakt deel uit van de vergeleken waarde: het weglaten leidt tot afwijzing. |
Stap 2 — De te ondertekenen payload opbouwen
Vier stukken zonder scheidingsteken aaneengeschakeld, in precies deze volgorde:
charge = X-Timestamp
+ MÉTHODE HTTP en majuscules
+ chemin logique de la requête
+ corps brut de la requête
| Stuk | Exacte regel |
| Tijdstempel |
De tekenreeks die identiek is aan die in X-Timestamp. |
| Methode |
POST, PUT… altijd in hoofdletters. |
| Pad |
Het pad zonder schema, zonder host, zonder querystring, beginnend met / — bijvoorbeeld /api/v1/cms/orders. Wordt de API vanuit een submap bediend, dan komt dat installatievoorvoegsel niet in de handtekening: het hoort bij het basisadres, niet bij het logische pad. |
| Body |
De bytereeks precies zoals ze verzonden wordt. Serialiseer één keer, onderteken die reeks, verstuur die reeks. Lege body → lege reeks. |
Stap 3 — Berekenen
X-Signature = "sha256=" + HMAC_SHA256(charge, secret) // hexadécimal minuscule
Stap 4 — Uw implementatie op dit voorbeeld controleren
Deze waarden liggen vast en de getoonde handtekening is werkelijk die van deze gegevens: levert uw code iets anders op, dan zit het probleem in uw code, niet in de onze.
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 !"}
Te ondertekenen payload (één enkele regel, geen toegevoegde spaties):
1786000000POST/api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response{"content":"Merci pour votre retour !"}
Verwacht resultaat:
X-Signature: sha256=8a9c0de10516226f965465704902e00c666892b483b45b361f4536a6b2ee36e9
Dezelfde berekening in één shellregel:
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 en niet echo: die laatste voegt een afsluitende regelovergang toe, wat de handtekening verandert.
Stap 5 — Een volledige aanroep met 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 en niet --data: die laatste interpreteert bepaalde tekens en kan de verzonden body wijzigen, waarmee de handtekening ongeldig wordt.
2.2 PHP-voorbeeld
De minimale client, zonder afhankelijkheden. Het is hetzelfde mechanisme als dat van de PrestaShop- en WooCommerce-modules, teruggebracht tot de kern.
<?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 De vier valkuilen
Vier oorzaken verklaren nagenoeg elke signature_mismatch. Van buitenaf lijken ze allemaal op elkaar — vandaar het nut ze in deze volgorde uit te sluiten.
- De body werd na het ondertekenen opnieuw gecodeerd. Het vaakst voorkomende geval, en het moeilijkst zichtbare: een array die twee keer geserialiseerd wordt levert twee verschillende tekenreeksen op zodra er een accent of een schuine streep in staat (
JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES, volgorde van de sleutels). Onderteken de reeks, verstuur die reeks, bouw ze nooit opnieuw op.
- Het ondertekende pad draagt een voorvoegsel dat het niet zou mogen hebben. Het ondertekende pad is
/api/v1/cms/orders, ook als de API vanaf https://voorbeeld.nl/platform/api/v1/cms/orders bediend wordt. Het installatievoorvoegsel hoort bij het basisadres. Onderteken omgekeerd evenmin de volledige URL met schema en host.
-
De serverklok loopt uit de pas.
Tolerantie: 300 seconden afwijking, in beide richtingen. Daarboven luidt het antwoord
timestamp_out_of_range en vermeldt het bericht de gemeten afwijking in seconden — precies de informatie voor uw hostingprovider. Dit geval uit zich vaak als een integratie die "gisteren nog werkte".
- De methode of het voorvoegsel ontbreekt. De methode komt in hoofdletters in de payload, en de waarde van
X-Signature begint met sha256=. Een kale HMAC zonder voorvoegsel wordt afgewezen.
Wat de querystring niet doet
URL-parameters (?page=2) komen niet in de ondertekende payload: alleen het pad staat erin. In de praktijk maakt dat niets uit, aangezien de ondertekende endpoints allemaal schrijfacties zijn die hun parameters in de body dragen — maar een implementatie die ze aan de payload zou toevoegen, zou falen.
Herspelen en geldigheidsvenster
De ondertekende payload dekt het tijdstempel, de methode, het pad en de body. Eén ervan weglaten zou een gat openen: zonder het pad zou een handtekening die geldig is voor POST /orders herspeelbaar zijn op DELETE /orders; zonder het tijdstempel zou het verzoek onbeperkt herspeelbaar zijn.
Het venster van 300 seconden is wat het herspelen begrenst: een onderschept verzoek kan daarbuiten niet opnieuw verstuurd worden. Er is geen woordenboek van reeds geziene handtekeningen — binnen dat venster wordt een identiek verzoek dus twee keer aanvaard. Dat heeft geen gevolg voor het doorgeven van bestellingen, dat idempotent op external_order_id is: de tweede ontvangt de reeds geregistreerde bestelling en verstuurt geen tweede e-mail.
Authenticatieantwoorden
Al deze antwoorden dragen status 401.
| Code | Oorzaak | Wat te doen |
missing_api_key |
Header X-Api-Key ontbreekt. |
Voeg de header toe. |
invalid_api_key |
Sleutel onbekend, ingetrokken of verlopen. Het bericht is in alle drie de gevallen bewust identiek: ze onderscheiden zou het mogelijk maken massaal te testen welke identificatoren bestaan. |
Controleer de sleutel in de handelaarsomgeving, of maak een nieuwe aan. |
missing_signature |
Schrijfactie zonder header X-Signature. |
Onderteken het verzoek (§2.1). |
missing_timestamp |
Schrijfactie zonder header X-Timestamp. |
Voeg het tijdstempel toe en onderteken het. |
invalid_timestamp |
X-Timestamp is geen reeks cijfers — milliseconden, ISO-datum of een teken. |
Stuur een Unix-tijdstempel in seconden. |
timestamp_out_of_range |
Meer dan 300 seconden afwijking. Het bericht geeft het exacte verschil. |
Synchroniseer de serverklok (NTP). |
signature_mismatch |
De handtekening komt niet overeen met de verwachte payload. |
Loop de vier valkuilen uit §2.3 op volgorde na. |
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "timestamp_out_of_range",
"message": "Tijdstempel buiten tolerantie: +412 s afwijking met onze server (maximaal 300 s). De klok van uw server loopt waarschijnlijk niet gelijk."
}
}
3. Een webshop koppelen
Met deze twee endpoints kan een module een sleutel ophalen zonder dat de handelaar iets hoeft over te typen. De module opent een verzoek, toont een link, de handelaar keurt goed in zijn browser, en de module ontvangt zijn sleutel en geheim bij de volgende leesactie.
Ze zijn zonder authenticatie, en dat is opzet. De veiligheid berust niet op een identiteit maar op drie dingen: het verzoek levert niets op zolang een ingelogde handelaar het niet heeft goedgekeurd, het polltoken verlaat de server van de webshop nooit, en het geheim wordt maar één keer afgegeven. Het ergste wat een kwaadwillige aanroep kan opleveren is een openstaand verzoek dat niemand goedkeurt — en dat na een kwartier verloopt.
POST
/api/v1/pairing
Zonder authenticatie
Opent een koppelingsverzoek en geeft de goedkeuringslink terug die aan de handelaar getoond moet worden.
| Veld | Type | Verplicht | Omschrijving |
shop_domain | tekenreeks | ja |
Domein van de webshop, bv. shop.voorbeeld.nl. |
platform | tekenreeks | nee |
prestashop, woocommerce, custom… standaard unknown. |
shop_name | tekenreeks | nee |
Leesbare naam van de webshop, hergebruikt bij het aanmaken van het account. |
platform_version | tekenreeks | nee |
Versie van het platform, bv. 8.1.6. |
plugin_version | tekenreeks | nee |
Versie van de aanroepende module. |
shop_uid | tekenreeks | nee |
Unieke identificator die eenmaal bij de installatie van de module wordt getrokken. Sterk aanbevolen bij meerdere webshops: zie §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 — te openen in een nieuw tabblad van de browser van de handelaar, buiten zijn beheeromgeving. Daar logt hij in of maakt hij zijn account aan, en keurt hij vervolgens goed.
poll_token — uitsluitend aan serverzijde te bewaren. Het mag nooit in een pagina of in een URL verschijnen: het is waarmee het geheim opgehaald wordt.
code — toonbaar aan de handelaar, zodat hij kan nagaan dat hij het juiste verzoek goedkeurt.
Foutcodes
| Status | Code | Oorzaak |
| 422 | invalid_request | shop_domain ontbreekt of is onbruikbaar. |
| 429 | rate_limited | Meer dan 10 openingen per uur per IP. |
POST
/api/v1/pairing/{code}
Zonder authenticatie
Bevraagt de status van het verzoek en geeft de sleutel af zodra — en uitsluitend zodra — de handelaar heeft goedgekeurd.
Een POST ofschoon het een leesactie is, omdat de aanroep een neveneffect heeft: hij verbruikt het geheim. Bij een GET zou een vooruitladende browser of een antivirus die links volgt het in plaats van de module opgebruiken.
| Veld | Type | Verplicht | Omschrijving |
poll_token | tekenreeks | ja |
Het token dat bij het openen van het verzoek ontvangen is. |
curl -X POST 'https://louis.guide/api/v1/pairing/4K7M-9QR3' \
-H 'Content-Type: application/json' \
--data-raw '{"poll_token":"pt_8f2c1a7e5b2d48a6c3d9e0f1a2b3c4d5"}'
In afwachting van goedkeuring:
HTTP/1.1 200 OK
{ "status": "pending" }
Goedgekeurd — de gegevens worden uitsluitend bij deze aanroep afgegeven:
HTTP/1.1 200 OK
{
"status": "approved",
"api_key": "ak_live_5c2f81b0",
"secret": "sk_live_3f9c1a7e5b2d48a6",
"merchant": "tissufiesta"
}
Mogelijke waarden van status
| Waarde | Betekenis | Wat te doen |
pending | De handelaar heeft nog niet beslist. | Blijven pollen. |
approved | Goedgekeurd. Het antwoord draagt de gegevens. | Ze opslaan, stoppen met pollen. |
rejected | De handelaar heeft geweigerd. | Stoppen en het hem melden. |
expired | Vijftien minuten verstreken zonder beslissing. | Een nieuw verzoek openen. |
consumed |
Het geheim is al afgegeven, en dat gebeurt nooit twee keer. De module heeft het antwoord verloren. |
De koppeling opnieuw beginnen — dat is het veilige gedrag. |
unknown |
Onbekende code of verkeerd token. Bewust niet te onderscheiden: ze scheiden zou van dit endpoint een orakel maken dat verklapt welke webshops koppelen. |
Het paar code / token controleren. |
rate_limited | Te veel gepold (HTTP-status 429). | De aanroepen verder uit elkaar zetten. |
Sla het geheim onmiddellijk op. Het wordt alleen in dat ene antwoord meegestuurd. Een module die het niet weet te bewaren, moet de handelaar de hele koppeling opnieuw laten doen.
Altijd 200, ook voor een wachttoestand. De module bevraagt in een lus: een HTTP-foutcode op een volkomen normale situatie zou voor niets meldingen opleveren. Poll elke vijf seconden; de limiet is 240 aanroepen per kwartier per IP — daarboven luidt het antwoord { "status": "rate_limited" } met status 429.
4. CMS-API (ondertekend)
Het kanaal voor serverintegraties: e-commercemodules, ERP, CRM, interne hulpmiddelen. Alle URL's worden voorafgegaan door https://louis.guide.
Lezen: de sleutel volstaat. Schrijven: sleutel + handtekening. De berekening staat uitgewerkt in §2. De onderstaande fiches herinneren met een badge aan het regime van elk.
Wat de API niet toestaat, en nooit zal toestaan
Geen enkel endpoint wijzigt of verwijdert een review. De handelaar kan openbaar antwoorden en voor moderatie melden, meer niet — precies wat zijn eigen omgeving toelaat. Een API die meer toestaat dan de interface zou een achterdeur in de naleving zijn, en dat is het eerste wat een audit nakijkt.
GET
/api/v1/cms/ping
API-sleutel
Controleert of een sleutel werkt. Het is de eerste aanroep om te schrijven, en degene die u de handelaar als knop "Verbinding testen" aanbiedt: liever dat hij een foute sleutel bij de configuratie ontdekt dan bij de eerste bestelling die niet doorkomt.
Bewust zonder handtekening. Een leesactie wijzigt niets, en vooral: dit endpoint moet bruikbaar blijven om te bewijzen dat een sleutel goed is terwijl de HMAC-implementatie nog fout is. De ping lukt, de schrijfactie niet: het probleem zit in de handtekening, niet in de sleutel.
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 wordt om een precieze reden teruggegeven: vergelijk het met de klok van uw server. Een afwijking van meer dan 300 seconden laat al uw ondertekende schrijfacties mislukken (§2.3), en hier ziet u dat vóór u er een dag aan verliest.
GET
/api/v1/cms/me
API-sleutel
Toestand van het account: identiteit van de handelaar, mogelijkheden van het abonnement, quotum, en adressen van het platform.
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/nl/m/tissufiesta"
},
"server_time": "2026-08-13T14:52:07+00:00"
}
Lees de mogelijkheden, niet de naam van het abonnement
Het blok plan toont naast de abonnementscode ook mogelijkheden (can_…). Test die eerste: een module die if (plan === 'pro') vast in code zet houdt op juist te zijn op de dag dat er een abonnement bij komt of van naam verandert, bij alle handelaren tegelijk en zonder dat een van hen het kan herstellen.
| Mogelijkheid | Waarover ze gaat |
can_display_product_reviews |
Weergave van productreviews — de sterren op de productpagina's. |
can_use_photos |
Het leveren van klantfoto's via de API. Ze worden al vanaf het gratis abonnement verzameld maar alleen tegen betaling geleverd: bij een gratis account antwoordt de galerij een lege lijst, nooit een fout. |
can_use_reviews_api |
Reviews via de API lezen, op reviews antwoorden, en MCP-toegang. Het doorgeven van bestellingen valt hier niet onder: dat zit in elk abonnement. |
can_remove_branding |
Verwijdering van de vermelding van het platform op widgets en e-mails. |
Het blok urls voorkomt raden
Uw integratie hoeft slechts één adres te kennen: dat van de API. De overige — handelaarsomgeving, widgetscript, publieke pagina van de handelaar — worden hier teruggegeven. Een module die ze uit één basis samenstelt, veronderstelt dat alles op dezelfde host leeft, wat ophoudt waar te zijn zodra een kanaal naar een subdomein verhuist, en levert dode links op bij alle reeds geïnstalleerde handelaren.
quota.remaining verdient een plek in uw interface: op nul worden bestellingen nog steeds aanvaard maar vertrekt er geen enkele uitnodiging meer. Waarschuwen bij 90% verbruik voorkomt dat de handelaar het in zijn statistieken ontdekt.
POST
/api/v1/cms/orders
Handtekening vereist
Het centrale endpoint. Het registreert een bestelling en plant het reviewverzoek in. Al het overige op het platform vloeit uit deze aanroep voort: zonder hem is er geen uitnodiging, geen review, geen score.
Idempotent op external_order_id
Dezelfde referentie opnieuw versturen geeft de reeds geregistreerde bestelling terug met status 200 in plaats van 201, zonder duplicaat en zonder een tweede e-mail aan de klant. Het veld idempotent van het antwoord is dan true. U mag dus zonder voorzorg opnieuw proberen na een netwerkonderbreking of een verstreken wachttijd — dat gedrag verdient de voorkeur boven elke zelfgemaakte ontdubbelingslogica.
Wanneer aanroepen
Op het moment dat de ervaring beleefd wordt, niet besteld: bij levering, bij verzending naargelang uw vak, of bij het bereiken van de status die daarvoor staat. experienced_at draagt die datum, en die start de uitnodigingstermijn.
Body van het verzoek
Wortel
| Veld | Type | Verpl. | Omschrijving |
external_order_id | tekenreeks (100) | ja |
Referentie van de bestelling bij u. Idempotentiesleutel en aankoopbewijs dat vijf jaar bewaard wordt (AFNOR §6.3). Moet in de tijd stabiel zijn. |
customer | object | ja |
Identiteit van de te benaderen klant — zie de volgende tabel. |
experienced_at | ISO 8601 | ja |
Datum van levering of gebruik, met expliciete tijdzone. Zie het kader hieronder: dit is niet de besteldatum. |
source | object | ja |
Technische context van de verzending — zie verderop. |
items | array (max. 200) | nee |
Artikelen. Zonder hen wordt er geen enkele productreview gevraagd — alleen de review over de zaak. |
amount | decimale tekenreeks | nee |
Totaalbedrag, bv. "129.90". Nooit een drijvendekommagetal. |
currency | ISO 4217 | nee |
"EUR", "CHF"… |
channel | opsomming | nee |
ecommerce_order (standaard), pos_transaction, qr_scan, manual, csv_import, nfc. |
location_id | tekenreeks (100) | nee |
De betrokken vestiging, zoals door de handelaar opgegeven. Een onbekende waarde laat het verzoek met een 422 mislukken in plaats van de bestelling aan het verkeerde verkooppunt te koppelen. |
solicitation_delay_days | geheel getal 0–365 | nee |
Een termijn die specifiek is voor deze bestelling en de accountinstelling overschrijft. Handig wanneer dezelfde verkoper een boeket verstuurt waarover morgen gevraagd moet worden en een matras waarover pas over een maand. Buiten de grenzen wordt de waarde genegeerd en geldt de accountinstelling — een onzinnige waarde mag geen bestelling kosten. |
order_status_id | tekenreeks (20) | nee |
Status van de bestelling bij u op het moment van verzenden. Louter diagnostisch — wij interpreteren hem niet — maar het is de enige informatie waarmee "waarom heeft deze bestelling niets in gang gezet?" beantwoord kan worden. |
order_status_label | tekenreeks (120) | nee |
Leesbaar label van die status. |
experienced_at: de leverdatum, niet de besteldatum
Een pakket dat op de 1e besteld en op de 6e geleverd wordt, draagt de 6e. Dat is geen spitsvondigheid: twee gerandomiseerde proeven met meer dan 300 000 consumenten (Jung, Ryu, Han & Cho, Journal of Marketing, 2023) tonen aan dat een uitnodiging die verstuurd wordt voordat de klant zich een oordeel heeft kunnen vormen een negatief effect heeft op het aantal inzendingen. De termijn op de besteldatum verankeren betekent stelselmatig te vroeg uitnodigen, met de hele levertijd.
Het is ook een van de drie datums die openbaar naast de review getoond worden (AFNOR §6.3).
customer
| Veld | Type | Verpl. | Omschrijving |
email | e-mail (255) | ja |
Het enige persoonsgegeven dat wij onversleuteld aanvaarden. Gewist na de inzendtermijn; alleen de vingerafdruk blijft over. |
country | ISO 3166-1 alpha-2 | nee* |
*Sterk aanbevolen. Google berekent zijn handelaarsscores per land en negeert reviews waarvan het land onbekend is. Deze informatie bestaat alleen op het moment van de bestelling: zodra het adres gewist is, is ze definitief onherstelbaar, zonder enige inhaalmogelijkheid. |
locale | fr, en, nl, de, it, es | nee |
Taal van de uitnodigingsmail. Bij ontbreken de standaardtaal van de handelaar — een Nederlandstalige klant in het Frans benaderen laat het antwoordpercentage instorten. |
phone | tekenreeks (32) | nee |
Mobiel nummer voor de sms-uitnodiging. Internationaal formaat (+33612345678) sterk aanbevolen: het is het enige eenduidige. Een nationaal nummer wordt omgezet aan de hand van country; zonder bekend land wordt het terzijde gelegd zonder de bestelling te laten mislukken. Zie de waarschuwing hieronder. |
first_name, last_name | tekenreeks (100) | nee |
Personalisering van de uitnodiging en getoonde naam van de auteur. |
company | tekenreeks (255) | nee |
Handelsnaam, voor een zakelijke bestelling. |
postal_code, city | tekenreeks | nee |
Gewist tegelijk met het e-mailadres. |
Stuur het mobiele nummer alleen als de handelaar sms heeft afgenomen. Zonder die optie wordt het ontvangen en bewaard zonder dat er enig bericht vertrekt: een persoonsgegeven dat zonder doel verzameld wordt, wat geen van beide partijen bij een controle kan verantwoorden.
source — verplicht
Dit blok is geen statistiek. Wanneer een handelaar schrijft "mijn reviews vertrekken niet meer sinds de update", zit het antwoord er al in: versie van het platform, versie van de module, aanleidende gebeurtenis. Het optioneel maken zou erop neerkomen het nooit te hebben — integrators vullen in wat vereist is, niet wat gesuggereerd wordt.
| Veld | Type | Verpl. | Omschrijving |
platform | tekenreeks (50) | ja |
prestashop, woocommerce, shopify, magento, custom… |
platform_version | tekenreeks (30) | nee |
Bv. 8.1.6. |
plugin_version | tekenreeks (30) | nee |
Versie van uw integratie. Bij elke oplevering te verhogen. |
trigger | tekenreeks (100) | nee |
Gebeurtenis achter de verzending, bv. woocommerce_order_status_completed. Maakt begrijpelijk waarom een bestelling te vroeg of te laat vertrekt. |
shop_uid | tekenreeks (80) | nee* |
*Doorslaggevend bij meerdere webshops. Identificator die eenmaal bij de installatie getrokken en bewaard wordt. Zie het kader. |
shop_id | tekenreeks (50) | nee |
Identificator van de webshop bij het platform. Dient als terugval wanneer shop_uid ontbreekt. |
shop_name | tekenreeks (255) | nee |
Leesbare naam van deze webshop. Zonder die naam vindt de handelaar in zijn omgeving een vestiging die "3" heet en moet hij raden welke het is. |
shop_group_id, lang_id | tekenreeks | nee |
Bewaard voor diagnose, nooit geïnterpreteerd. lang_id scheidt niets: de taal van de review komt uit customer.locale. |
Meerdere webshops: shop_id volstaat niet
Hij is "1" bij elke installatie met één webshop. Een handelaar die twee sites onder hetzelfde account uitbaat — één merk per domein, een gangbaar geval — zou dus vanuit beide "1" versturen: de twee webshops zouden tot één vestiging versmelten, de reviews van de ene zouden op de pagina van de andere verschijnen, en de behouden naam zou die van de laatst ontvangen bestelling zijn. Een gebrek dat bij het testen op twee echte PrestaShop-installaties is vastgesteld.
shop_uid lost het op: trek hem eenmaal bij de installatie en bewaar hem. Hij overleeft zowel een domeinwissel als een sleutelvernieuwing — de twee andere onderscheidingen waaraan men eerst denkt, en die allebei veranderen.
items[] — optioneel, maximaal 200 artikelen
| Veld | Type | Verpl. | Omschrijving |
external_product_id | tekenreeks (100) | ja |
Identificator van het product in uw catalogus. |
name | tekenreeks (255) | ja |
Naam van het product zoals aan de klant getoond. |
variant_id | tekenreeks (100) | nee* |
*Het belangrijkste veld uit deze lijst. Zonder hem delen de rode en de blauwe stoel dezelfde productsleutel: hun reviews lopen door elkaar en "de poot is gebroken" duidt niets meer aan. Komt overeen met id_product_attribute (PrestaShop), de variatie (WooCommerce), de variant (Shopify). |
variant_label | tekenreeks (255) | nee |
Leesbaar label: "Kleur: rood, Maat: L". |
gtin | 8 tot 14 cijfers | nee* |
EAN-13 of omgezette UPC-A. Aggregatiesleutel tussen handelaren, en een eis van Google om sterren in zijn resultaten te tonen. |
upc, isbn, mpn | tekenreeks | nee |
Los van de GTIN bijgehouden omdat catalogi ze in aparte kolommen bijhouden. De ISBN is doorslaggevend bij boeken, waar de GTIN vaak leeg is. |
sku, brand | tekenreeks | nee |
Interne referentie en merk. |
category_id, category_name | tekenreeks | nee |
Hoofdcategorie in uw catalogus. |
product_url, image_url | URL (500) | nee |
Gebruikt in de uitnodigingsmail: een productafbeelding verhoogt het aantal inzendingen merkbaar. |
images | lijst met URL's (max. 10) | nee |
Bijkomende afbeeldingen. |
description | tekenreeks (5000) | nee |
Ontvangen, nooit als zodanig opnieuw getoond: het is uw tekst, niet die van de auteur van de review. Ze dient om het product bij de moderatie te situeren. |
tags | lijst (max. 30) | nee |
Sleutelwoorden van het product, elk 60 tekens. |
meta_title, meta_description | tekenreeks | nee |
Metagegevens van de productpagina. |
quantity | geheel getal > 0 | nee |
Standaard 1. |
unit_price | decimale tekenreeks | nee |
Bv. "19.90". Als tekenreeks, zoals alle bedragen. |
Volledig voorbeeld
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"
}
}
Bestelling geregistreerd:
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
}
Hetzelfde verzoek opnieuw gespeeld:
HTTP/1.1 200 OK
{ "…": "…", "idempotent": true }
Het e-mailadres wordt nooit teruggegeven, ook niet als u het net verstuurd hebt: elk teruggegeven gegeven is een gegeven dat in uw eigen logs kan lekken.
Mogelijke waarden van status
| Waarde | Betekenis |
pending | Ontvangen, in afwachting van inplanning. |
scheduled | Uitnodiging ingepland. |
solicited | Reviewverzoek naar de klant verstuurd. |
reviewed | De klant heeft zijn review ingediend. |
cancelled | Geannuleerd vóór verzending. |
expired | Inzendtermijn verstreken zonder review. |
Fouten
Dit endpoint is het enige dat door API Platform bediend wordt: zijn validatiefouten komen dus als een lijst van violations binnen, en niet in de envelop { "error": … } van de rest van de API. Uw code moet beide vormen aanvaarden.
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."
}
]
}
| Status | Oorzaak | Wat te doen |
| 401 |
Sleutel ontbreekt, ongeldig, of handtekening geweigerd. |
Zie §2. |
| 422 |
Een veld ontbreekt of is verkeerd opgebouwd — zie violations. |
Corrigeer het veld dat propertyPath aanwijst. |
| 422 |
experienced_at ligt in de toekomst (meer dan één dag marge). |
Controleer de tijdzone van de server: daar komt het verschil vrijwel altijd vandaan. |
| 422 |
experienced_at gaat meer dan 90 dagen terug. |
Zie het kader hieronder. Neem contact op met de ondersteuning om een geschiedenis over te nemen. |
| 422 |
Geen enkele vestiging komt overeen met location_id. |
Maak de vestiging in de handelaarsomgeving aan, of laat het veld weg. |
| 415 |
Header Content-Type ontbreekt of is onverwacht. |
Stuur Content-Type: application/json. |
Waarom bestellingen ouder dan 90 dagen geweigerd worden
Het rampscenario is bekend: een module wordt geïnstalleerd en duwt in één keer drie jaar geschiedenis door. Duizenden uitnodigingen vertrekken naar verouderde adressen, het weigeringspercentage schiet omhoog — en aangezien alle e-mails van ons domein vertrekken, stort de afleverbaarheid van alle handelaren in, niet alleen die van de nieuwkomer.
De weigering valt aan de poort, met een duidelijk bericht, en niet bij het inplannen: de integrator begrijpt het meteen in plaats van zijn bestellingen in stilte te zien verdwijnen.
GET
/api/v1/cms/reviews
API-sleutel
Betaald abonnement
Somt de reviews van de handelaar op, van recent naar oud, met het gepubliceerde antwoord en de eventuele melding bij elk. Dit is het endpoint waarmee reviews naar een ERP, een CRM of een klantendienstsysteem gehaald worden.
| Parameter | Standaard | Omschrijving |
type | merchant |
merchant voor reviews over de zaak, product voor productreviews. |
status | alle |
published, pending, awaiting_email, rejected, disputed, withdrawn. Een onbekende waarde wordt genegeerd — het filter geldt dan niet, in plaats van een fout terug te geven. |
page | 1 | Paginanummer. |
per_page | 25 |
Van 1 tot 100. Daarboven wordt de waarde tot 100 teruggebracht. |
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
}
De drie datums, en waarom het er drie zijn
experienced_at (de beleefde ervaring), submitted_at (de inzending) en published_at (het online gaan) zijn drie verschillende dingen, en AFNOR eist dat ze te onderscheiden zijn. Een integrator die ze door elkaar haalt toont "3 dagen geleden" bij een ervaring van drie weken oud. published_at is null zolang de review niet gepubliceerd is.
order_reference neemt uw external_order_id over: daarmee wordt de review in uw systeem aan de bestelling gekoppeld. Bij een review die buiten een uitnodiging is ingediend, is hij null.
Stapsgewijze synchronisatie
Bevraag met status=published en vergelijk published_at met de vorige doorloop: de hele geschiedenis bij elke uitvoering ophalen werkt de eerste maanden, en wordt daarna elk uur een query van enkele duizenden regels. De paginering begint bij 1 en het veld total geeft het aantal reviews dat aan het filter voldoet, niet het aantal pagina's.
| Status | Code | Oorzaak |
| 402 | plan_required |
Het abonnement van de handelaar omvat de reviews-API niet. De reviews blijven zonder sleutel leesbaar via de Publieke API — wat niet hetzelfde is: die dient voor de openbare weergave, niet voor export. |
POST
/api/v1/cms/reviews/{uuid}/response
Handtekening vereist
Betaald abonnement
Publiceert een openbaar antwoord op een review over de zaak, of werkt het bestaande antwoord bij. Een review draagt slechts één antwoord: opnieuw versturen vervangt de tekst.
| Veld | Type | Verpl. | Omschrijving |
content | tekenreeks | ja |
Tekst van het antwoord. Afgekapt op 3000 tekens zonder fout — controleer de lengte aan uw kant als de afkapping u stoort. |
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
}
}
Lees published voordat u ook maar iets aankondigt
De handelaar stelt in zijn omgeving in of antwoorden die door een programma geschreven zijn rechtstreeks vertrekken dan wel op zijn nalezing wachten. Die instelling leeft op het account en is geen parameter van het verzoek: als de beller zelf kon kiezen of hij nagelezen moet worden, zou de garantie niets waard zijn.
Gevolg voor uw interface: een aanvaard antwoord is niet noodzakelijk zichtbaar. published: false betekent "als concept opgeslagen, goed te keuren in de handelaarsomgeving" — zeg dat, in plaats van een "gepubliceerd" te tonen dat door de publieke pagina tegengesproken wordt.
201 bij aanmaken, 200 bij bijwerken; het veld created herhaalt diezelfde informatie in de body. Een reeds gepubliceerd antwoord blijft gepubliceerd: een bijwerking zet het nooit terug op concept, wat het van de pagina zou laten verdwijnen zonder dat iemand dat beslist heeft.
| Status | Code | Oorzaak |
| 402 | plan_required | Abonnement zonder antwoorden op reviews. |
| 404 | review_not_found |
Identificator onbekend, verkeerd opgebouwd, of van een andere handelaar — alle drie de gevallen zijn niet te onderscheiden, en de afscherming vereist dat. |
| 422 | content_required | content ontbreekt of is leeg. |
POST
/api/v1/cms/reviews/{uuid}/report
Handtekening vereist
Meldt een review voor moderatie. De review krijgt de status "betwist" en het dossier komt in de onderzoekswachtrij.
| Veld | Type | Verpl. | Omschrijving |
reason | opsomming | ja |
Reden, te kiezen uit onderstaande lijst. |
detail | tekenreeks | nee |
Toelichting voor de moderator, afgekapt op 1000 tekens. Hier schrijft men "bestelling nr. X, nooit op dat adres geleverd" — een gemotiveerde melding wordt sneller behandeld. |
Ontvankelijke redenen
| Waarde | Wanneer in te roepen |
inappropriate_content | Belediging, haatdragende uitlatingen, onwettige inhoud. |
spam_or_advertising | Reclame, commerciële link, geautomatiseerde inhoud. |
off_topic | Zonder verband met de beleefde ervaring — de vervoerder, het weer. |
conflict_of_interest | Concurrent, oud-werknemer, betaalde review. |
personal_data_disclosure | De review legt persoonsgegevens bloot. |
Een lage score is geen reden
Geen enkele reden staat toe een review vanwege zijn score te betwisten, en dat is geen vergetelheid: het is dat verbod dat het verschil maakt tussen een reviewplatform en een etalage. Een slecht gemotiveerde melding wordt afgewezen, en de review blijft online.
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 is altijd true, en het veld bestaat opdat geen enkele interface van het tegendeel uitgaat: de review blijft gedurende het hele onderzoek openbaar. Hem op een enkele melding weghalen zou erop neerkomen dat de handelaar mag wegwerken wat hem niet bevalt — Google verbiedt dat uitdrukkelijk, AFNOR eveneens. Het antwoord is een 202: het verzoek is geregistreerd, niet beslist.
| Status | Code | Oorzaak |
| 404 | review_not_found | Identificator onbekend, verkeerd opgebouwd, of buiten uw account. |
| 409 | already_reported | Er loopt al een melding over deze review. |
| 422 | invalid_reason |
Reden ontbreekt of staat niet in de lijst. Het bericht herinnert aan de toegestane waarden. |
5. Publieke API
Alleen lezen, zonder authenticatie, onder /api/v1/public/. Dit is wat de widgets gebruiken, en wat elke weergave op maat kan gebruiken.
De {slug} in de paden is de publieke identificator van de handelaar — die van zijn publieke pagina, zichtbaar in urls.profile zoals teruggegeven door /cms/me.
Wat een API zonder sleutel beschermt
Er valt geen identiteit te controleren: deze code draait bij de bezoekers van een webshop, er kan daar geen geheim leven. De bescherming berust dus op drie andere dingen, die u moet kennen voordat u integreert.
-
Er komen hier geen gevoelige gegevens uit. Geen e-mailadres, geen e-mailvingerafdruk, geen bestelreferentie, geen interne identificator. Een widget toont openbare reviews; alles wat via dit kanaal naar buiten komt is voor iedereen leesbaar.
-
Snelheidsbeperkt tot 60 verzoeken per minuut per IP-adres, over een schuivend venster. Een productpagina doet twee aanroepen: dat laat 30 laadbeurten per minuut vanaf hetzelfde adres toe — ruim voor een bezoeker, krap voor een contentschraper. Zie §9.
-
CORS beperkt tot de opgegeven domeinen van de handelaar. Een jokerteken
* zou elke site — concurrent, vergelijker, namaker — toestaan de reviews van om het even welke handelaar te tonen alsof het de zijne waren.
CORS: wat opgegeven moet worden opdat de browser het antwoord aanvaardt
De header Access-Control-Allow-Origin wordt alleen gezet als de aanroepende oorsprong overeenkomt met een domein dat aan de in de URL genoemde handelaar gekoppeld is. Subdomeinen worden aanvaard: een domein dat als voorbeeld.nl is opgegeven staat www.voorbeeld.nl en shop.voorbeeld.nl toe.
Het typische symptoom van een niet-opgegeven domein: het verzoek vertrekt, de server antwoordt 200, en de browser blokkeert het lezen in de console. De oplossing ligt in de handelaarsomgeving, niet in de code.
Een aanroep van server naar server valt hier niet onder: zonder Origin-header is er geen CORS-controle. De snelheidsbeperking dekt dat geval. CORS beschermt de browser tegen een andere site, nooit de gegevens zelf.
Cache
Alle antwoorden zijn openbaar en worden gecachet: 60 seconden voor de reviews en de scores, 300 seconden voor de weergave-instellingen. Dat vangt het verkeer op van een webshop in actie zonder op de piek te hoeven dimensioneren. Bouw geen weergave die ervan uitgaat dat een gepubliceerde review meteen verschijnt.
GET
/api/v1/public/merchants/{slug}/score
Zonder authenticatie
Algemene score van de webshop en verdeling per score.
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 wordt uitdrukkelijk teruggegeven in plaats van verondersteld: een integrator die "op 10" codeert omdat zijn vorige leverancier dat was, levert een onjuiste weergave op die niemand naleest. count telt uitsluitend de openbaar zichtbare reviews.
404 merchant_not_found als de slug onbekend is.
GET
/api/v1/public/merchants/{slug}/reviews
Zonder authenticatie
Reviews over de zaak, van recent naar oud.
| Parameter | Standaard | Omschrijving |
page | 1 | Paginanummer. |
per_page | 10 |
Van 1 tot 50. Daarboven teruggebracht tot 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
}
De chronologische volgorde is opgelegd, niet gekozen
Er is geen sorteerparameter, en die komt er niet: AFNOR (§6.3) eist omgekeerde chronologische volgorde als standaardweergave. "Best beoordeeld eerst" als beginsortering aanbieden zou een gekleurde presentatie zijn. Sorteren in JavaScript op de ontvangen pagina valt onder uw verantwoordelijkheid, niet de onze.
Wat uw weergave moet overnemen
-
Minstens twee datums — die van de ervaring en die van de publicatie. Het is een weergaveverplichting, en alleen de API kan ze u leveren. De publieke datums zijn teruggebracht tot de dag (
2026-08-06): geen enkele widget toont het uur.
verified_purchase — de review is aan een werkelijke bestelling gekoppeld. Dat onderscheidt een verzamelde review van een spontaan geplaatste.
-
disputed — de review is betwist en het onderzoek loopt. Hij blijft getoond (zie §4.6); markeer hem liever dan hem te verbergen.
reply — het antwoord van de handelaar hoort voor de lezer bij de review. published_at draagt daar de datum van de laatste wijziging wanneer er een geweest is: de oorspronkelijke datum tonen onder een herschreven tekst zou misleiden.
photos — leeg bij een handelaar wiens abonnement ze niet levert. De reviews blijven volledig, alleen de afbeeldingen ontbreken.
Er is op dit kanaal geen veld total: een lege pagina betekent dat er niets meer te laden valt. Dat is wat de knop "meer tonen" van de widget doet.
GET
/api/v1/public/products/{slug}/{productId}/score
Zonder authenticatie
Score van een product. {productId} is uw catalogusidentificator, die als external_product_id is doorgegeven — wij herschrijven hem nooit. Denk eraan hem te coderen als hij gereserveerde tekens bevat.
| Parameter | Standaard | Omschrijving |
variant | — |
Beperkt de score tot één variant. Bij afwezigheid slaat de score op alle varianten samen — wat het juiste gedrag is zolang de bezoeker zijn maat niet gekozen heeft. |
with_variants | false |
Voegt de uitsplitsing per variant toe. Kost een extra query: schakel het niet in op de productpagina, de meest bekeken pagina van de webshop. |
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 is null wanneer with_variants niet gevraagd is — dat is een afwezigheid van berekening, geen afwezigheid van varianten.
GET
/api/v1/public/products/{slug}/{productId}/reviews
Zonder authenticatie
Reviews van een product. Dezelfde antwoordstructuur en dezelfde pagineringsparameters als de reviews over de zaak, met daarbij het filter variant — handig wanneer een maatkiezer alleen de reviews van de gekozen variant wil tonen.
curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/reviews?variant=REF-42-ROUGE-L&per_page=5'
Voorzie bij een handelaar wiens abonnement de weergave van productreviews niet omvat een weergave die netjes wegvalt in plaats van een leeg kader: de widget zelf verdwijnt van de pagina.
GET
/api/v1/public/merchants/{slug}/photos
Zonder authenticatie
Goedgekeurde klantfoto's, zonder de tekst van de reviews. Dit voedt een carrousel: zonder dit endpoint zouden vijftig volledige reviews geladen moeten worden — tekst, datums, scores — om er alleen de afbeeldingen uit te houden, op een productpagina die al het thema van de handelaar laadt.
| Parameter | Standaard | Omschrijving |
produit | — |
Beperkt tot één product. Bij afwezigheid worden de foto's van de hele webshop teruggegeven — wat een carrousel op de startpagina voedt. |
limite | 24 |
Van 1 tot 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
}
]
}
De URL's zijn absoluut: deze JSON wordt gelezen door JavaScript dat op het domein van de webshop draait, waar een relatieve URL naar de webshop zelf zou wijzen. Gebruik width en height om de ruimte vóór het laden te reserveren — anders springt de productpagina onder de ogen van de bezoeker.
Bij een handelaar wiens abonnement de foto's niet levert luidt het antwoord { "photos": [] } met status 200, nooit een fout: de carrousel verdwijnt netjes in plaats van een mislukt kader te tonen.
GET
/api/v1/public/merchants/{slug}/display
Zonder authenticatie
Weergave-instellingen die de handelaar in zijn omgeving bepaalt. Daarmee kunnen de tags eens en voor altijd in een thema geplaatst worden, waarna een element aangezet, uitgezet of verplaatst wordt zonder de code van de webshop aan te raken.
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/nl/m/tissufiesta"
}
| Instelling | Standaard | Betekenis |
badge_flottant | false |
Scorebadge vastgezet in een hoek van het scherm. Standaard uit: hij ligt over de pagina van de handelaar heen, en er mag bij hem niets verschijnen zonder dat hij erom gevraagd heeft. |
badge_cote | droite | droite of gauche. |
badge_decalage | 16 | Afstand in pixels tot de rand. |
seuil_avis | 1 |
Aantal reviews waaronder de weergave verdwijnt. Zie §6: "geen reviews" tonen is erger dan niets tonen. |
etoiles_fiche | true | Sterren op de productpagina. |
etoiles_vignettes | true | Sterren op de miniaturen in overzichten. |
onglet_avis | true | Tabblad "Reviews" van de productpagina. |
bloc_accueil | true | Reviewblok op de startpagina. |
display is altijd volledig, standaardwaarden inbegrepen: uw code hoeft onze standaardwaarden niet te kennen en ze evenmin over te nemen — verandert er ooit één, dan volgt hij vanzelf.
accent_color is null wanneer de handelaar geen kleur gekozen heeft of wanneer zijn abonnement dat niet meer toelaat. Voorzie altijd een terugvalkleur aan uw kant: dat doet de widget ook, waarvan de tint alleen bestaat als hij opgegeven is.
GET
/api/v1/public/health
Zonder authenticatie
Beschikbaarheid van de dienst. Te bevragen door een sonde of door de gezondheidscontrole van een module.
HTTP/1.1 200 OK
{ "status": "ok" }
Bewust minimaal: geen databasetoegang, geen externe afhankelijkheid. Traagheid van de database mag geen valse storingsmelding uitlokken — en omgekeerd zegt dit endpoint niets over de toestand van de database. Om te controleren of een sleutel werkt is /cms/ping degene die aangeroepen moet worden.
6. Weergavewidgets
Vier HTML-elementen om in een thema te plaatsen. Eén script te laden, geen afhankelijkheden, geen configuratie: het adres van de API wordt afgeleid uit de URL van het script zelf.
<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>
Het script wordt één keer per pagina geladen, waar u wilt; de elementen mogen ervóór geplaatst worden. Het wordt bediend onder /widget/v1/: een onverenigbare wijziging verschijnt als /v2/ en deze blijft bediend zoals hij is — hij leeft in thema's die niemand zal bijwerken.
De vier elementen
| Element | Wat het toont | Waar te plaatsen |
<avis-score> |
Gemiddelde score, sterren, aantal reviews. |
Productpagina, kop van de webshop, pagina "over ons". |
<avis-liste> |
Gepagineerde reviews, met foto's en antwoorden van de handelaar. |
Tabblad "Reviews" van een productpagina, aparte pagina. |
<avis-carrousel> |
Uitsluitend klantfoto's, aanklikbaar. |
Productpagina, startpagina. |
<avis-flottant> |
Scorebadge vastgezet in een hoek, aanklikbaar. |
Het gedeelde sjabloon, één keer voor de hele site. |
Attributen
| Attribuut | Elementen | Standaard | Rol |
marchand | alle | — |
Verplicht. Publieke identificator van de handelaar, die van zijn publieke pagina. |
produit |
score, lijst, carrousel | — |
Uw catalogusidentificator (external_product_id). Bij afwezigheid slaat het element op de hele webshop. |
langue | alle | lang van de pagina |
Taal van de labels. Bij ontbreken het lang-attribuut van het document — dat het thema al invult — en daarna Frans. Alleen fr en en zijn werkelijk vertaald; elke andere waarde valt terug op het Frans in plaats van halfvertaalde labels te tonen. |
mini | score | 1 |
Aantal reviews waaronder het element verdwijnt. Op 3 toont een pagina met slechts twee reviews niets, in plaats van een score die op vrijwel niets berust. |
par-page | liste | 5 |
Aantal reviews dat tegelijk geladen wordt; een knop "Meer tonen" laadt de rest. |
max | carrousel | 12 |
Aantal foto's, maximaal 50. |
Volledig voorbeeld op een productpagina
<!-- 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>
Een widget die niets toont is niet noodzakelijk stuk
Drie situaties laten een element verdwijnen, en elke keer is dat gewild: geen reviews (of minder dan de drempel), geen foto's voor de carrousel, en elke netwerk- of serverfout.
"Nog geen reviews" tonen op een productpagina is erger dan niets tonen: de bezoeker concludeert dat niemand besteld heeft. En een foutmelding op de webshop van een handelaar omdat onze API hapert zou onverdedigbaar zijn — het element trekt zich terug uit de opmaak, de productpagina blijft intact.
Praktisch gevolg voor de integrator: bouw geen opmaak die een vaste hoogte voor een widget reserveert. Hij kan helemaal niets innemen.
Wat de handelaar zonder u bestuurt
De elementen lezen /display bij het laden. Twee instellingen komen daarvandaan in plaats van uit een attribuut, en dat is bewust: de handelaar kan zijn tags eens en voor altijd plaatsen en daarna vanuit zijn omgeving van gedachten veranderen zonder zijn thema opnieuw te openen.
-
De zwevende badge — aan of uit, rechts of links, met zijn afstand tot de rand.
<avis-flottant> in het sjabloon geplaatst toont niets zolang de handelaar hem niet heeft ingeschakeld. Hij staat standaard uit: er mag bij hem niets verschijnen zonder dat hij erom gevraagd heeft.
-
De merkkleur — toegepast op de vlakken die dat toelaten. De sterren behouden hun amber, net als het groen van "Geverifieerde aankoop" en het amber van "Betwist": die kleuren dragen betekenis, ze versieren niet, en ze overschilderen zou de score onleesbaar maken bij een handelaar wiens merk lichtgeel of wit is.
De instellingen worden slechts één keer per pagina opgevraagd, zelfs met vier elementen: het lopende verzoek wordt gedeeld. Dat voorkomt dat de widget het script is dat de productpagina vertraagt — een terecht verwijt aan de meeste reviewmodules.
Afscherming ten opzichte van het thema
Elk element rendert zijn inhoud in een shadow DOM: de CSS van het thema loopt niet over op de widget, en die van de widget niet op de webshop. Geen van beide zou in de andere richting aanvaardbaar zijn.
Een gevolg om te kennen voordat u het probeert: uw CSS-regels zullen de binnenkant van de widgets niet bereiken. De enige voorziene aanpassing is de merkkleur, ingesteld in de handelaarsomgeving. Een werkelijk op maat gemaakte weergave loopt via de Publieke API — precies waarvoor die gedocumenteerd is.
Voordat u ook maar iets plakt
Onder PrestaShop en WooCommerce plaatst de module deze tags zelf, op de juiste plek in het thema. Handmatig plakken is bedoeld voor andere platformen en maatwerkthema's — bekijk de modules.
7. MCP-server
MCP (Model Context Protocol) stelt dezelfde mogelijkheden beschikbaar als de API, in een vorm die een AI-assistent zelf kan ontdekken. Waar een ontwikkelaar documentatie leest, de authenticatie schrijft en de JSON interpreteert, vraagt de assistent de lijst met hulpmiddelen op, leest hun beschrijvingen en roept ze aan.
Concreet: de handelaar sluit zijn assistent op deze server aan en schrijft dan "welke reviews hebben nog geen antwoord?" of "beantwoord deze en bied excuses aan voor de vertraging". Niemand heeft integratiecode geschreven.
| Waarde |
| Adres | https://louis.guide/api/v1/mcp |
| Transport | JSON-RPC 2.0 over HTTP, via POST |
| Protocolversie | 2024-11-05 |
| Aangekondigde server | avis-clients, versie 1.0.0 |
| Mogelijkheden | tools — geen resources, geen prompts |
| Authenticatie |
Bearer-token (§7.1) of API-sleutel + HMAC-handtekening (§7.2) |
| Abonnement | Betaald — anders JSON-RPC-fout -32001 |
7.1 Een assistent aansluiten: het token
Dit is de gewone weg, en de enige die niets geïnstalleerds vereist. De handelaar maakt een token aan in zijn omgeving — Instellingen · Verzamelen, sectie "Een assistent aansluiten" — en plakt het vervolgens samen met het serveradres in de configuratie van zijn assistent.
POST https://louis.guide/api/v1/mcp
Authorization: Bearer mcp_live_…
Content-Type: application/json
Gebruikelijke vorm van het configuratiebestand van een MCP-client:
{
"mcpServers": {
"avis-clients": {
"url": "https://louis.guide/api/v1/mcp",
"headers": { "Authorization": "Bearer mcp_live_…" }
}
}
}
Controle in één commando, voordat u iets aansluit:
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"}'
Wat het token wel en niet kan
| Eigenschap | Gedrag |
| Bereik |
Uitsluitend /api/v1/mcp. Aangeboden op de CMS-API wordt het niet eens onderzocht: geen doorgifte van bestellingen, geen export van reviews, geen koppeling. |
| Schrijven |
Standaard verboden. De handelaar vinkt bij het aanmaken uitdrukkelijk "het opstellen van antwoorden toestaan" aan. Zonder dat verschijnt het hulpmiddel repondre_a_un_avis niet eens in tools/list — de assistent zal het dus niet voorstellen. |
| Levensduur |
Eén jaar, daarna vervalt het. Het is in tien seconden opnieuw aangemaakt. |
| Intrekking |
Onmiddellijk en definitief, token per token, zonder de API-sleutels of de modules van de handelaar aan te raken. |
| Bewaring |
Slechts één keer getoond. Wij bewaren er niets dan een vingerafdruk van: niemand kan het opnieuw tonen, wijzelf incluis. |
| Aantal |
Maximaal drie geldige tokens per account. |
Een bearer-token reist mee: behandel het als een wachtwoord
Anders dan het HMAC-geheim vertrekt het bij elk verzoek en leeft het in de configuratie van een dienst die wij niet beheren. Dat is de prijs van de rechtstreekse aansluiting, en daarom is het afgeschermd, verlopend, intrekbaar en standaard stom bij schrijven. Zet het nooit in een URL noch in een coderepository: URL's belanden in de logs van elke tussenpersoon die ze passeren.
De pogingen zijn begrensd op 20 mislukkingen per kwartier per IP-adres — daarboven luidt het antwoord 429.
7.2 Alternatief: API-sleutel en HMAC-handtekening
Hetzelfde endpoint aanvaardt de authenticatie die in §2 beschreven staat: API-sleutel en HMAC-handtekening. Ze heeft een echt voordeel — het geheim verlaat de server van de handelaar nooit — en een nadeel dat haar tot integrators beperkt: geen enkele MCP-client kan bij elke aanroep een HMAC herberekenen, ze zetten alleen vaste headers.
Er is dus een brug nodig: een klein programma dat door de assistent gestart wordt, dat de JSON-RPC op zijn standaardinvoer ontvangt, ondertekent, verstuurt en het antwoord teruggeeft. Node.js 18 of nieuwer, geen afhankelijkheden. Bewaar het als 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');
}
}
});
Aangifte aan MCP-clientzijde (gebruikelijke vorm van de configuratiebestanden):
{
"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"
}
}
}
}
Het geheim verlaat de machine niet. Het dient om lokaal te ondertekenen; wat over het netwerk gaat is de handtekening. Een absoluut pad is onmisbaar: de assistent start het programma niet vanuit de map waarin u het geschreven hebt.
Om de brug te controleren voordat u iets aansluit, voert u er met de hand één regel in. Er hoort een lijst met hulpmiddelen terug te komen:
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 Methoden
| Methode | Effect |
initialize |
Kondigt de protocolversie, de mogelijkheden en de identiteit van de server aan. |
tools/list | Catalogus van de hulpmiddelen en hun invoerschema's. |
tools/call | Voert een hulpmiddel uit — params.name en params.arguments. |
notifications/initialized, ping | Bevestigd met een leeg resultaat. |
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 De drie hulpmiddelen
Twee lezen, één schrijft. De scheidslijn is niet technisch: reviews lezen brengt geen enkel risico mee, terwijl een antwoord publiceren de handelaar in het openbaar laat spreken op een pagina die wij hosten — één ongelukkige formulering bij een gevoelige review, en er gaat een schermafbeelding rond.
Daarom geeft tools/list slechts twee hulpmiddelen terug wanneer de beller een alleen-lezen token aanbiedt. Zet de lijst dus niet vast in code: vraag hem op, en kondig aan de handelaar alleen aan wat erin staat.
hulpmiddel
lister_avis
Gepubliceerde reviews over de zaak, van recent naar oud. Het hulpmiddel dat de assistent aanroept voor "toon me de ontevreden klanten" of "wat heeft nog geen antwoord?".
| Argument | Type | Standaard | Effect |
note_max | geheel getal 1–5 | — |
Geeft uitsluitend reviews terug waarvan de score kleiner dan of gelijk aan dit getal is. |
sans_reponse | booleaans | false |
Sluit reviews uit waarop al een antwoord gepubliceerd is. |
limite | geheel getal 1–50 | 20 |
Aantal gelezen reviews. |
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lister_avis",
"arguments": { "note_max": 3, "sans_reponse": true, "limite": 10 }
}
}
Het resultaat is een tekstblok met JSON erin — dat is de vorm die het protocol voor een gestructureerd resultaat voorziet, en die assistenten kunnen lezen:
{
"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 filtert na de limiet, niet ervoor. Twintig reviews zonder antwoord vragen leest de laatste 20 gepubliceerde reviews en verwijdert daaruit de reeds behandelde: het resultaat kan er veel minder bevatten, en total zegt dat. Verhoog limite om het leesvenster te verbreden.
Alleen gepubliceerde reviews komen hier terug: niet de wachtende, niet de afgewezen, niet de ingetrokken. Daarvoor is er GET /cms/reviews met zijn status-filter.
hulpmiddel
resume_reputation
Een overzicht, zonder argument. Wat de assistent aanroept voor "hoe staat het met mijn reputatie?".
{
"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 telt de reviews over de zaak; avis_produit telt apart die over een artikel. Ze optellen zou een totaal geven dat met geen enkele getoonde score overeenkomt.
hulpmiddel
repondre_a_un_avis
Publiek schrijven
| Argument | Type | Verpl. | Effect |
avis_id | tekenreeks | ja | Identificator van de review, zoals teruggegeven door lister_avis. |
contenu | tekenreeks | ja |
Tekst van het antwoord, afgekapt op 3000 tekens. |
{
"avis_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"publiee": false,
"message": "Antwoord als concept opgeslagen. De handelaar moet het in zijn omgeving goedkeuren voordat het verschijnt."
}
Gepubliceerd of concept: dat heeft de handelaar beslist, niet de aanroep
De handelaar stelt in zijn omgeving in of antwoorden die door een assistent opgesteld zijn rechtstreeks vertrekken dan wel op zijn nalezing wachten. Geen van beide gedragingen is absoluut juist: wie twee reviews per week krijgt wil nalezen, wie er tweehonderd krijgt wil dat het vertrekt.
Die instelling is geen parameter van het verzoek, en dat is wezenlijk: als de assistent zelf kon kiezen of hij nagelezen moet worden, zou de garantie niets waard zijn. Het veld publiee en het veld message zeggen wat er werkelijk gebeurd is — een assistent moet dat onverkort aan de handelaar melden.
Bij een review die al een antwoord heeft wordt dat antwoord vervangen. Een reeds openbaar antwoord blijft openbaar: een herschrijving zet het nooit terug op concept, wat het van de pagina zou laten verdwijnen zonder dat iemand dat beslist heeft.
Wat een assistent moet weten voordat hij schrijft
-
Antwoord in de taal van de review — het veld
langue is daarvoor bedoeld. Een Frans antwoord onder een Nederlandse review zegt de lezer dat die niet gelezen is.
-
Beloof nooit een commercieel gebaar dat niet waargemaakt kan worden: terugbetaling, omruiling, korting. Dit antwoord is openbaar en bindend voor de handelaar.
-
Geen enkel hulpmiddel wijzigt of verwijdert een review, en dat zal er ook niet komen. Een assistent die gevraagd wordt een review "te laten verwijderen" kan hem alleen melden, met een ontvankelijke reden (§4.6) — de score is er geen.
7.5 Fouten
Altijd een HTTP-status 200, ook bij een fout: in JSON-RPC reist de fout in de body. Een 4xx zou de client doen geloven dat het transport gefaald heeft, en de meeste zouden opnieuw proberen in plaats van het bericht te tonen.
De enige uitzondering: de authenticatie, geweigerd voordat de JSON-RPC-laag bereikt wordt. Ze antwoordt met de gebruikelijke foutenvelop van de API.
| Status | Code | Oorzaak |
| 401 | invalid_mcp_token |
Token onbekend, ingetrokken of verlopen — bewust niet te onderscheiden. De handelaar maakt er een nieuwe aan in zijn omgeving. |
| 401 | codes uit §2 |
HMAC-weg: sleutel ontbreekt, handtekening of tijdstempel geweigerd. |
| 429 | too_many_attempts |
Meer dan 20 mislukte authenticaties in een kwartier vanaf hetzelfde adres. Wacht liever dan in een lus opnieuw te proberen. |
| Code | Betekenis | Wat te doen |
-32001 |
Het abonnement van de handelaar omvat geen MCP-toegang. |
Overstappen op een betaald abonnement; de reviews blijven openbaar leesbaar. |
-32601 | Onbekende JSON-RPC-methode. | method controleren. |
-32602 | Onbekend hulpmiddel. | tools/list aanroepen, de namen niet vast in code zetten. |
-32603 |
Fout van de lokale brug — netwerk, ontbrekend geheim. |
Deze code komt van de brug hierboven, niet van de server. |
De zakelijke fouten van een hulpmiddel zijn geen JSON-RPC-fouten: het antwoord blijft een resultaat, met isError: true en een object { "erreur": "…" } in de tekst. Dat geldt voor een niet-gevonden review, lege inhoud, of een antwoord dat met een alleen-lezen token geprobeerd wordt. De assistent kan het zo aan de handelaar uitleggen in plaats van een storing aan te kondigen.
8. Inkomende webhooks
Er is geen uitgaande webhook
Het platform belt u niet: het verstuurt geen enkele melding naar uw server bij de publicatie van een review, een antwoord of een moderatiebeslissing. Om de activiteit te volgen, bevraagt u GET /api/v1/cms/reviews in uw eigen tempo, gefilterd op status=published en met vergelijking van published_at met uw vorige doorloop.
Een doorloop per uur voldoet voor nagenoeg elk gebruik: reviews komen niet per seconde binnen, en het publicatietempo van een webshop telt in eenheden per dag. Elke minuut bevragen laat niets sneller verschijnen.
De twee onderstaande endpoints bestaan voor welbepaalde bellers — onze sms-operator en onze betaaldienstverlener. Geen enkele integrator hoeft ze aan te roepen, en niemand kan het: beide zijn afgesloten met een geheim dat niet verspreid wordt.
POST
/api/v1/stripe/webhook
Stripe-handtekening
Ontvangt de abonnementsgebeurtenissen: checkout.session.completed, customer.subscription.created, .updated en .deleted. Dat is wat een account naar een betaald abonnement doet omslaan, en dus wat de reviews-API en de MCP-toegang opent.
De handtekening van de payload is het enige wat deze route beschermt: zonder haar zou iedereen "abonnement actief" kunnen posten en zichzelf met één curl-verzoek het betaalde abonnement kunnen geven. Ze wordt gecontroleerd vóór enige lezing van de inhoud, en een ontbrekend geheim laat het verzoek falen in plaats van het door te laten.
Niet-verwerkte gebeurtenissen worden met een 200 bevestigd ({ "ignored": … }): Stripe beschouwt elk antwoord dat geen 2xx is als een mislukking en herhaalt drie dagen lang, met oplopende tussenpozen. Een 404 antwoorden op een gebeurtenistype waar wij niets mee doen zou duizenden nutteloze herhalingen opleveren, en vervolgens het uitschakelen van het eindpunt aan hun kant. Omgekeerd antwoordt een echte verwerkingsfout wel degelijk 500 — daar willen we dat Stripe opnieuw stuurt in plaats van een handelaar die betaald heeft op het gratis abonnement te laten staan.
POST
/api/v1/sms/inbound/{token}
Gedeeld token
Ontvangt de inkomende sms'jes, dat wil zeggen de "STOP"-berichten. De operator verwerkt het sleutelwoord aan zijn kant en levert niet meer af — maar zonder dit endpoint zouden wij er niets van weten: we zouden hem berichten blijven sturen die wel gefactureerd maar nooit ontvangen worden, het verzet zou verdwijnen op de dag dat we van operator wisselen, en we zouden niet kunnen bewijzen dat we het gehonoreerd hebben terwijl de bewijslast bij ons ligt.
Het token reist mee in het pad, wat zwakker is dan een handtekening — maar het is wat de interfaces van de Franse operatoren weten in te stellen. Vandaar dat dit endpoint niets anders kan dan een verzet toevoegen: het ergste wat een frauduleuze aanroep oplevert is dat er geen sms naar een nummer meer vertrekt. Hinderlijk, nooit gevaarlijk, en omkeerbaar vanuit de beheeromgeving.
Het sleutelwoord wordt als eerste woord van het bericht gezocht, niet ergens middenin: wie schrijft "dit moet stoppen, die winkel is waardeloos" vraagt niet om afgemeld te worden, en hem toch afmelden zou het kanaal wegnemen waarlangs hij rechtmatig benaderd wordt. Het verzet wordt voor alle handelaren geregistreerd: het inkomende bericht zegt niet om welke webshop het gaat — de persoon antwoordt op het verzendnummer — en raden zou zowel onjuist als gevaarlijk zijn.
Het snijdt alleen het sms-kanaal af. E-mail blijft vertrekken: die draagt de beheerlink van de review en de verplichte vermeldingen, en een verzet dat op één kanaal is geuit geldt niet voor het andere.
9. Snelheidslimieten
De limieten worden berekend over een schuivend venster: geen teller die op het hele uur op nul springt, en dus geen mogelijke uitbarsting aan het begin van een periode.
| Kanaal | Limiet | Sleutel | Waarom dit getal |
Publieke API /api/v1/public/ |
60 / minuut |
IP-adres |
Een productpagina doet twee aanroepen: dat laat 30 laadbeurten per minuut vanaf hetzelfde adres toe. Ruim voor een bezoeker, krap voor een contentschraper. |
Beschikbaarheid /public/health |
geen |
— |
Bewust uitgesloten: hij wordt doorlopend door de monitoring bevraagd, en hem afknijpen zou valse storingsmeldingen opleveren. |
Koppeling openen POST /pairing |
10 / uur |
IP-adres |
Elke aanroep maakt zonder enige authenticatie een regel in de database aan. Tien volstaat ruimschoots voor een integrator die opnieuw begint. |
Pollen POST /pairing/{code} |
240 / 15 minuten |
IP-adres |
Bewust ruim: de module bevraagt elke vijf seconden terwijl de handelaar zijn account aanmaakt, zijn adres bevestigt en goedkeurt. |
| MCP-authenticatie met token |
20 mislukkingen / 15 minuten |
IP-adres |
Telt uitsluitend de mislukkingen: een werkende koppeling raakt hem nooit. Stopt het aftasten van elders gevonden tokens en voorkomt dat een verkeerd ingestelde client de logs verzuipt. |
| Een review indienen |
10 / minuut |
Token van de uitnodiging |
Per token en niet per IP: meerdere klanten van hetzelfde bedrijf delen vaak één uitgaand adres, en ze samen afknijpen zou rechtmatige inzendingen straffen. |
| Publieke melding van een review |
5 / uur |
IP-adres |
Openstaand voor elke lezer (DSA-verplichting), dus ook voor elke bot. Elke inzending maakt een regel in de moderatiewachtrij aan. |
De CMS-API is niet afgeknepen, wat niet alles toestaat
Op de ondertekende endpoints (/api/v1/cms/ en MCP) geldt vandaag geen enkele snelheidslimiet: ze zijn geauthenticeerd, en het werkelijke volume wordt begrensd door het uitnodigingsquotum van de handelaar. Verwerk de 429 toch — er kan een limiet bij komen, en een integratie die ze niet kan lezen valt uit op de dag dat ze verschijnt.
In de praktijk: geef bestellingen door zodra ze binnenkomen in plaats van in nachtelijke partijen van duizenden, en bevraag de reviews per uur in plaats van per minuut (§8). Afwijkend volume is bij ons zichtbaar en leidt tot contact, niet tot een stille afsluiting.
Wat een overschrijding teruggeeft
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Remaining: 0
{
"error": {
"code": "rate_limit_exceeded",
"message": "Te veel verzoeken. Probeer het over enkele ogenblikken opnieuw."
}
}
Retry-After geeft het aantal seconden dat u moet wachten. Respecteer het: onmiddellijk opnieuw proberen verbruikt alleen het volgende venster. Exponentieel uitstel, afgetopt op één minuut, volstaat voor alle hier beschreven gevallen.
Het pollen bij koppeling vormt een uitzondering en antwoordt { "status": "rate_limited" }: het is dezelfde gebeurtenis, uitgedrukt in het vocabulaire van een endpoint dat de module in een lus bevraagt.
10. Gangbare foutcodes
Twee formaten, en meestal maar één om te verwerken
Overal in de API draagt een fout dezelfde envelop:
{
"error": {
"code": "invalid_api_key",
"message": "API-sleutel onbekend, ingetrokken of verlopen."
}
}
De code is stabiel en bedoeld voor uw programma; het message is bedoeld voor de mens die foutzoekt en kan zonder aankondiging geherformuleerd worden. Bouw uw logica nooit op de tekst van het bericht.
Eén enkele uitzondering: POST /cms/orders, bediend door een andere laag, geeft zijn validatiefouten terug als een lijst violations. Een robuuste client leest dus error.code als die bestaat, en valt anders terug op violations.
HTTP-statussen
| Status | Betekenis | Opnieuw proberen? |
| 200 | Geslaagd. In JSON-RPC zit een eventuele fout in de body. | — |
| 201 | Aangemaakt — bestelling geregistreerd, antwoord gepubliceerd. | — |
| 202 | Aanvaard maar niet beslist: de melding komt in de wachtrij. | — |
| 400 | Onleesbaar verzoek. | Nee, corrigeer het. |
| 401 | Sleutel ontbreekt, ongeldig, of handtekening geweigerd. | Nee, tenzij de klok gelijkgezet moet worden. |
| 402 | Het abonnement van de handelaar omvat deze functie niet. | Nee. |
| 404 | Onbekende bron — of buiten uw account. | Nee. |
| 409 | Conflict: de handeling is al verricht. | Nee, het is een toestand, geen storing. |
| 415 | Content-Type ontbreekt of is onverwacht. | Nee, stuur JSON. |
| 422 | Correct opgebouwd verzoek maar geweigerd: ontbrekend veld, waarde buiten bereik. | Nee, corrigeer het. |
| 429 | Snelheidslimiet overschreden. | Ja, na Retry-After. |
| 5xx | Incident aan onze kant. | Ja, met oplopend uitstel. |
Overzicht van de codes
| Code | Status | Waar | Oorzaak en oplossing |
missing_api_key | 401 | CMS, MCP |
Header X-Api-Key ontbreekt. |
invalid_api_key | 401 | CMS, MCP |
Sleutel onbekend, ingetrokken of verlopen — alle drie bewust niet te onderscheiden. Controleer hem in de handelaarsomgeving. |
missing_signature, missing_timestamp |
401 | CMS, MCP |
Niet-ondertekende schrijfactie. Zie §2.1. |
invalid_timestamp | 401 | CMS, MCP |
X-Timestamp is geen Unix-tijdstempel in seconden — meestal milliseconden of een ISO-datum. |
timestamp_out_of_range | 401 | CMS, MCP |
Meer dan 300 s afwijking. Het bericht geeft het exacte verschil: zet de klok gelijk (NTP). |
signature_mismatch | 401 | CMS, MCP |
Loop de vier valkuilen uit §2.3 op volgorde na. |
invalid_mcp_token | 401 | MCP |
Bearer-token onbekend, ingetrokken of verlopen — niet te onderscheiden. De handelaar maakt er een nieuwe aan in zijn omgeving (§7.1). |
too_many_attempts | 429 | MCP |
Te veel mislukte authenticaties vanaf hetzelfde adres. |
plan_required | 402 | Reviews, antwoord |
Functie inbegrepen vanaf het betaalde abonnement. De publieke weergave van reviews blijft gratis. |
merchant_not_found | 404 | Publieke API |
Onbekende publieke identificator. Controleer de slug, niet de handelsnaam. |
review_not_found | 404 | Antwoord, melding |
Identificator onbekend, verkeerd opgebouwd, of van een andere handelaar: de afscherming vereist dat ze niet onderscheiden worden. |
already_reported | 409 | Melding |
Er loopt al een dossier over deze review. |
content_required | 422 | Antwoord |
content ontbreekt of is leeg na opschoning. |
invalid_reason | 422 | Melding |
Reden buiten de lijst. Een lage score is geen ontvankelijke reden (§4.6). |
invalid_request | 422 | Koppeling |
shop_domain ontbreekt of is onbruikbaar. |
rate_limit_exceeded | 429 | Publieke API |
Zie §9 en de header Retry-After. |
-32001 | 200 | MCP |
Abonnement zonder MCP-toegang (een JSON-RPC-fout, geen HTTP-fout). |
-32601, -32602 | 200 | MCP |
Onbekende methode of onbekend hulpmiddel. Ga via tools/list. |
Drie symptomen, en waar te beginnen
| Symptoom | Meest voorkomende oorzaak |
| "Gisteren werkte alles, vandaag is alles 401." |
De serverklok is gaan afwijken. GET /cms/ping geeft server_time terug: vergelijk het met het uwe voordat u ergens anders zoekt. |
| "De ping lukt, maar al mijn schrijfacties mislukken." |
De sleutel is goed, de handtekening niet — precies wat dat gescheiden regime u laat concluderen. De body is bijna altijd na het ondertekenen opnieuw gecodeerd (§2.3). |
| "De widget toont niets, maar de API antwoordt 200 in de console." |
Domein niet opgegeven aan handelaarszijde: de browser blokkeert het lezen bij gebrek aan een CORS-header. Of eenvoudigweg: er zijn nog geen reviews — een lege widget haalt zichzelf van de pagina (§6). |
Als niets hiervan past
Schrijf ons vanuit de handelaarsomgeving en voeg drie dingen bij: het aangeroepen pad, het tijdstempel van het verzoek en de ontvangen foutcode. Met die drie is het verzoek in de logs terug te vinden; zonder is het enige mogelijke antwoord u erom te vragen.
↑ Terug naar het begin