Ir al contenido

Documentación de la API

Todo lo que la plataforma expone: transmisión de pedidos, lectura y respuesta a las reseñas, visualización pública y conexión de un asistente de IA. Diecinueve endpoints, tres canales, una sola clave.

Un comerciante normalmente no tiene nada que programar. Los módulos de PrestaShop y WooCommerce hacen todo lo que aquí se describe — ver los módulos. Esta página se dirige a los desarrolladores que integran una plataforma sin módulo, un ERP, un CRM o una herramienta interna.

1. Introducción

La plataforma expone tres canales distintos. No comparten ni el mismo público, ni el mismo régimen de autenticación, ni los mismos límites. Elegir el correcto es la primera decisión de una integración.

Canal Prefijo Para quién Autenticación
API CMS /api/v1/cms/ Módulos de comercio electrónico, ERP, CRM, herramientas internas Clave de API + firma HMAC en escritura
API pública /api/v1/public/ Widgets de visualización, JavaScript de la tienda Ninguna — caudal limitado, CORS restringido
MCP /api/v1/mcp Asistentes de IA (Claude, ChatGPT, otros) La misma clave y la misma firma que la API CMS

Antes de escribir una línea de código: compruebe que un módulo no baste

Los módulos de PrestaShop y WooCommerce hacen íntegramente lo que describe esta página: transmiten los pedidos en el momento adecuado, colocan el script de los widgets en la plantilla, ponen las estrellas en las fichas de producto y el bloque de reseñas, y se ocupan de la firma de las peticiones. El comerciante no pega nada y no escribe nada.

Descargar los módulos →

Esta documentación se dirige por tanto a tres casos: una plataforma para la que aún no tenemos módulo, un desarrollo a medida, o la conexión de una herramienta de terceros (ERP, atención al cliente, asistente de IA) a las reseñas ya recogidas.

Direcciones base

Todas las URL de esta página son relativas a la dirección de la API. Un módulo solo debe conocer esa: las demás direcciones se le devuelven mediante GET /api/v1/cms/me, lo que le evita adivinarlas y nos permite cambiarlas sin actualizar nada en casa de los comerciantes.

UsoDirección
API (todos los canales)https://louis.guide
Área del comerciantehttps://louis.guide/app
Script de los widgetshttps://louis.guide/widget/v1/avis.js

Convenciones

  • Formato — JSON tanto de entrada como de salida, codificado en UTF-8. La cabecera Content-Type: application/json se espera en toda petición que lleve cuerpo.
  • Nomenclatura — serpiente en minúsculas (external_order_id, experienced_at), la convención dominante de las API que consumen los integradores de PHP y JavaScript.
  • Fechas — ISO 8601 con zona horaria explícita en la entrada (2026-08-01T14:22:00+02:00). En la salida, las fechas completas usan el mismo formato; las fechas públicas de una reseña se reducen al día (2026-08-01) porque ningún widget muestra la hora.
  • Importes — transmitidos como cadena ("129.90") y nunca como número en coma flotante: un céntimo perdido al redondear en un pedido se convierte en una diferencia de facturación.
  • Identificadores — los objetos que creamos llevan un UUID permanente. Los suyos (pedido, producto, variante) siguen siendo suyos: nunca los reescribimos.
  • Errores — siempre el mismo sobre { "error": { "code": …, "message": … } }. El code es estable y está destinado a su programa, el message al humano que depura. Véase §10.
  • Versionado — el /v1 de la ruta es un contrato. Puede añadirse un campo opcional en cualquier momento; ningún campo existente será renombrado, eliminado ni convertido en obligatorio. Una ruptura saldría en /v2, quedando la versión anterior aún servida — los módulos corren en casa de los comerciantes y nadie puede actualizarlos a distancia. Su código debe por tanto ignorar los campos que no conoce en lugar de fallar al verlos.

Obtener una clave de API

  1. Cree una cuenta de comerciante en el área del comerciante.
  2. Confirme la dirección de correo y abra a continuación la sección de claves de API.
  3. Anote el secreto: solo se muestra una vez. Una vez perdido no se recupera — se crea una clave nueva y se revoca la antigua.

Un módulo de instalación no necesita esta maniobra: abre él mismo una solicitud de conexión que el comerciante valida con un clic. Véase §3.

2. Autenticación

La API CMS y el servidor MCP usan el mismo mecanismo: una clave que dice quién llama, y una firma que prueba que quien llama posee el secreto. Son dos cosas distintas.

Método HTTPCabeceras exigidasPor qué
GET, HEAD X-Api-Key Una lectura no modifica nada: la clave basta para autorizarla.
POST, PUT, PATCH, DELETE X-Api-Key, X-Timestamp, X-Signature Una escritura compromete al comerciante: debe estar probada y no ser reproducible.

El secreto nunca circula

Solo viaja la firma. Eso cierra tres puertas que no requieren compromiso alguno de la tienda: la fuga pasiva del secreto en los registros de un intermediario, la reproducción de una petición interceptada y la alteración del cuerpo en tránsito. En cambio no protege de una tienda cuya base de datos haya sido sustraída — contra ese caso la defensa es la rotación de claves. No ponga nunca el secreto en una URL: las URL acaban en los registros de todos los intermediarios atravesados.

2.1 La firma, paso a paso

Paso 1 — Las tres cabeceras

CabeceraContenido
X-Api-Key Identificador público de la clave, tal como aparece en el área del comerciante.
X-Timestamp Marca de tiempo Unix en segundos, solo cifras. Nada de milisegundos, nada de fecha ISO.
X-Signature El prefijo literal sha256= seguido del HMAC-SHA256 en hexadecimal minúsculo. El prefijo forma parte del valor comparado: omitirlo produce un rechazo.

Paso 2 — Construir la carga que se va a firmar

Cuatro piezas concatenadas sin separador, exactamente en este orden:

charge = X-Timestamp
       + MÉTHODE HTTP en majuscules
       + chemin logique de la requête
       + corps brut de la requête
PiezaRegla exacta
Marca de tiempo La cadena idéntica a la enviada en X-Timestamp.
Método POST, PUT… siempre en mayúsculas.
Ruta La ruta sin esquema, sin host, sin cadena de consulta, empezando por / — por ejemplo /api/v1/cms/orders. Si la API se sirve desde un subdirectorio, ese prefijo de instalación no entra en la firma: vive en la dirección base, no en la ruta lógica.
Cuerpo La cadena de bytes exactamente tal como se envía. Serialice una vez, firme esa cadena, envíe esa cadena. Cuerpo vacío → cadena vacía.

Paso 3 — Calcular

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

Paso 4 — Comprobar su implementación con este ejemplo

Estos valores son fijos y la firma mostrada es realmente la de estos datos: si su código produce otra cosa, el problema está en su código, no en el nuestro.

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 !"}

Carga que se va a firmar (una sola línea, sin espacios añadidos):

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

Resultado esperado:

X-Signature: sha256=8a9c0de10516226f965465704902e00c666892b483b45b361f4536a6b2ee36e9

El mismo cálculo en una línea de shell:

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

printf y no echo: este último añade un salto de línea final, que cambia la firma.

Paso 5 — Una llamada completa con curl

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

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

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

--data-raw y no --data: el segundo interpreta ciertos caracteres y puede modificar el cuerpo enviado, invalidando así la firma.

2.2 Ejemplo en PHP

El cliente mínimo, sin dependencias. Es el mismo mecanismo que el de los módulos de PrestaShop y WooCommerce, reducido a lo esencial.

<?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 Las cuatro trampas

Cuatro causas explican la práctica totalidad de los signature_mismatch. Vistas desde fuera se parecen todas — de ahí el interés de descartarlas en este orden.

  1. El cuerpo se recodificó después de la firma. El caso más frecuente y el más difícil de ver: un array serializado dos veces da dos cadenas distintas en cuanto contiene un acento o una barra (JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES, orden de las claves). Firme la cadena, envíe esa cadena, no la reconstruya jamás.
  2. La ruta firmada lleva un prefijo que no debería tener. La ruta firmada es /api/v1/cms/orders, aunque la API se sirva desde https://ejemplo.es/plataforma/api/v1/cms/orders. El prefijo de instalación pertenece a la dirección base. A la inversa, tampoco firme la URL completa con su esquema y su host.
  3. El reloj del servidor se ha desviado. Tolerancia: 300 segundos de desfase, en un sentido como en otro. Más allá, la respuesta es timestamp_out_of_range y su mensaje indica el desfase medido en segundos — exactamente la información que hay que dar a su alojador. Este caso se manifiesta a menudo como una integración que "ayer funcionaba".
  4. Faltan el método o el prefijo. El método entra en la carga en mayúsculas, y el valor de X-Signature empieza por sha256=. Un HMAC desnudo, sin prefijo, se rechaza.

