Naar de inhoud

API-documentatie

Alles wat het platform blootstelt: bestellingen doorgeven, reviews lezen en beantwoorden, publieke weergave, en een AI-assistent aansluiten. Negentien endpoints, drie kanalen, één sleutel.

Een handelaar hoeft normaal gesproken niets te programmeren. De PrestaShop- en WooCommerce-modules doen alles wat hier beschreven staat — bekijk de modules. Deze pagina is bedoeld voor ontwikkelaars die een platform zonder module, een ERP, een CRM of een intern hulpmiddel integreren.

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.

GebruikAdres
API (alle kanalen)https://louis.guide
Handelaarsomgevinghttps://louis.guide/app
Widgetscripthttps://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

  1. Maak een handelaarsaccount aan op de handelaarsomgeving.
  2. Bevestig het e-mailadres en open vervolgens de sectie met API-sleutels.
  3. 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-methodeVereiste headersWaarom
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

HeaderInhoud
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
StukExacte 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.

  1. 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.
  2. 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.
  3. 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".
  4. 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.

CodeOorzaakWat 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.

VeldTypeVerplichtOmschrijving
shop_domaintekenreeksja Domein van de webshop, bv. shop.voorbeeld.nl.
platformtekenreeksnee prestashop, woocommerce, custom… standaard unknown.
shop_nametekenreeksnee Leesbare naam van de webshop, hergebruikt bij het aanmaken van het account.
platform_versiontekenreeksnee Versie van het platform, bv. 8.1.6.
plugin_versiontekenreeksnee Versie van de aanroepende module.
shop_uidtekenreeksnee 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

StatusCodeOorzaak
422invalid_requestshop_domain ontbreekt of is onbruikbaar.
429rate_limitedMeer 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.

VeldTypeVerplichtOmschrijving
poll_tokentekenreeksja 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

WaardeBetekenisWat te doen
pendingDe handelaar heeft nog niet beslist.Blijven pollen.
approvedGoedgekeurd. Het antwoord draagt de gegevens.Ze opslaan, stoppen met pollen.
rejectedDe handelaar heeft geweigerd.Stoppen en het hem melden.
expiredVijftien 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_limitedTe 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.

MogelijkheidWaarover 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