Lo que la cadena de consulta no hace

Los parámetros de URL (?page=2) no entran en la carga firmada: solo figura la ruta. En la práctica carecen de consecuencia, ya que los endpoints firmados son todos escrituras que llevan sus parámetros en el cuerpo — pero una implementación que los añadiera a la carga fallaría.

Reproducción y ventana de validez

La carga firmada cubre la marca de tiempo, el método, la ruta y el cuerpo. Omitir uno de ellos abriría una brecha: sin la ruta, una firma válida para POST /orders sería reproducible en DELETE /orders; sin la marca de tiempo, la petición sería reproducible indefinidamente.

La ventana de 300 segundos es lo que acota la reproducción: una petición interceptada no puede reemitirse más allá. No hay diccionario de firmas ya vistas — dentro de esa ventana una petición idéntica se acepta por tanto dos veces. Eso no afecta a la transmisión de pedidos, que es idempotente por external_order_id: la segunda recibe el pedido ya registrado y no envía un segundo correo.

Respuestas de autenticación

Todas estas respuestas llevan el estado 401.

CódigoCausaQué hacer
missing_api_key Falta la cabecera X-Api-Key. Añadir la cabecera.
invalid_api_key Clave desconocida, revocada o caducada. El mensaje es deliberadamente idéntico en los tres casos: distinguirlos permitiría comprobar en masa qué identificadores existen. Comprobar la clave en el área del comerciante, o crear una nueva.
missing_signature Escritura sin cabecera X-Signature. Firmar la petición (§2.1).
missing_timestamp Escritura sin cabecera X-Timestamp. Añadir la marca de tiempo y firmarla.
invalid_timestamp X-Timestamp no es una secuencia de cifras — milisegundos, fecha ISO o un signo. Enviar una marca de tiempo Unix en segundos.
timestamp_out_of_range Más de 300 segundos de desfase. El mensaje da la cifra exacta. Sincronizar el reloj del servidor (NTP).
signature_mismatch La firma no corresponde a la carga esperada. Repasar las cuatro trampas del §2.3, por orden.
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "timestamp_out_of_range",
    "message": "Marca de tiempo fuera de tolerancia: +412 s de desfase con nuestro servidor (máximo 300 s). El reloj de su servidor está probablemente desincronizado."
  }
}

3. Conectar una tienda

Estos dos endpoints permiten a un módulo recuperar una clave sin que el comerciante tenga que copiar nada. El módulo abre una solicitud, muestra un enlace, el comerciante valida en su navegador, y el módulo recibe su clave y su secreto en la lectura siguiente.

Van sin autenticación, por construcción. La seguridad no descansa en una identidad sino en tres cosas: la solicitud no obtiene nada mientras un comerciante conectado no la haya validado, el token de sondeo nunca sale del servidor de la tienda, y el secreto se entrega una sola vez. Lo peor que puede producir una llamada malintencionada es una solicitud pendiente que nadie aprobará — y que caduca en un cuarto de hora.

POST /api/v1/pairing Sin autenticación

Abre una solicitud de conexión y devuelve el enlace de validación que hay que presentar al comerciante.

CampoTipoObligatorioDescripción
shop_domaincadenasí Dominio de la tienda, p. ej. tienda.ejemplo.es.
platformcadenano prestashop, woocommerce, custom… unknown por defecto.
shop_namecadenano Nombre legible de la tienda, reutilizado al crear la cuenta.
platform_versioncadenano Versión de la plataforma, p. ej. 8.1.6.
plugin_versioncadenano Versión del módulo que llama.
shop_uidcadenano Identificador único generado una vez al instalar el módulo. Muy recomendable con varias tiendas: véase §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 — para abrir en una pestaña nueva del navegador del comerciante, fuera de su back office. Ahí es donde inicia sesión o crea su cuenta, y luego valida.
  • poll_token — para conservar únicamente del lado del servidor. No debe aparecer nunca en una página ni en una URL: es lo que permitirá recuperar el secreto.
  • code — mostrable al comerciante, para que compruebe que valida la solicitud correcta.

Códigos de error

EstadoCódigoCausa
422invalid_requestshop_domain ausente o inutilizable.
429rate_limitedMás de 10 aperturas por hora y por IP.
POST /api/v1/pairing/{code} Sin autenticación

Consulta el estado de la solicitud y entrega la clave una vez — y una sola — que el comerciante ha validado.

En POST aunque sea una lectura, porque la llamada tiene un efecto secundario: consume el secreto. En GET, un precargador del navegador o un antivirus que siga los enlaces lo consumiría en lugar del módulo.

CampoTipoObligatorioDescripción
poll_tokencadenasí El token recibido al abrir la solicitud.
curl -X POST 'https://louis.guide/api/v1/pairing/4K7M-9QR3' \
  -H 'Content-Type: application/json' \
  --data-raw '{"poll_token":"pt_8f2c1a7e5b2d48a6c3d9e0f1a2b3c4d5"}'

A la espera de validación:

HTTP/1.1 200 OK

{ "status": "pending" }

Validado — las credenciales se entregan únicamente en esta llamada:

HTTP/1.1 200 OK

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

Valores posibles de status

ValorSignificadoQué hacer
pendingEl comerciante aún no ha decidido.Seguir sondeando.
approvedValidado. La respuesta lleva las credenciales.Guardarlas, dejar de sondear.
rejectedEl comerciante ha rechazado.Detenerse y comunicárselo.
expiredQuince minutos transcurridos sin decisión.Abrir una nueva solicitud.
consumed El secreto ya se ha entregado, y eso nunca ocurre dos veces. El módulo ha perdido la respuesta. Volver a empezar una conexión — es el comportamiento seguro.
unknown Código desconocido o token erróneo. Deliberadamente indistintos: separarlos convertiría este endpoint en un oráculo capaz de revelar qué tiendas se están conectando. Comprobar la pareja código / token.
rate_limitedDemasiados sondeos (estado HTTP 429).Espaciar las llamadas.

Guarde el secreto de inmediato. Solo se transmite en esa respuesta. Un módulo que no consiga persistirlo tendrá que hacer que el comerciante repita toda la conexión.

Siempre 200, incluso para un estado de espera. El módulo consulta en bucle: un código HTTP de error ante una situación perfectamente normal haría saltar alertas para nada. Sondee cada cinco segundos; el límite es de 240 llamadas por cuarto de hora y por IP — más allá, la respuesta es { "status": "rate_limited" } con un estado 429.

4. API CMS (firmada)

El canal de las integraciones de servidor: módulos de comercio electrónico, ERP, CRM, herramientas internas. Todas las URL van precedidas de https://louis.guide.

Lecturas: basta la clave. Escrituras: clave + firma. El detalle del cálculo está en §2. Las fichas siguientes recuerdan el régimen de cada una con una insignia.

Lo que la API no permite, y no permitirá

Ningún endpoint modifica ni elimina una reseña. El comerciante puede responder públicamente y denunciar para moderación, nada más — exactamente lo que permite su área. Una API más permisiva que la interfaz sería una puerta trasera en el cumplimiento normativo, y es lo primero que verifica una auditoría.

GET /api/v1/cms/ping Clave de API

Comprueba que una clave funciona. Es la primera llamada que hay que escribir, y la que conviene ofrecer al comerciante como botón «Probar la conexión»: más vale que descubra una clave errónea en la configuración que en el primer pedido no transmitido.

Sin firma, deliberadamente. Una lectura no modifica nada y, sobre todo: este endpoint debe seguir siendo utilizable para probar que una clave es buena mientras la implementación HMAC sigue siendo errónea. El ping pasa, la escritura no: el problema está en la firma, no en la clave.

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 se devuelve por una razón precisa: compárelo con el reloj de su servidor. Un desfase superior a 300 segundos hará fallar todas sus escrituras firmadas (§2.3), y es aquí donde se ve antes de perder un día con ello.

GET /api/v1/cms/me Clave de API

Estado de la cuenta: identidad del comerciante, capacidades del plan, cuota y direcciones de la plataforma.

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/es/m/tissufiesta"
  },
  "server_time": "2026-08-13T14:52:07+00:00"
}

Lea las capacidades, no el nombre del plan

El bloque plan expone capacidades (can_…) además del código de plan. Compruebe las primeras: un módulo que codifica en duro if (plan === 'pro') dejará de ser correcto el día en que se añada un plan o cambie de nombre, en todos los comerciantes a la vez y sin que ninguno pueda corregirlo.

CapacidadLo que gobierna
can_display_product_reviews Visualización de las reseñas de producto — las estrellas en las fichas.
can_use_photos Salida de las fotos de clientes por la API. Se recogen ya desde el plan gratuito pero solo se sirven pagando: en una cuenta gratuita la galería responde con una lista vacía, nunca con un error.
can_use_reviews_api Lectura de las reseñas por la API, respuesta a las reseñas y acceso MCP. La transmisión de pedidos no está afectada: va incluida en todos los planes.
can_remove_branding Retirada de la mención de la plataforma en widgets y correos.

El bloque urls evita adivinar

Su integración solo debe conocer una única dirección: la de la API. Las demás — área del comerciante, script de los widgets, página pública del comerciante — se devuelven aquí. Un módulo que las recompone a partir de una base única supone que todo vive en el mismo host, lo que deja de ser cierto en cuanto un canal pasa a un subdominio, y produce enlaces muertos en todos los comerciantes ya instalados.

quota.remaining merece un lugar en su interfaz: a cero, los pedidos siguen aceptándose pero ya no sale ninguna solicitud. Avisar al 90 % de consumo evita al comerciante descubrirlo en sus estadísticas.

POST /api/v1/cms/orders Firma exigida

El endpoint central. Registra un pedido y planifica la petición de reseña. Todo lo demás en la plataforma se deriva de esta llamada: sin ella no hay solicitud, ni reseña, ni puntuación.

Idempotente por external_order_id

Reemitir la misma referencia devuelve el pedido ya registrado con un estado 200 en lugar de 201, sin crear duplicado y sin enviar un segundo correo al cliente. El campo idempotent de la respuesta vale entonces true. Puede por tanto reintentar sin precaución tras un corte de red o un tiempo de espera agotado — es el comportamiento preferible a cualquier lógica de deduplicación casera.

Cuándo llamar

En el momento en que la experiencia se vive, no se pide: en la entrega, en el envío según su oficio, o al pasar al estado que haga las veces. Es experienced_at el que lleva esa fecha, y es ella la que hace arrancar el plazo de solicitud.

Cuerpo de la petición

Raíz

CampoTipoOblig.Descripción
external_order_idcadena (100)sí Referencia del pedido en su sistema. Clave de idempotencia y prueba de compra conservada cinco años (AFNOR §6.3). Debe ser estable en el tiempo.
customerobjetosí Identidad del cliente al que se va a solicitar — véase la tabla siguiente.
experienced_atISO 8601sí Fecha de entrega o de consumo, con zona horaria explícita. Véase el recuadro de abajo: no es la fecha del pedido.
sourceobjetosí Contexto técnico de la emisión — véase más abajo.
itemsarray (200 máx.)no Artículos. Sin ellos no se pedirá ninguna reseña de producto — solo la del establecimiento.
amountcadena decimalno Importe total, p. ej. "129.90". Nunca un número en coma flotante.
currencyISO 4217no "EUR", "CHF"…
channelenumeraciónno ecommerce_order (por defecto), pos_transaction, qr_scan, manual, csv_import, nfc.
location_idcadena (100)no Establecimiento afectado, tal como lo ha declarado el comerciante. Un valor desconocido hace fallar la petición con un 422 en lugar de vincular el pedido al punto de venta equivocado.
solicitation_delay_daysentero 0–365no Plazo propio de este pedido, que anula el ajuste de la cuenta. Útil cuando un mismo vendedor envía un ramo sobre el que preguntar mañana y un colchón sobre el que preguntar dentro de un mes. Fuera de límites, el valor se ignora y se aplica el ajuste de la cuenta — un valor aberrante no debe hacer perder un pedido.
order_status_idcadena (20)no Estado del pedido en su sistema en el momento del envío. Puramente diagnóstico — no lo interpretamos — pero es la única información que permite responder a «por qué este pedido no ha desencadenado nada».
order_status_labelcadena (120)no Etiqueta legible de ese estado.

experienced_at: la fecha de entrega, no la del pedido

Un paquete pedido el día 1 y entregado el 6 lleva el 6. No es una sutileza: dos ensayos aleatorizados sobre más de 300 000 consumidores (Jung, Ryu, Han & Cho, Journal of Marketing, 2023) establecen que una solicitud enviada antes de que el cliente haya podido formarse una opinión tiene un efecto negativo sobre la tasa de envío. Anclar el plazo en la fecha del pedido equivale a solicitar sistemáticamente demasiado pronto, todo el plazo de entrega.

Es además una de las tres fechas mostradas públicamente junto a la reseña (AFNOR §6.3).

customer

CampoTipoOblig.Descripción
emailcorreo (255)sí El único dato personal en claro que aceptamos. Borrado tras el plazo de envío; solo subsiste su huella.
countryISO 3166-1 alpha-2no* *Muy recomendable. Google calcula sus puntuaciones de comerciante por país y descarta las reseñas cuyo país sea desconocido. Esta información solo existe en el momento del pedido: una vez depurada la dirección, es definitivamente irrecuperable, sin posibilidad de rescate.
localefr, en, nl, de, it, esno Idioma del correo de solicitud. En su defecto, el idioma por defecto del comerciante — solicitar a un cliente neerlandófono en francés hunde la tasa de respuesta.
phonecadena (32)no Móvil para la solicitud por SMS. Formato internacional (+33612345678) muy recomendable: es el único sin ambigüedad. Un número nacional se convierte a partir de country; sin país conocido se descarta sin hacer fallar el pedido. Véase la advertencia de abajo.
first_name, last_namecadena (100)no Personalización de la solicitud y nombre mostrado del autor.
companycadena (255)no Razón social, para un pedido profesional.
postal_code, citycadenano Depurados al mismo tiempo que la dirección de correo.

Envíe el número de móvil solo si el comerciante ha contratado el SMS. Sin esa opción se recibe y se conserva sin que salga mensaje alguno: un dato personal recogido sin finalidad, algo que ninguna de las dos partes puede justificar en caso de inspección.

source — obligatorio

Este bloque no es estadística. Cuando un comerciante escribe «mis reseñas ya no salen desde la actualización», la respuesta ya está dentro: versión de la plataforma, versión del módulo, evento desencadenante. Hacerlo opcional equivaldría a no tenerlo nunca — los integradores rellenan lo que se exige, no lo que se sugiere.

CampoTipoOblig.Descripción
platformcadena (50)sí prestashop, woocommerce, shopify, magento, custom…
platform_versioncadena (30)no P. ej. 8.1.6.
plugin_versioncadena (30)no Versión de su integración. Hay que incrementarla en cada entrega.
triggercadena (100)no Evento en el origen del envío, p. ej. woocommerce_order_status_completed. Permite comprender por qué un pedido sale demasiado pronto o demasiado tarde.
shop_uidcadena (80)no* *Decisivo con varias tiendas. Identificador generado una vez en la instalación y conservado. Véase el recuadro.
shop_idcadena (50)no Identificador de tienda en la plataforma. Sirve de reserva cuando falta shop_uid.
shop_namecadena (255)no Nombre legible de esta tienda. Sin él, el comerciante descubre en su área un establecimiento llamado «3» y debe adivinar cuál es.
shop_group_id, lang_idcadenano Conservados para el diagnóstico, nunca interpretados. lang_id no separa nada: el idioma de la reseña viene de customer.locale.

Varias tiendas: shop_id no basta

Vale «1» en toda instalación de tienda única. Un comerciante que explota dos sitios bajo la misma cuenta — una marca por dominio, caso corriente — enviaría por tanto «1» desde ambos: las dos tiendas se fundirían en un solo establecimiento, las reseñas de una aparecerían en la página de la otra, y el nombre conservado sería el del último pedido recibido. Defecto constatado en pruebas sobre dos PrestaShop reales.

shop_uid resuelve el problema: genérelo una vez en la instalación y consérvelo. Sobrevive a un cambio de dominio igual que a una renovación de clave — los otros dos discriminantes en los que se piensa primero, y que se mueven ambos.

items[] — opcional, 200 artículos como máximo