VeldTypeVerpl.Omschrijving
external_order_idtekenreeks (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.
customerobjectja Identiteit van de te benaderen klant — zie de volgende tabel.
experienced_atISO 8601ja Datum van levering of gebruik, met expliciete tijdzone. Zie het kader hieronder: dit is niet de besteldatum.
sourceobjectja Technische context van de verzending — zie verderop.
itemsarray (max. 200)nee Artikelen. Zonder hen wordt er geen enkele productreview gevraagd — alleen de review over de zaak.
amountdecimale tekenreeksnee Totaalbedrag, bv. "129.90". Nooit een drijvendekommagetal.
currencyISO 4217nee "EUR", "CHF"…
channelopsommingnee ecommerce_order (standaard), pos_transaction, qr_scan, manual, csv_import, nfc.
location_idtekenreeks (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_daysgeheel getal 0–365nee 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_idtekenreeks (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_labeltekenreeks (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

VeldTypeVerpl.Omschrijving
emaile-mail (255)ja Het enige persoonsgegeven dat wij onversleuteld aanvaarden. Gewist na de inzendtermijn; alleen de vingerafdruk blijft over.
countryISO 3166-1 alpha-2nee* *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.
localefr, en, nl, de, it, esnee Taal van de uitnodigingsmail. Bij ontbreken de standaardtaal van de handelaar — een Nederlandstalige klant in het Frans benaderen laat het antwoordpercentage instorten.
phonetekenreeks (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_nametekenreeks (100)nee Personalisering van de uitnodiging en getoonde naam van de auteur.
companytekenreeks (255)nee Handelsnaam, voor een zakelijke bestelling.
postal_code, citytekenreeksnee 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.

VeldTypeVerpl.Omschrijving
platformtekenreeks (50)ja prestashop, woocommerce, shopify, magento, custom…
platform_versiontekenreeks (30)nee Bv. 8.1.6.
plugin_versiontekenreeks (30)nee Versie van uw integratie. Bij elke oplevering te verhogen.
triggertekenreeks (100)nee Gebeurtenis achter de verzending, bv. woocommerce_order_status_completed. Maakt begrijpelijk waarom een bestelling te vroeg of te laat vertrekt.
shop_uidtekenreeks (80)nee* *Doorslaggevend bij meerdere webshops. Identificator die eenmaal bij de installatie getrokken en bewaard wordt. Zie het kader.
shop_idtekenreeks (50)nee Identificator van de webshop bij het platform. Dient als terugval wanneer shop_uid ontbreekt.
shop_nametekenreeks (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_idtekenreeksnee 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

VeldTypeVerpl.Omschrijving
external_product_idtekenreeks (100)ja Identificator van het product in uw catalogus.
nametekenreeks (255)ja Naam van het product zoals aan de klant getoond.
variant_idtekenreeks (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_labeltekenreeks (255)nee Leesbaar label: "Kleur: rood, Maat: L".
gtin8 tot 14 cijfersnee* EAN-13 of omgezette UPC-A. Aggregatiesleutel tussen handelaren, en een eis van Google om sterren in zijn resultaten te tonen.
upc, isbn, mpntekenreeksnee 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, brandtekenreeksnee Interne referentie en merk.
category_id, category_nametekenreeksnee Hoofdcategorie in uw catalogus.
product_url, image_urlURL (500)nee Gebruikt in de uitnodigingsmail: een productafbeelding verhoogt het aantal inzendingen merkbaar.
imageslijst met URL's (max. 10)nee Bijkomende afbeeldingen.
descriptiontekenreeks (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.
tagslijst (max. 30)nee Sleutelwoorden van het product, elk 60 tekens.
meta_title, meta_descriptiontekenreeksnee Metagegevens van de productpagina.
quantitygeheel getal > 0nee Standaard 1.
unit_pricedecimale tekenreeksnee 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

WaardeBetekenis
pendingOntvangen, in afwachting van inplanning.
scheduledUitnodiging ingepland.
solicitedReviewverzoek naar de klant verstuurd.
reviewedDe klant heeft zijn review ingediend.
cancelledGeannuleerd vóór verzending.
expiredInzendtermijn 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."
    }
  ]
}
StatusOorzaakWat 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.

ParameterStandaardOmschrijving
typemerchant merchant voor reviews over de zaak, product voor productreviews.
statusalle 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.
page1Paginanummer.
per_page25 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.

StatusCodeOorzaak
402plan_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.

VeldTypeVerpl.Omschrijving
contenttekenreeksja 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.

StatusCodeOorzaak
402plan_requiredAbonnement zonder antwoorden op reviews.
404review_not_found Identificator onbekend, verkeerd opgebouwd, of van een andere handelaar — alle drie de gevallen zijn niet te onderscheiden, en de afscherming vereist dat.
422content_requiredcontent 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.

VeldTypeVerpl.Omschrijving
reasonopsommingja Reden, te kiezen uit onderstaande lijst.
detailtekenreeksnee 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

WaardeWanneer in te roepen
inappropriate_contentBelediging, haatdragende uitlatingen, onwettige inhoud.
spam_or_advertisingReclame, commerciële link, geautomatiseerde inhoud.
off_topicZonder verband met de beleefde ervaring — de vervoerder, het weer.
conflict_of_interestConcurrent, oud-werknemer, betaalde review.
personal_data_disclosureDe 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.

StatusCodeOorzaak
404review_not_foundIdentificator onbekend, verkeerd opgebouwd, of buiten uw account.
409already_reportedEr loopt al een melding over deze review.
422invalid_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.

ParameterStandaardOmschrijving
page1Paginanummer.
per_page10 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.

ParameterStandaardOmschrijving
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_variantsfalse 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.

ParameterStandaardOmschrijving
produit— Beperkt tot één product. Bij afwezigheid worden de foto's van de hele webshop teruggegeven — wat een carrousel op de startpagina voedt.
limite24 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"
}
InstellingStandaardBetekenis
badge_flottantfalse 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_cotedroitedroite of gauche.
badge_decalage16Afstand in pixels tot de rand.
seuil_avis1 Aantal reviews waaronder de weergave verdwijnt. Zie §6: "geen reviews" tonen is erger dan niets tonen.
etoiles_fichetrueSterren op de productpagina.
etoiles_vignettestrueSterren op de miniaturen in overzichten.
onglet_avistrueTabblad "Reviews" van de productpagina.
bloc_accueiltrueReviewblok 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

ElementWat het toontWaar 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

AttribuutElementenStandaardRol
marchandalle— 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.
langueallelang 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.
miniscore1 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-pageliste5 Aantal reviews dat tegelijk geladen wordt; een knop "Meer tonen" laadt de rest.
maxcarrousel12 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
Adreshttps://louis.guide/api/v1/mcp
TransportJSON-RPC 2.0 over HTTP, via POST
Protocolversie2024-11-05
Aangekondigde serveravis-clients, versie 1.0.0
Mogelijkhedentools — geen resources, geen prompts
Authenticatie Bearer-token (§7.1) of API-sleutel + HMAC-handtekening (§7.2)
AbonnementBetaald — 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

EigenschapGedrag
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

MethodeEffect
initialize Kondigt de protocolversie, de mogelijkheden en de identiteit van de server aan.
tools/listCatalogus van de hulpmiddelen en hun invoerschema's.
tools/callVoert een hulpmiddel uit — params.name en params.arguments.
notifications/initialized, pingBevestigd 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?".

ArgumentTypeStandaardEffect
note_maxgeheel getal 1–5— Geeft uitsluitend reviews terug waarvan de score kleiner dan of gelijk aan dit getal is.
sans_reponsebooleaansfalse Sluit reviews uit waarop al een antwoord gepubliceerd is.
limitegeheel getal 1–5020 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
ArgumentTypeVerpl.Effect
avis_idtekenreeksjaIdentificator van de review, zoals teruggegeven door lister_avis.
contenutekenreeksja 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.

StatusCodeOorzaak
401invalid_mcp_token Token onbekend, ingetrokken of verlopen — bewust niet te onderscheiden. De handelaar maakt er een nieuwe aan in zijn omgeving.
401codes uit §2 HMAC-weg: sleutel ontbreekt, handtekening of tijdstempel geweigerd.
429too_many_attempts Meer dan 20 mislukte authenticaties in een kwartier vanaf hetzelfde adres. Wacht liever dan in een lus opnieuw te proberen.
CodeBetekenisWat te doen
-32001 Het abonnement van de handelaar omvat geen MCP-toegang. Overstappen op een betaald abonnement; de reviews blijven openbaar leesbaar.
-32601Onbekende JSON-RPC-methode.method controleren.
-32602Onbekend 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.

KanaalLimietSleutelWaarom 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

StatusBetekenisOpnieuw proberen?
200Geslaagd. In JSON-RPC zit een eventuele fout in de body.—
201Aangemaakt — bestelling geregistreerd, antwoord gepubliceerd.—
202Aanvaard maar niet beslist: de melding komt in de wachtrij.—
400Onleesbaar verzoek.Nee, corrigeer het.
401Sleutel ontbreekt, ongeldig, of handtekening geweigerd.Nee, tenzij de klok gelijkgezet moet worden.
402Het abonnement van de handelaar omvat deze functie niet.Nee.
404Onbekende bron — of buiten uw account.Nee.
409Conflict: de handeling is al verricht.Nee, het is een toestand, geen storing.
415Content-Type ontbreekt of is onverwacht.Nee, stuur JSON.
422Correct opgebouwd verzoek maar geweigerd: ontbrekend veld, waarde buiten bereik.Nee, corrigeer het.
429Snelheidslimiet overschreden.Ja, na Retry-After.
5xxIncident aan onze kant.Ja, met oplopend uitstel.

Overzicht van de codes

CodeStatusWaarOorzaak en oplossing
missing_api_key401CMS, MCP Header X-Api-Key ontbreekt.
invalid_api_key401CMS, MCP Sleutel onbekend, ingetrokken of verlopen — alle drie bewust niet te onderscheiden. Controleer hem in de handelaarsomgeving.
missing_signature, missing_timestamp 401CMS, MCP Niet-ondertekende schrijfactie. Zie §2.1.
invalid_timestamp401CMS, MCP X-Timestamp is geen Unix-tijdstempel in seconden — meestal milliseconden of een ISO-datum.
timestamp_out_of_range401CMS, MCP Meer dan 300 s afwijking. Het bericht geeft het exacte verschil: zet de klok gelijk (NTP).
signature_mismatch401CMS, MCP Loop de vier valkuilen uit §2.3 op volgorde na.
invalid_mcp_token401MCP Bearer-token onbekend, ingetrokken of verlopen — niet te onderscheiden. De handelaar maakt er een nieuwe aan in zijn omgeving (§7.1).
too_many_attempts429MCP Te veel mislukte authenticaties vanaf hetzelfde adres.
plan_required402Reviews, antwoord Functie inbegrepen vanaf het betaalde abonnement. De publieke weergave van reviews blijft gratis.
merchant_not_found404Publieke API Onbekende publieke identificator. Controleer de slug, niet de handelsnaam.
review_not_found404Antwoord, melding Identificator onbekend, verkeerd opgebouwd, of van een andere handelaar: de afscherming vereist dat ze niet onderscheiden worden.
already_reported409Melding Er loopt al een dossier over deze review.
content_required422Antwoord content ontbreekt of is leeg na opschoning.
invalid_reason422Melding Reden buiten de lijst. Een lage score is geen ontvankelijke reden (§4.6).
invalid_request422Koppeling shop_domain ontbreekt of is onbruikbaar.
rate_limit_exceeded429Publieke API Zie §9 en de header Retry-After.
-32001200MCP Abonnement zonder MCP-toegang (een JSON-RPC-fout, geen HTTP-fout).
-32601, -32602200MCP Onbekende methode of onbekend hulpmiddel. Ga via tools/list.

Drie symptomen, en waar te beginnen

SymptoomMeest 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