CampoTipoOblig.Descripción
external_product_idcadena (100)sí Identificador del producto en su catálogo.
namecadena (255)sí Nombre del producto tal como se muestra al cliente.
variant_idcadena (100)no* *El campo más importante de esta lista. Sin él, la silla roja y la silla azul comparten la misma clave de producto: sus reseñas se mezclan y «se rompió la pata» ya no designa nada. Corresponde a id_product_attribute (PrestaShop), a la variación (WooCommerce), al variant (Shopify).
variant_labelcadena (255)no Etiqueta legible: «Color: rojo, Talla: L».
gtinde 8 a 14 cifrasno* EAN-13 o UPC-A convertido. Clave de agregación entre comerciantes, y exigencia de Google para mostrar las estrellas en sus resultados.
upc, isbn, mpncadenano Mantenidos aparte del GTIN porque los catálogos los mantienen en columnas distintas. El ISBN es decisivo en el libro, donde el GTIN suele estar vacío.
sku, brandcadenano Referencia interna y marca.
category_id, category_namecadenano Categoría principal en su catálogo.
product_url, image_urlURL (500)no Usadas en el correo de solicitud: una imagen de producto mejora netamente la tasa de envío.
imageslista de URL (10 máx.)no Imágenes adicionales.
descriptioncadena (5000)no Recibida, nunca mostrada de nuevo tal cual: es su texto, no el del autor de la reseña. Sirve para situar el producto en moderación.
tagslista (30 máx.)no Palabras clave del producto, 60 caracteres cada una.
meta_title, meta_descriptioncadenano Metadatos de la ficha.
quantityentero > 0no 1 por defecto.
unit_pricecadena decimalno P. ej. "19.90". Como cadena, igual que todos los importes.

Ejemplo completo

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

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

Pedido registrado:

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
}

Misma petición reproducida:

HTTP/1.1 200 OK

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

La dirección de correo nunca se devuelve, aunque acabe de transmitirla: todo dato devuelto es un dato que puede filtrarse en sus propios registros.

Valores posibles de status

ValorSignificado
pendingRecibido, a la espera de planificación.
scheduledSolicitud programada.
solicitedPetición de reseña enviada al cliente.
reviewedEl cliente ha enviado su reseña.
cancelledAnulado antes del envío.
expiredPlazo de envío transcurrido sin reseña.

Errores

Este endpoint es el único servido por API Platform: sus errores de validación llegan por tanto en forma de lista de violations, y no en el sobre { "error": … } del resto de la API. Su código debe aceptar ambas formas.

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."
    }
  ]
}
EstadoCausaQué hacer
401 Clave ausente, no válida, o firma rechazada. Véase §2.
422 Falta un campo o está mal formado — véase violations. Corregir el campo designado por propertyPath.
422 experienced_at está en el futuro (más allá de un día de margen). Comprobar la zona horaria del servidor: casi siempre es de ahí de donde viene el desfase.
422 experienced_at se remonta a más de 90 días. Véase el recuadro de abajo. Para retomar un histórico, póngase en contacto con el soporte.
422 Ningún establecimiento corresponde a location_id. Crear el establecimiento en el área del comerciante, u omitir el campo.
415 Cabecera Content-Type ausente o inesperada. Enviar Content-Type: application/json.

Por qué se rechazan los pedidos de más de 90 días

El escenario de siniestro es conocido: se instala un módulo y empuja tres años de histórico de golpe. Miles de invitaciones salen hacia direcciones caducadas, la tasa de rebote se dispara — y como todos los correos salen de nuestro dominio, es la entregabilidad de todos los comerciantes la que se hunde, no solo la del recién llegado.

El rechazo se pronuncia en la entrada, con un mensaje explícito, en lugar de en la planificación: el integrador lo entiende de inmediato en lugar de ver sus pedidos desaparecer en silencio.

GET /api/v1/cms/reviews Clave de API Plan de pago

Lista las reseñas del comerciante, de la más reciente a la más antigua, con la respuesta publicada y la eventual denuncia de cada una. Es este endpoint el que permite volcar las reseñas en un ERP, un CRM o una herramienta de atención al cliente.

ParámetroPor defectoDescripción
typemerchant merchant para las reseñas del establecimiento, product para las reseñas de producto.
statustodos published, pending, awaiting_email, rejected, disputed, withdrawn. Un valor desconocido se ignora — el filtro entonces no se aplica, en lugar de devolver un error.
page1Número de página.
per_page25 De 1 a 100. Por encima, el valor se reduce a 100.
curl 'https://louis.guide/api/v1/cms/reviews?status=published&per_page=50' \
  -H 'X-Api-Key: ak_live_5c2f81b0'
HTTP/1.1 200 OK

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

Las tres fechas, y por qué son tres

experienced_at (la experiencia vivida), submitted_at (el envío) y published_at (la puesta en línea) son tres cosas distintas, y la AFNOR obliga a poder distinguirlas. Un integrador que las confunde muestra «hace 3 días» sobre una experiencia de hace tres semanas. published_at vale null mientras la reseña no esté publicada.

order_reference recoge su external_order_id: es lo que vincula la reseña al pedido en su sistema. Vale null para una reseña enviada fuera de una solicitud.

Sincronización incremental

Consulte con status=published y compare published_at con la última pasada: volcar todo el histórico en cada ejecución funciona los primeros meses, y luego se convierte en una consulta de varios miles de filas cada hora. La paginación empieza en 1 y el campo total da el número de reseñas que corresponden al filtro, no el número de páginas.

EstadoCódigoCausa
402plan_required El plan del comerciante no incluye la API de reseñas. Las reseñas siguen siendo legibles sin clave mediante la API pública — que no es lo mismo: esa sirve la visualización pública, no la exportación.
POST /api/v1/cms/reviews/{uuid}/response Firma exigida Plan de pago

Publica una respuesta pública a una reseña del establecimiento, o actualiza la que ya existe. Una reseña solo lleva una respuesta: reemitir sustituye el texto.

CampoTipoOblig.Descripción
contentcadenasí Texto de la respuesta. Truncado a 3000 caracteres sin error — compruebe la longitud por su parte si el corte le molesta.
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
  }
}

Lea published antes de anunciar nada

El comerciante ajusta en su área si las respuestas escritas por un programa salen directamente o esperan su relectura. Ese ajuste vive en la cuenta y no es un parámetro de la petición: si quien llama pudiera elegir por sí mismo si debe ser releído, la garantía no valdría nada.

Consecuencia para su interfaz: una respuesta aceptada no es necesariamente visible. published: false significa «guardada como borrador, pendiente de validar en el área del comerciante» — dígalo, en lugar de mostrar un «publicado» que la página pública desmentirá.

201 en la creación, 200 en la actualización; el campo created recoge la misma información en el cuerpo. Una respuesta ya publicada sigue siéndolo: una actualización nunca la devuelve a borrador, lo que la haría desaparecer de la página sin que nadie lo haya decidido.

EstadoCódigoCausa
402plan_requiredPlan sin respuesta a las reseñas.
404review_not_found Identificador desconocido, mal formado, o perteneciente a otro comerciante — los tres casos son indistinguibles, y lo impone la compartimentación.
422content_requiredcontent ausente o vacío.
POST /api/v1/cms/reviews/{uuid}/report Firma exigida

Denuncia una reseña para moderación. La reseña pasa al estado «impugnada» y el expediente entra en la cola de instrucción.

CampoTipoOblig.Descripción
reasonenumeraciónsí Motivo, que hay que elegir en la lista siguiente.
detailcadenano Precisiones para el moderador, truncadas a 1000 caracteres. Aquí es donde se escribe «pedido n.º X, jamás entregado en esa dirección» — una denuncia motivada se instruye más deprisa.

Motivos admisibles

ValorCuándo invocarlo
inappropriate_contentInsulto, discurso de odio, contenido ilícito.
spam_or_advertisingPublicidad, enlace comercial, contenido automatizado.
off_topicSin relación con la experiencia vivida — el transportista, el tiempo.
conflict_of_interestCompetidor, antiguo empleado, reseña remunerada.
personal_data_disclosureLa reseña expone datos personales.

Una puntuación baja no es un motivo

Ningún motivo permite impugnar una reseña por su puntuación, y no es un olvido: es esa prohibición la que marca la diferencia entre una plataforma de reseñas y un escaparate. Una denuncia mal motivada se rechaza, y la reseña sigue en línea.

HTTP/1.1 202 Accepted

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

review_remains_visible vale siempre true, y el campo existe para que ninguna interfaz se diseñe suponiendo lo contrario: la reseña sigue siendo pública durante toda la instrucción. Retirarla ante una simple denuncia equivaldría a dejar que el comerciante devalúe lo que le desagrada — Google lo prohíbe explícitamente, la AFNOR también. La respuesta es un 202: la solicitud queda registrada, no resuelta.

EstadoCódigoCausa
404review_not_foundIdentificador desconocido, mal formado, o fuera de su cuenta.
409already_reportedYa hay una denuncia abierta sobre esta reseña.
422invalid_reason Motivo ausente o fuera de lista. El mensaje recuerda los valores admitidos.

5. API pública

Solo lectura, sin autenticación, en /api/v1/public/. Es lo que consumen los widgets, y lo que puede consumir cualquier visualización a medida.

El {slug} de las rutas es el identificador público del comerciante — el de su página pública, visible en urls.profile que devuelve /cms/me.

Lo que protege una API sin clave

No hay identidad que comprobar: este código se ejecuta en casa de los visitantes de una tienda, ningún secreto puede vivir ahí. La protección descansa por tanto en otras tres cosas, que hay que conocer antes de integrar.

  • De aquí no sale ningún dato sensible. Ni correo, ni huella de correo, ni referencia de pedido, ni identificador interno. Un widget muestra reseñas públicas; todo lo que sale por este canal es legible por cualquiera.
  • Caudal limitado a 60 peticiones por minuto y por dirección IP, en ventana deslizante. Una ficha de producto hace dos llamadas: eso deja 30 cargas por minuto desde una misma dirección — amplio para un visitante, estrecho para un aspirador de contenidos. Véase §9.
  • CORS restringido a los dominios declarados del comerciante. Un comodín * autorizaría a cualquier sitio — competidor, comparador, falsificador — a mostrar las reseñas de cualquier comerciante como si fueran suyas.

CORS: qué hay que declarar para que el navegador acepte la respuesta

La cabecera Access-Control-Allow-Origin solo se pone si el origen que llama corresponde a un dominio conectado al comerciante indicado en la URL. Los subdominios se aceptan: un dominio declarado como ejemplo.es autoriza www.ejemplo.es y tienda.ejemplo.es.

Síntoma típico de un dominio no declarado: la petición sale, el servidor responde 200, y el navegador bloquea la lectura en la consola. El remedio está en el área del comerciante, no en el código.

Una llamada de servidor a servidor no está afectada: sin cabecera Origin, no hay control CORS. Ese caso lo cubre la limitación de caudal. El CORS protege al navegador de otro sitio, nunca al dato en sí.

Caché

Todas las respuestas son públicas y se guardan en caché: 60 segundos para las reseñas y las puntuaciones, 300 segundos para los ajustes de visualización. Es lo que permite absorber el tráfico de una tienda en promoción sin dimensionar para el pico. No construya una visualización que suponga la aparición instantánea de una reseña publicada.

GET /api/v1/public/merchants/{slug}/score Sin autenticación

Puntuación global de la tienda y reparto por puntuación.

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 se devuelve explícitamente en lugar de sobreentenderse: un integrador que programa «sobre 10» porque su anterior proveedor lo era produce una visualización falsa que nadie relee. count solo cuenta las reseñas públicamente visibles.

404 merchant_not_found si el slug es desconocido.

GET /api/v1/public/merchants/{slug}/reviews Sin autenticación

Reseñas sobre el establecimiento, de la más reciente a la más antigua.

ParámetroPor defectoDescripción
page1Número de página.
per_page10 De 1 a 50. Por encima, se reduce a 50.
HTTP/1.1 200 OK

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

El orden cronológico está impuesto, no elegido

No hay parámetro de ordenación, y no lo habrá: la AFNOR (§6.3) exige el orden cronológico inverso como visualización por defecto. Proponer «los mejor puntuados primero» como ordenación inicial sería una presentación sesgada. Una ordenación en JavaScript sobre la página recibida es responsabilidad suya, no nuestra.

Lo que su visualización debe recoger

  • Dos fechas como mínimo — la de la experiencia y la de la publicación. Es una obligación de visualización, y solo la API puede facilitárselas. Las fechas públicas se reducen al día (2026-08-06): ningún widget muestra la hora.
  • verified_purchase — la reseña está vinculada a un pedido real. Es lo que distingue una reseña recogida de una publicada espontáneamente.
  • disputed — la reseña está impugnada y su instrucción está en curso. Sigue mostrándose (véase §4.6); señálela en lugar de ocultarla.
  • reply — la respuesta del comerciante forma parte de la reseña para el lector. published_at lleva ahí la fecha de última modificación cuando la ha habido: mostrar la fecha original bajo un texto reescrito induciría a error.
  • photos — vacío en un comerciante cuyo plan no las sirve. Las reseñas siguen completas, solo faltan las imágenes.

En este canal no hay campo total: una página vacía significa que no queda nada por cargar. Es lo que hace el botón «ver más» del widget.

GET /api/v1/public/products/{slug}/{productId}/score Sin autenticación

Puntuación de un producto. {productId} es su identificador de catálogo, el transmitido en external_product_id — nunca lo reescribimos. Acuérdese de codificarlo si contiene caracteres reservados.

ParámetroPor defectoDescripción
variant— Restringe la puntuación a una variante. Si falta, la puntuación abarca todas las variantes juntas — que es el comportamiento correcto mientras el visitante no haya elegido su talla.
with_variantsfalse Añade el detalle por variante. Cuesta una consulta más: no lo active en la ficha de producto, que es la página más vista de la tienda.
curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/score?with_variants=true'
HTTP/1.1 200 OK

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

variants vale null cuando no se pide with_variants — es una ausencia de cálculo, no una ausencia de variantes.

GET /api/v1/public/products/{slug}/{productId}/reviews Sin autenticación

Reseñas de un producto. Misma estructura de respuesta y mismos parámetros de paginación que las reseñas del establecimiento, más el filtro variant — útil cuando un selector de talla quiere mostrar solo las reseñas de la variante elegida.

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

En un comerciante cuyo plan no incluya la visualización de reseñas de producto, prevea una visualización que se reduzca limpiamente en lugar de un marco vacío: el widget, por su parte, desaparece de la página.

GET /api/v1/public/merchants/{slug}/photos Sin autenticación

Fotos de clientes aprobadas, sin el texto de las reseñas. Es lo que alimenta un carrusel: sin este endpoint habría que cargar cincuenta reseñas completas — texto, fechas, puntuaciones — para quedarse solo con las imágenes, en una ficha de producto que ya carga la plantilla del comerciante.

ParámetroPor defectoDescripción
produit— Restringe a un producto. Si falta, devuelve las fotos de toda la tienda — lo que alimenta un carrusel de página de inicio.
limite24 De 1 a 50.
HTTP/1.1 200 OK

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

Las URL son absolutas: este JSON lo lee JavaScript ejecutado en el dominio de la tienda, donde una URL relativa apuntaría a la tienda misma. Sírvase de width y height para reservar el espacio antes de cargar — de lo contrario la ficha de producto saltará ante los ojos del visitante.

En un comerciante cuyo plan no sirve las fotos, la respuesta es { "photos": [] } con estado 200, nunca un error: el carrusel desaparece limpiamente en lugar de mostrar un marco fallido.

GET /api/v1/public/merchants/{slug}/display Sin autenticación

Ajustes de visualización decididos por el comerciante en su área. Es lo que permite colocar las etiquetas de una vez por todas en una plantilla y luego encender, apagar o mover un elemento sin tocar el código de la tienda.

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/es/m/tissufiesta"
}
AjustePor defectoSignificado
badge_flottantfalse Insignia de puntuación fijada en una esquina de la pantalla. Apagada por defecto: se superpone a la página del comerciante, y nada debe aparecer en su sitio sin que lo haya pedido.
badge_cotedroitedroite o gauche.
badge_decalage16Separación en píxeles respecto al borde.
seuil_avis1 Número de reseñas por debajo del cual la visualización desaparece. Véase §6: mostrar «ninguna reseña» es peor que no mostrar nada.
etoiles_fichetrueEstrellas en la ficha de producto.
etoiles_vignettestrueEstrellas en las miniaturas de los listados.
onglet_avistruePestaña «Reseñas» de la ficha de producto.
bloc_accueiltrueBloque de reseñas en la página de inicio.

display está siempre completo, valores por defecto incluidos: su código no tiene que conocer nuestros valores por defecto ni copiarlos — el día en que uno cambie, los seguirá.

accent_color vale null cuando el comerciante no ha elegido color o cuando su plan ya no lo permite. Prevea siempre un color de reserva por su parte: es lo que hace el widget, cuyo tono solo existe si está declarado.

GET /api/v1/public/health Sin autenticación

Disponibilidad del servicio. Para consultarlo con una sonda o con el control de salud de un módulo.

HTTP/1.1 200 OK

{ "status": "ok" }

Deliberadamente mínimo: ningún acceso a base de datos, ninguna dependencia externa. Una lentitud de la base no debe disparar una falsa alerta de indisponibilidad — y a la inversa, este endpoint no dice nada del estado de la base. Para comprobar que una clave funciona, hay que llamar a /cms/ping.

6. Widgets de visualización

Cuatro elementos HTML que colocar en una plantilla. Un solo script que cargar, ninguna dependencia, ninguna configuración: la dirección de la API se deduce de la URL del propio script.

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

El script se carga una vez por página, donde se quiera; los elementos pueden colocarse antes que él. Se sirve bajo /widget/v1/: una evolución incompatible saldría en /v2/ y este seguiría sirviéndose tal cual — vive en plantillas que nadie va a actualizar.

Los cuatro elementos

ElementoLo que muestraDónde colocarlo
<avis-score> Puntuación media, estrellas, número de reseñas. Ficha de producto, cabecera de la tienda, página «quiénes somos».
<avis-liste> Reseñas paginadas, con fotos y respuestas del comerciante. Pestaña «Reseñas» de una ficha de producto, página dedicada.
<avis-carrousel> Solo las fotos de clientes, pulsables. Ficha de producto, página de inicio.
<avis-flottant> Insignia de puntuación fijada en una esquina, pulsable. La plantilla común, una sola vez para todo el sitio.

Atributos

AtributoElementosPor defectoFunción
marchandtodos— Obligatorio. Identificador público del comerciante, el de su página pública.
produit score, lista, carrusel— Su identificador de catálogo (external_product_id). Si falta, el elemento abarca toda la tienda.
languetodoslang de la página Idioma de las etiquetas. En su defecto, el atributo lang del documento — que la plantilla ya rellena — y después el francés. Solo fr y en están realmente traducidos; cualquier otro valor recae en el francés en lugar de mostrar etiquetas traducidas a medias.
miniscore1 Número de reseñas por debajo del cual el elemento desaparece. En 3, una ficha que solo tiene dos reseñas no muestra nada en lugar de una puntuación basada en casi nada.
par-pageliste5 Reseñas cargadas de una vez; un botón «Ver más» carga el resto.
maxcarrousel12 Número de fotos, 50 como máximo.

Ejemplo completo en una ficha de producto

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

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

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

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

Un widget que no muestra nada no está necesariamente averiado

Tres situaciones hacen desaparecer un elemento, y cada vez es intencionado: ninguna reseña (o menos que el umbral), ninguna foto para el carrusel, y cualquier error de red o de servidor.

Mostrar «Aún no hay reseñas» en una ficha de producto es peor que no mostrar nada: el visitante concluye que nadie ha comprado. Y una franja de error en la tienda de un comerciante porque nuestra API tose sería indefendible — el elemento se retira de la maquetación, la ficha de producto queda intacta.

Consecuencia práctica para el integrador: no construya una maquetación que reserve una altura fija para un widget. Puede no ocupar nada en absoluto.

Lo que el comerciante gobierna sin usted

Los elementos leen /display al cargarse. Dos ajustes vienen de ahí en lugar de un atributo, y es deliberado: el comerciante puede colocar sus etiquetas de una vez por todas y luego cambiar de opinión desde su área sin reabrir la plantilla.

  • La insignia flotante — encendida o apagada, a la derecha o a la izquierda, con su separación. <avis-flottant> colocado en la plantilla no muestra nada mientras el comerciante no la haya activado. Está apagada por defecto: nada debe aparecer en su sitio sin que lo haya pedido.
  • El color de marca — aplicado a las superficies que lo admiten. Las estrellas conservan su ámbar, igual que el verde de «Compra verificada» y el ámbar de «Impugnada»: esos colores portan un sentido, no decoran, y repintarlos haría ilegible la puntuación en un comerciante cuya marca sea amarillo pálido o blanca.

Los ajustes se piden una sola vez por página, incluso con cuatro elementos: la petición en curso se comparte. Eso es lo que evita que el widget sea el script que ralentiza la ficha de producto — reproche fundado que cabe hacer a la mayoría de los módulos de reseñas.

Aislamiento respecto a la plantilla

Cada elemento renderiza su contenido en un shadow DOM: el CSS de la plantilla no se desborda sobre el widget, y el del widget no se desborda sobre la tienda. Ninguno de los dos sería aceptable en el otro sentido.

Corolario que conviene conocer antes de intentarlo: sus reglas CSS no alcanzarán el interior de los widgets. La única personalización prevista es el color de marca, ajustado en el área del comerciante. Una visualización realmente a medida pasa por la API pública — que es exactamente para lo que está documentada.

Antes de pegar nada

Con PrestaShop y WooCommerce, el módulo coloca él mismo estas etiquetas, en el punto adecuado de la plantilla. El pegado manual se dirige a las demás plataformas y a las plantillas a medida — ver los módulos.

7. Servidor MCP

MCP (Model Context Protocol) expone las mismas capacidades que la API, en una forma que un asistente de IA puede descubrir por sí solo. Donde un desarrollador lee una documentación, escribe la autenticación e interpreta el JSON, el asistente pide la lista de herramientas, lee sus descripciones y las llama.

En concreto: el comerciante conecta su asistente a este servidor y luego escribe «¿qué reseñas no tienen respuesta todavía?» o «responde a esta disculpándote por el retraso». Nadie ha escrito código de integración.

Valor
Direcciónhttps://louis.guide/api/v1/mcp
TransporteJSON-RPC 2.0 sobre HTTP, en POST
Versión del protocolo2024-11-05
Servidor anunciadoavis-clients, versión 1.0.0
Capacidadestools — ni recursos, ni prompts
Autenticación Token portador (§7.1) o clave de API + firma HMAC (§7.2)
PlanDe pago — si no, error JSON-RPC -32001

7.1 Conectar un asistente: el token

Es la vía normal, y la única que no requiere nada instalado. El comerciante crea un token en su área — Ajustes · Recogida, sección «Conectar un asistente» — y luego lo pega en la configuración de su asistente junto con la dirección del servidor.

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

Forma habitual de los archivos de configuración de un cliente MCP:

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

Verificación en un solo comando, antes de conectar nada:

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"}'

Lo que el token puede y lo que no puede

PropiedadComportamiento
Alcance Únicamente /api/v1/mcp. Presentado en la API CMS, ni siquiera se examina: ni transmisión de pedidos, ni exportación de reseñas, ni conexión.
Escritura Prohibida por defecto. El comerciante marca explícitamente «autorizar la redacción de respuestas» al crearlo. Sin ella, la herramienta repondre_a_un_avis ni siquiera aparece en tools/list — el asistente no la propondrá por tanto.
Vida útil Un año, después deja de valer. Se vuelve a crear en diez segundos.
Revocación Inmediata y definitiva, token a token, sin tocar las claves de API ni los módulos del comerciante.
Conservación Mostrado una sola vez. Solo guardamos una huella: nadie puede volver a mostrarlo, nosotros incluidos.
Número Tres tokens válidos como máximo por cuenta.

Un token portador viaja: trátelo como una contraseña

A diferencia del secreto HMAC, sale en cada petición y vive en la configuración de un servicio que no controlamos. Es el precio de la conexión directa, y por eso está compartimentado, caduca, es revocable y va mudo en escritura por defecto. No lo ponga nunca en una URL ni en un repositorio de código: las URL acaban en los registros de todos los intermediarios atravesados.

Los intentos están acotados a 20 fallos por cuarto de hora y por dirección IP — más allá, la respuesta es un 429.

7.2 Alternativa: clave de API y firma HMAC

El mismo endpoint acepta la autenticación descrita en §2: clave de API y firma HMAC. Tiene una ventaja real — el secreto nunca sale del servidor del comerciante — y un inconveniente que la reserva a los integradores: ningún cliente MCP sabe recalcular un HMAC en cada llamada, solo ponen cabeceras fijas.

Hace falta por tanto un puente: un pequeño programa lanzado por el asistente, que recibe el JSON-RPC por su entrada estándar, lo firma, lo envía y devuelve la respuesta. Node.js 18 o más reciente, ninguna dependencia. Guárdelo como 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');
    }
  }
});

Declaración del lado del cliente MCP (forma habitual de los archivos de configuración):

{
  "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"
      }
    }
  }
}

El secreto no sale de la máquina. Sirve para firmar localmente; lo que sale a la red es la firma. Una ruta absoluta es indispensable: el asistente no lanza el programa desde la carpeta en la que usted lo escribió.

Para comprobar el puente antes de conectar nada, envíele una línea a mano. Debe volver una lista de herramientas:

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

7.3 Métodos

MétodoEfecto
initialize Anuncia la versión del protocolo, las capacidades y la identidad del servidor.
tools/listCatálogo de las herramientas y de sus esquemas de entrada.
tools/callEjecuta una herramienta — params.name y params.arguments.
notifications/initialized, pingConfirmados con un resultado vacío.
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 Las tres herramientas

Dos leen, una escribe. La línea divisoria no es técnica: leer reseñas no presenta riesgo alguno, mientras que publicar una respuesta hace hablar al comerciante en público en una página que alojamos nosotros — una formulación desafortunada en una reseña delicada, y hay una captura de pantalla circulando.

Por eso tools/list devuelve solo dos herramientas cuando quien llama presenta un token de solo lectura. No codifique por tanto la lista en duro: pídala, y anuncie al comerciante únicamente lo que contiene.

herramienta lister_avis

Reseñas publicadas sobre el establecimiento, de la más reciente a la más antigua. La herramienta que el asistente llama para «muéstrame los clientes descontentos» o «¿qué no tiene respuesta todavía?».

ArgumentoTipoPor defectoEfecto
note_maxentero 1–5— Solo devuelve las reseñas cuya puntuación sea menor o igual.
sans_reponsebooleanofalse Descarta las reseñas a las que ya se ha publicado una respuesta.
limiteentero 1–5020 Número de reseñas leídas.
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "lister_avis",
    "arguments": { "note_max": 3, "sans_reponse": true, "limite": 10 }
  }
}

El resultado es un bloque de texto que contiene JSON — es la forma que el protocolo prevé para un resultado estructurado, y la que los asistentes saben leer:

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

sans_reponse filtra después del límite, no antes. Pedir 20 reseñas sin respuesta lee las 20 últimas reseñas publicadas y luego retira las ya tratadas: el resultado puede contener muchas menos, y total lo dice. Suba limite para ensanchar la ventana de lectura.

Aquí solo salen las reseñas publicadas: ni las pendientes, ni las rechazadas, ni las retiradas. Para esas está GET /cms/reviews con su filtro status.

herramienta resume_reputation

Visión de conjunto, sin argumento. Lo que el asistente llama para «¿cómo va mi reputación?».

{
  "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 cuenta las reseñas sobre el establecimiento; avis_produit cuenta por separado las que se refieren a un artículo. Sumarlas daría un total que no corresponde a ninguna puntuación mostrada.

herramienta repondre_a_un_avis Escritura pública
ArgumentoTipoOblig.Efecto
avis_idcadenasíIdentificador de la reseña, tal como lo devuelve lister_avis.
contenucadenasí Texto de la respuesta, truncado a 3000 caracteres.
{
  "avis_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  "publiee": false,
  "message": "Respuesta guardada como borrador. El comerciante debe validarla en su área antes de que aparezca."
}

Publicada o en borrador: lo decidió el comerciante, no la llamada

El comerciante ajusta en su área si las respuestas redactadas por un asistente salen directamente o esperan su relectura. Ninguno de los dos comportamientos es correcto en absoluto: quien recibe dos reseñas por semana quiere releer, quien recibe doscientas quiere que salgan.

Ese ajuste no es un parámetro de la petición, y eso es esencial: si el asistente pudiera elegir por sí mismo si debe ser releído, la garantía no valdría nada. El campo publiee y el campo message dicen lo que realmente ha ocurrido — un asistente debe comunicarlo tal cual al comerciante.

Una reseña ya respondida ve su respuesta sustituida. Una respuesta ya pública sigue siéndolo: una reescritura nunca la devuelve a borrador, lo que la haría desaparecer de la página sin decisión de nadie.

Lo que un asistente debe saber antes de redactar

  • Responder en el idioma de la reseña — para eso está el campo langue. Una respuesta en francés bajo una reseña en neerlandés le dice al lector que no se ha leído.
  • No prometer nunca un gesto comercial que no se pueda cumplir: reembolso, reenvío, descuento. Esta respuesta es pública y oponible al comerciante.
  • Ninguna herramienta modifica ni elimina una reseña, y no las habrá. Un asistente al que se le pida «hacer retirar» una reseña solo puede denunciarla, con un motivo admisible (§4.6) — la puntuación no lo es.

7.5 Errores

Siempre un estado HTTP 200, incluso en caso de error: en JSON-RPC el error viaja en el cuerpo. Un 4xx haría creer al cliente que el transporte ha fallado, y la mayoría reintentaría en lugar de mostrar el mensaje.

La única excepción: la autenticación, rechazada antes de alcanzar la capa JSON-RPC. Responde con el sobre de error habitual de la API.

EstadoCódigoCausa
401invalid_mcp_token Token desconocido, revocado o caducado — indistinguibles a propósito. El comerciante crea uno nuevo en su área.
401códigos de §2 Vía HMAC: clave ausente, firma o marca de tiempo rechazadas.
429too_many_attempts Más de 20 fallos de autenticación en quince minutos desde la misma dirección. Espere en lugar de reintentar en bucle.
CódigoSignificadoQué hacer
-32001 El plan del comerciante no incluye el acceso MCP. Pasar a un plan de pago; las reseñas siguen siendo legibles públicamente.
-32601Método JSON-RPC desconocido.Comprobar method.
-32602Herramienta desconocida.Llamar a tools/list, no codificar los nombres en duro.
-32603 Error del puente local — red, secreto ausente. Este código viene del puente anterior, no del servidor.

Los errores de negocio de una herramienta no son errores JSON-RPC: la respuesta sigue siendo un resultado, con isError: true y un objeto { "erreur": "…" } en el texto. Es el caso de una reseña no encontrada, de un contenido vacío, o de una respuesta intentada con un token de solo lectura. El asistente puede así explicárselo al comerciante en lugar de anunciar una avería.

8. Webhooks entrantes

No hay webhook saliente

La plataforma no le llama: no emite notificación alguna hacia su servidor cuando se publica una reseña, una respuesta o una decisión de moderación. Para seguir la actividad, consulte GET /api/v1/cms/reviews a su ritmo, filtrando por status=published y comparando published_at con su última pasada.

Una pasada cada hora conviene a la práctica totalidad de los usos: las reseñas no llegan al segundo, y el ritmo de publicación de una tienda se cuenta en unidades por día. Consultar cada minuto no hará aparecer nada más deprisa.

Los dos endpoints siguientes existen para llamantes concretos — nuestro operador de SMS y nuestro proveedor de pagos. Ningún integrador tiene que llamarlos, y ninguno puede hacerlo: ambos están cerrados por un secreto que no se distribuye.

POST /api/v1/stripe/webhook Firma de Stripe

Recibe los eventos de suscripción: checkout.session.completed, customer.subscription.created, .updated y .deleted. Es lo que hace pasar una cuenta al plan de pago, y por tanto lo que abre la API de reseñas y el acceso MCP.

La firma de la carga útil es lo único que protege esta ruta: sin ella, cualquiera podría enviar «suscripción activa» y regalarse el plan de pago con una sola petición curl. Se verifica antes de cualquier lectura del contenido, y un secreto ausente hace fallar la petición en lugar de dejarla pasar.

Los eventos no tratados se confirman con un 200 ({ "ignored": … }): Stripe considera toda respuesta que no sea 2xx como un fallo y reintenta durante tres días, con intervalos crecientes. Responder 404 a un tipo de evento que no nos sirve provocaría miles de reenvíos inútiles y luego la desactivación del punto de destino de su lado. A la inversa, un fallo real de tratamiento sí responde 500 — ahí queremos que Stripe reintente en lugar de dejar en plan gratuito a un comerciante que ha pagado.

POST /api/v1/sms/inbound/{token} Token compartido

Recibe los SMS entrantes, es decir los «STOP». El operador trata la palabra clave por su lado y deja de entregar — pero sin este endpoint no sabríamos nada: seguiríamos enviándole mensajes facturados y jamás recibidos, la oposición desaparecería el día de un cambio de operador, y no podríamos probar haberla respetado aunque la carga de la prueba nos incumbe.

El token viaja en la ruta, lo cual es más débil que una firma — pero es lo que las interfaces de los operadores franceses saben configurar. De ahí que este endpoint no pueda hacer otra cosa que añadir una oposición: lo peor que produce una llamada fraudulenta es impedir el envío de SMS a un número. Molesto, nunca peligroso, y reversible desde el back office.

La palabra clave se busca como primera palabra del mensaje, no en cualquier lugar dentro de él: quien escribe «que pare esto, esa tienda es pésima» no está pidiendo darse de baja, y darle de baja de oficio le quitaría el canal por el que se le contacta legítimamente. La oposición se registra para todos los comerciantes: el mensaje entrante no dice de qué tienda se trata — la persona responde al número de envío — y adivinarlo sería a la vez falso y peligroso.

Solo corta el canal SMS. El correo sigue saliendo: es el que lleva el enlace de gestión de la reseña y las menciones obligatorias, y una oposición expresada en un canal no vale para el otro.

9. Límites de caudal

Los límites se calculan sobre una ventana deslizante: sin contador que se ponga a cero en la hora en punto, y por tanto sin ráfaga posible al inicio de un periodo.

CanalLímiteClavePor qué esta cifra
API pública /api/v1/public/ 60 / minuto Dirección IP Una ficha de producto hace dos llamadas: eso deja 30 cargas por minuto desde una misma dirección. Amplio para un visitante, estrecho para un aspirador de contenidos.
Disponibilidad /public/health ninguno — Excluido deliberadamente: la supervisión lo consulta de continuo, y limitarlo haría saltar falsas alertas de indisponibilidad.
Apertura de conexión POST /pairing 10 / hora Dirección IP Cada llamada crea una fila en la base de datos sin autenticación alguna. Diez bastan con holgura a un integrador que vuelve a empezar.
Sondeo POST /pairing/{code} 240 / 15 minutos Dirección IP Generoso a propósito: el módulo consulta cada cinco segundos mientras el comerciante crea su cuenta, confirma su dirección y valida.
Autenticación MCP por token 20 fallos / 15 minutos Dirección IP Cuenta únicamente los fallos: una conexión que funciona nunca lo toca. Detiene el barrido de tokens hallados en otra parte y evita que un cliente mal configurado ahogue los registros.
Envío de una reseña 10 / minuto Token de la invitación Por token y no por IP: varios clientes de una misma empresa comparten a menudo una única dirección de salida, y limitarlos juntos castigaría envíos legítimos.
Denuncia pública de una reseña 5 / hora Dirección IP Abierta a todo lector (obligación DSA), y por tanto a todo robot. Cada envío crea una fila en la cola de moderación.

La API CMS no está limitada, lo que no lo autoriza todo

Hoy no se aplica ningún límite de caudal a los endpoints firmados (/api/v1/cms/ y MCP): están autenticados, y el volumen real está acotado por la cuota de solicitudes del comerciante. Trate de todos modos el 429 — podrá añadirse un límite, y una integración que no sepa leerlo se romperá el día en que aparezca.

En la práctica: transmita los pedidos sobre la marcha en lugar de en lotes nocturnos de varios miles, y consulte las reseñas cada hora en lugar de cada minuto (§8). Un volumen anómalo es visible por nuestra parte y provoca un contacto, no un corte silencioso.

Lo que devuelve un exceso

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

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Demasiadas peticiones. Inténtelo de nuevo dentro de unos instantes."
  }
}

Retry-After da el número de segundos que hay que esperar. Respételo: reintentar de inmediato solo consume la ventana siguiente. Una espera exponencial, con tope de un minuto, basta para todos los casos aquí descritos.

El sondeo de conexión es la excepción y responde { "status": "rate_limited" }: es el mismo suceso, expresado en el vocabulario de un endpoint que el módulo consulta en bucle.

10. Códigos de error comunes

Dos formatos, y solo uno que tratar en la mayoría de los casos

En toda la API, un error lleva el mismo sobre:

{
  "error": {
    "code": "invalid_api_key",
    "message": "Clave de API desconocida, revocada o caducada."
  }
}

El code es estable y está destinado a su programa; el message está destinado al humano que depura y puede reformularse sin aviso. No construya jamás su lógica sobre el texto del mensaje.

Una sola excepción: POST /cms/orders, servido por una capa distinta, devuelve sus errores de validación como una lista de violations. Un cliente robusto lee por tanto error.code si existe, y recurre a violations en caso contrario.

Estados HTTP

EstadoSentido¿Reintentar?
200Éxito. En JSON-RPC, el error eventual está en el cuerpo.—
201Creado — pedido registrado, respuesta publicada.—
202Aceptado pero no resuelto: la denuncia entra en cola.—
400Petición ilegible.No, corrija.
401Clave ausente, no válida, o firma rechazada.No, salvo reloj que resincronizar.
402El plan del comerciante no incluye esta función.No.
404Recurso desconocido — o fuera de su cuenta.No.
409Conflicto: la acción ya se ha realizado.No, es un estado, no una avería.
415Content-Type ausente o inesperado.No, envíe JSON.
422Petición bien formada pero rechazada: campo ausente, valor fuera de límites.No, corrija.
429Caudal superado.Sí, tras Retry-After.
5xxIncidencia de nuestro lado.Sí, con espera creciente.

Resumen de los códigos

CódigoEstadoDóndeCausa y remedio
missing_api_key401CMS, MCP Falta la cabecera X-Api-Key.
invalid_api_key401CMS, MCP Clave desconocida, revocada o caducada — las tres deliberadamente indistinguibles. Compruébela en el área del comerciante.
missing_signature, missing_timestamp 401CMS, MCP Escritura sin firmar. Véase §2.1.
invalid_timestamp401CMS, MCP X-Timestamp no es una marca de tiempo Unix en segundos — milisegundos o una fecha ISO, las más de las veces.
timestamp_out_of_range401CMS, MCP Más de 300 s de desfase. El mensaje da la cifra exacta: sincronice el reloj (NTP).
signature_mismatch401CMS, MCP Repase las cuatro trampas del §2.3, por orden.
invalid_mcp_token401MCP Token portador desconocido, revocado o caducado — indistinguibles. El comerciante crea uno nuevo desde su área (§7.1).
too_many_attempts429MCP Demasiados fallos de autenticación desde la misma dirección.
plan_required402Reseñas, respuesta Función incluida a partir del plan de pago. La visualización pública de las reseñas sigue siendo gratuita.
merchant_not_found404API pública Identificador público desconocido. Compruebe el slug, no el nombre comercial.
review_not_found404Respuesta, denuncia Identificador desconocido, mal formado, o perteneciente a otro comerciante: la compartimentación impone no distinguirlos.
already_reported409Denuncia Ya hay un expediente abierto sobre esta reseña.
content_required422Respuesta content ausente o vacío tras la limpieza.
invalid_reason422Denuncia Motivo fuera de lista. Una puntuación baja no es un motivo admisible (§4.6).
invalid_request422Conexión shop_domain ausente o inutilizable.
rate_limit_exceeded429API pública Véase §9 y la cabecera Retry-After.
-32001200MCP Plan sin acceso MCP (un error JSON-RPC, no HTTP).
-32601, -32602200MCP Método o herramienta desconocidos. Pase por tools/list.

Tres síntomas, y por dónde empezar

SíntomaCausa más frecuente
«Ayer funcionaba todo, hoy todo da 401.» El reloj del servidor se ha desviado. GET /cms/ping devuelve server_time: compárelo con el suyo antes de buscar en otra parte.
«El ping pasa, pero todas mis escrituras fallan.» La clave es buena, la firma no — que es justamente lo que ese reparto de regímenes permite concluir. El cuerpo casi siempre se ha recodificado después de firmarse (§2.3).
«El widget no muestra nada, pero la API responde 200 en la consola.» Dominio no declarado del lado del comerciante: el navegador bloquea la lectura por falta de cabecera CORS. O, sencillamente, aún no hay reseñas: un widget vacío se retira de la página (§6).

Si nada de esto encaja

Escríbanos desde el área del comerciante adjuntando tres cosas: la ruta llamada, la marca de tiempo de la petición y el código de error recibido. Con esos tres elementos la petición se encuentra en los registros; sin ellos, la única respuesta posible es pedírselos.

↑ Volver al principio