1. Introdução
A plataforma expõe três canais distintos. Não partilham nem o mesmo público, nem o mesmo regime de autenticação, nem os mesmos limites. Escolher o certo é a primeira decisão de uma integração.
| Canal |
Prefixo |
Para quem |
Autenticação |
| API CMS |
/api/v1/cms/ |
Módulos de comércio eletrónico, ERP, CRM, ferramentas internas |
Chave de API + assinatura HMAC na escrita |
| API pública |
/api/v1/public/ |
Widgets de apresentação, JavaScript da loja |
Nenhuma — débito limitado, CORS restrito |
| MCP |
/api/v1/mcp |
Assistentes de IA (Claude, ChatGPT, outros) |
A mesma chave e a mesma assinatura da API CMS |
Antes de escrever uma linha de código: verifique se um módulo não chega
Os módulos PrestaShop e WooCommerce fazem por inteiro o que esta página descreve: transmitem as encomendas no momento certo, colocam o script dos widgets no tema, põem as estrelas nas fichas de produto e o bloco de avaliações, e tratam da assinatura dos pedidos. O comerciante não cola nada e não escreve nada.
Descarregar os módulos →
Esta documentação destina-se pois a três casos: uma plataforma para a qual ainda não temos módulo, um desenvolvimento à medida, ou a ligação de uma ferramenta de terceiros (ERP, apoio ao cliente, assistente de IA) às avaliações já recolhidas.
Endereços de base
Todos os URL desta página são relativos ao endereço da API. Um módulo só deve conhecer esse: os restantes endereços são-lhe devolvidos por GET /api/v1/cms/me, o que lhe evita adivinhá-los e nos permite alterá-los sem atualizar o que quer que seja junto dos comerciantes.
| Utilização | Endereço |
| API (todos os canais) | https://louis.guide |
| Área do comerciante | https://louis.guide/app |
| Script dos widgets | https://louis.guide/widget/v1/avis.js |
Convenções
- Formato — JSON tanto à entrada como à saída, codificado em UTF-8. O cabeçalho
Content-Type: application/json é esperado em qualquer pedido com corpo.
- Nomenclatura — serpente minúscula (
external_order_id, experienced_at), a convenção dominante das API que os integradores de PHP e JavaScript utilizam.
- Datas — ISO 8601 com fuso explícito à entrada (
2026-08-01T14:22:00+02:00). À saída, as datas completas usam o mesmo formato; as datas públicas de uma avaliação são reduzidas ao dia (2026-08-01) porque nenhum widget mostra a hora.
- Montantes — transmitidos como cadeia (
"129.90") e nunca como número de vírgula flutuante: um cêntimo perdido no arredondamento de uma encomenda torna-se um desvio de faturação.
- Identificadores — os objetos que criamos levam um UUID permanente. Os vossos (encomenda, produto, variante) continuam a ser vossos: nunca os reescrevemos.
-
Erros —
sempre o mesmo envelope
{ "error": { "code": …, "message": … } }. O code é estável e destina-se ao vosso programa, a message ao humano que está a depurar. Ver §10.
-
Versionamento —
o
/v1 do caminho é um contrato. Um campo opcional pode ser aí acrescentado a qualquer momento; nenhum campo existente será renomeado, removido nem tornado obrigatório. Uma rutura sairia em /v2, permanecendo a versão anterior servida — os módulos correm junto dos comerciantes e ninguém os pode atualizar à distância.
O vosso código deve pois ignorar os campos que não conhece em vez de falhar ao vê-los.
Obter uma chave de API
-
Crie uma conta de comerciante em a área do comerciante.
- Confirme o endereço de correio eletrónico e abra em seguida a secção das chaves de API.
- Anote o segredo: só é mostrado uma vez. Uma vez perdido não se recupera — cria-se uma nova chave e revoga-se a antiga.
Um módulo de instalação não precisa desta manobra: abre ele próprio um pedido de ligação que o comerciante valida com um clique. Ver §3.
2. Autenticação
A API CMS e o servidor MCP usam o mesmo mecanismo: uma chave que diz quem chama, e uma assinatura que prova que quem chama detém o segredo. São duas coisas distintas.
| Método HTTP | Cabeçalhos exigidos | Porquê |
GET, HEAD |
X-Api-Key |
Uma leitura não modifica nada: a chave basta para a autorizar. |
POST, PUT, PATCH, DELETE |
X-Api-Key, X-Timestamp, X-Signature |
Uma escrita vincula o comerciante: tem de ser provada e não reproduzível. |
O segredo nunca circula
Só a assinatura viaja. Isso fecha três portas que não exigem qualquer comprometimento da loja: a fuga passiva do segredo nos registos de um intermediário, a reprodução de um pedido intercetado e a alteração do corpo em trânsito. Em contrapartida, não protege de uma loja cuja base de dados tenha sido roubada — contra esse caso a defesa é a rotação das chaves.
Nunca ponha o segredo num URL: os URL acabam nos registos de todos os intermediários atravessados.
2.1 A assinatura, passo a passo
Passo 1 — Os três cabeçalhos
| Cabeçalho | Conteúdo |
X-Api-Key |
Identificador público da chave, tal como aparece na área do comerciante. |
X-Timestamp |
Marca temporal Unix em segundos, apenas algarismos. Nada de milissegundos, nada de data ISO. |
X-Signature |
O prefixo literal sha256= seguido do HMAC-SHA256 em hexadecimal minúsculo. O prefixo faz parte do valor comparado: omiti-lo produz uma recusa. |
Passo 2 — Construir a carga a assinar
Quatro pedaços concatenados sem separador, exatamente nesta ordem:
charge = X-Timestamp
+ MÉTHODE HTTP en majuscules
+ chemin logique de la requête
+ corps brut de la requête
| Pedaço | Regra exata |
| Marca temporal |
A cadeia idêntica à enviada em X-Timestamp. |
| Método |
POST, PUT… sempre em maiúsculas. |
| Caminho |
O caminho sem esquema, sem anfitrião, sem cadeia de consulta, começando por / — por exemplo /api/v1/cms/orders. Se a API for servida a partir de uma subpasta, esse prefixo de instalação não entra na assinatura: vive no endereço de base, não no caminho lógico. |
| Corpo |
A cadeia de bytes exatamente tal como é enviada. Serialize uma vez, assine essa cadeia, envie essa cadeia. Corpo vazio → cadeia vazia. |
Passo 3 — Calcular
X-Signature = "sha256=" + HMAC_SHA256(charge, secret) // hexadécimal minuscule
Passo 4 — Verificar a vossa implementação com este exemplo
Estes valores são fixos e a assinatura mostrada é realmente a destes dados: se o vosso código produzir outra coisa, o problema está no vosso código, não no nosso.
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 a assinar (uma única linha, nenhum espaço acrescentado):
1786000000POST/api/v1/cms/reviews/9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d/response{"content":"Merci pour votre retour !"}
Resultado esperado:
X-Signature: sha256=8a9c0de10516226f965465704902e00c666892b483b45b361f4536a6b2ee36e9
O mesmo cálculo numa linha 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 e não echo: este último acrescenta uma mudança de linha final, que altera a assinatura.
Passo 5 — Uma chamada completa em curl
SECRET='sk_demo_3f9c1a7e5b2d48a6'
CLE='ak_demo_5c2f81b0'
TS=$(date +%s)
CHEMIN='/api/v1/cms/orders'
CORPS='{"external_order_id":"CMD-1042","experienced_at":"2026-08-01T14:22:00+02:00","customer":{"email":"claire.martin@exemple.fr","first_name":"Claire","country":"FR","locale":"fr"},"source":{"platform":"custom","plugin_version":"1.0.0"}}'
SIG=$(printf '%s' "$TS""POST""$CHEMIN""$CORPS" \
| openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)
curl -X POST 'https://louis.guide/api/v1/cms/orders' \
-H "X-Api-Key: $CLE" \
-H "X-Timestamp: $TS" \
-H "X-Signature: sha256=$SIG" \
-H 'Content-Type: application/json' \
--data-raw "$CORPS"
--data-raw e não --data: o segundo interpreta certos carateres e pode alterar o corpo enviado, invalidando assim a assinatura.
2.2 Exemplo em PHP
O cliente mínimo, sem dependências. É o mesmo mecanismo dos módulos PrestaShop e WooCommerce, reduzido ao essencial.
<?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 As quatro armadilhas
Quatro causas explicam a quase totalidade dos signature_mismatch. Vistas de fora parecem-se todas — daí o interesse de as afastar por esta ordem.
- O corpo foi recodificado depois da assinatura. O caso mais frequente e o mais difícil de ver: um array serializado duas vezes dá duas cadeias diferentes assim que contém um acento ou uma barra (
JSON_UNESCAPED_UNICODE, JSON_UNESCAPED_SLASHES, ordem das chaves). Assine a cadeia, envie essa cadeia, nunca a reconstrua.
- O caminho assinado leva um prefixo que não deveria ter. O caminho assinado é
/api/v1/cms/orders, mesmo que a API seja servida a partir de https://exemplo.pt/plataforma/api/v1/cms/orders. O prefixo de instalação pertence ao endereço de base. Inversamente, também não assine o URL completo com o seu esquema e o seu anfitrião.
-
O relógio do servidor está a derivar.
Tolerância: 300 segundos de desvio, num sentido como no outro. Para além disso, a resposta é
timestamp_out_of_range e a sua mensagem indica o desvio medido em segundos — exatamente a informação a dar ao vosso alojador. Este caso manifesta-se muitas vezes como uma integração que "ontem funcionava".
- Faltam o método ou o prefixo. O método entra na carga em maiúsculas, e o valor de
X-Signature começa por sha256=. Um HMAC nu, sem prefixo, é recusado.
O que a cadeia de consulta não faz
Os parâmetros de URL (?page=2) não entram na carga assinada: só o caminho aí figura. Na prática não têm consequência, já que os endpoints assinados são todos escritas que levam os seus parâmetros no corpo — mas uma implementação que os acrescentasse à carga falharia.
Reprodução e janela de validade
A carga assinada cobre a marca temporal, o método, o caminho e o corpo. Omitir um deles abriria uma falha: sem o caminho, uma assinatura válida para POST /orders seria reproduzível em DELETE /orders; sem a marca temporal, o pedido seria reproduzível indefinidamente.
A janela de 300 segundos é o que limita a reprodução: um pedido intercetado não pode ser reenviado para além dela. Não existe dicionário de assinaturas já vistas — dentro dessa janela um pedido idêntico é pois aceite duas vezes. Isso não tem efeito na transmissão das encomendas, que é idempotente por external_order_id: o segundo recebe a encomenda já registada e não envia um segundo correio.
Respostas de autenticação
Todas estas respostas levam o estado 401.
| Código | Causa | A fazer |
missing_api_key |
Cabeçalho X-Api-Key ausente. |
Acrescentar o cabeçalho. |
invalid_api_key |
Chave desconhecida, revogada ou expirada. A mensagem é deliberadamente idêntica nos três casos: distingui-los permitiria testar em massa que identificadores existem. |
Verificar a chave na área do comerciante, ou criar uma nova. |
missing_signature |
Escrita sem cabeçalho X-Signature. |
Assinar o pedido (§2.1). |
missing_timestamp |
Escrita sem cabeçalho X-Timestamp. |
Acrescentar a marca temporal e assiná-la. |
invalid_timestamp |
X-Timestamp não é uma sequência de algarismos — milissegundos, data ISO ou um sinal. |
Enviar uma marca temporal Unix em segundos. |
timestamp_out_of_range |
Mais de 300 segundos de desvio. A mensagem dá o valor exato. |
Sincronizar o relógio do servidor (NTP). |
signature_mismatch |
A assinatura não corresponde à carga esperada. |
Percorrer as quatro armadilhas do §2.3, por ordem. |
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "timestamp_out_of_range",
"message": "Marca temporal fora de tolerância: +412 s de desvio face ao nosso servidor (máximo 300 s). O relógio do vosso servidor está provavelmente dessincronizado."
}
}
3. Ligar uma loja
Estes dois endpoints permitem a um módulo obter uma chave sem que o comerciante tenha de copiar seja o que for. O módulo abre um pedido, mostra uma ligação, o comerciante valida no seu navegador, e o módulo recebe a sua chave e o seu segredo na leitura seguinte.
São sem autenticação, por construção. A segurança não assenta numa identidade mas em três coisas: o pedido nada obtém enquanto um comerciante autenticado não o tiver validado, o token de sondagem nunca sai do servidor da loja, e o segredo só é entregue uma vez. O pior que uma chamada maliciosa pode produzir é um pedido pendente que ninguém aprovará — e que expira num quarto de hora.
POST
/api/v1/pairing
Sem autenticação
Abre um pedido de ligação e devolve a ligação de validação a apresentar ao comerciante.
| Campo | Tipo | Obrigatório | Descrição |
shop_domain | cadeia | sim |
Domínio da loja, p. ex. loja.exemplo.pt. |
platform | cadeia | não |
prestashop, woocommerce, custom… unknown por omissão. |
shop_name | cadeia | não |
Nome legível da loja, reutilizado na criação da conta. |
platform_version | cadeia | não |
Versão da plataforma, p. ex. 8.1.6. |
plugin_version | cadeia | não |
Versão do módulo que chama. |
shop_uid | cadeia | não |
Identificador único gerado uma vez na instalação do módulo. Fortemente recomendado com várias lojas: ver §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 — a abrir num novo separador do navegador do comerciante, fora do seu back office. É aí que ele se liga ou cria a sua conta, e depois valida.
poll_token — a conservar unicamente do lado do servidor. Nunca deve aparecer numa página nem num URL: é ele que permitirá obter o segredo.
code — mostrável ao comerciante, para que verifique que está a validar o pedido certo.
Códigos de erro
| Estado | Código | Causa |
| 422 | invalid_request | shop_domain ausente ou inutilizável. |
| 429 | rate_limited | Mais de 10 aberturas por hora e por IP. |
POST
/api/v1/pairing/{code}
Sem autenticação
Consulta o estado do pedido e entrega a chave assim que — e uma só vez — o comerciante tenha validado.
Em POST apesar de ser uma leitura, porque a chamada tem um efeito secundário: consome o segredo. Em GET, um pré-carregador do navegador ou um antivírus que siga as ligações consumi-lo-ia em vez do módulo.
| Campo | Tipo | Obrigatório | Descrição |
poll_token | cadeia | sim |
O token recebido na abertura do pedido. |
curl -X POST 'https://louis.guide/api/v1/pairing/4K7M-9QR3' \
-H 'Content-Type: application/json' \
--data-raw '{"poll_token":"pt_8f2c1a7e5b2d48a6c3d9e0f1a2b3c4d5"}'
A aguardar validação:
HTTP/1.1 200 OK
{ "status": "pending" }
Validado — as credenciais só são entregues nesta chamada:
HTTP/1.1 200 OK
{
"status": "approved",
"api_key": "ak_live_5c2f81b0",
"secret": "sk_live_3f9c1a7e5b2d48a6",
"merchant": "tissufiesta"
}
Valores possíveis de status
| Valor | Significado | Que fazer |
pending | O comerciante ainda não decidiu. | Continuar a sondar. |
approved | Validado. A resposta leva as credenciais. | Guardá-las, parar a sondagem. |
rejected | O comerciante recusou. | Parar e comunicar-lho. |
expired | Quinze minutos decorridos sem decisão. | Reabrir um pedido. |
consumed |
O segredo já foi entregue, e isso nunca acontece duas vezes. O módulo perdeu a resposta. |
Recomeçar uma ligação — é o comportamento seguro. |
unknown |
Código desconhecido ou token errado. Deliberadamente indistintos: separá-los faria deste endpoint um oráculo capaz de revelar que lojas se estão a ligar. |
Verificar o par código / token. |
rate_limited | Demasiadas sondagens (estado HTTP 429). | Espaçar as chamadas. |
Guarde o segredo de imediato. Só é transmitido nessa resposta. Um módulo que não consiga persisti-lo terá de fazer o comerciante repetir toda a ligação.
Sempre 200, mesmo para um estado de espera. O módulo consulta em ciclo: um código HTTP de erro numa situação perfeitamente normal faria subir alertas para nada. Sonde de cinco em cinco segundos; o limite é de 240 chamadas por quarto de hora e por IP — para além disso, a resposta é { "status": "rate_limited" } com um estado 429.
4. API CMS (assinada)
O canal das integrações de servidor: módulos de comércio eletrónico, ERP, CRM, ferramentas internas. Todos os URL são precedidos de https://louis.guide.
Leituras: a chave basta. Escritas: chave + assinatura. O detalhe do cálculo está em §2. As fichas abaixo recordam o regime de cada uma com um distintivo.
O que a API não permite, e não permitirá
Nenhum endpoint modifica nem elimina uma avaliação. O comerciante pode responder publicamente e denunciar para moderação, mais nada — exatamente o que a sua área permite. Uma API mais permissiva do que a interface seria uma porta traseira na conformidade, e é a primeira coisa que uma auditoria verifica.
GET
/api/v1/cms/ping
Chave de API
Verifica que uma chave funciona. É a primeira chamada a escrever, e a que convém propor ao comerciante sob a forma de um botão «Testar a ligação»: mais vale que descubra uma chave errada na configuração do que na primeira encomenda não transmitida.
Sem assinatura, deliberadamente. Uma leitura não modifica nada e, sobretudo: este endpoint deve continuar utilizável para provar que uma chave está boa enquanto a implementação HMAC ainda está errada. O ping passa, a escrita não: o problema está na assinatura, não na chave.
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 é devolvido por uma razão precisa: comparem-no com o relógio do vosso servidor. Um desvio superior a 300 segundos fará falhar todas as vossas escritas assinadas (§2.3), e é aqui que se vê antes de se perder um dia com isso.
GET
/api/v1/cms/me
Chave de API
Estado da conta: identidade do comerciante, capacidades do plano, quota e endereços da 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/pt/m/tissufiesta"
},
"server_time": "2026-08-13T14:52:07+00:00"
}
Leiam as capacidades, não o nome do plano
O bloco plan expõe capacidades (can_…) para além do código do plano. Testem as primeiras: um módulo que fixa no código if (plan === 'pro') deixará de estar certo no dia em que um plano for acrescentado ou mudar de nome, em todos os comerciantes ao mesmo tempo e sem que nenhum o possa corrigir.
| Capacidade | O que comanda |
can_display_product_reviews |
Apresentação das avaliações de produto — as estrelas nas fichas. |
can_use_photos |
Saída das fotografias de clientes pela API. São recolhidas já a partir do plano gratuito mas só são servidas a pagar: numa conta gratuita a galeria responde com uma lista vazia, nunca com um erro. |
can_use_reviews_api |
Leitura das avaliações pela API, resposta às avaliações e acesso MCP. A transmissão das encomendas não é abrangida: está incluída em todos os planos. |
can_remove_branding |
Retirada da menção da plataforma nos widgets e nos correios. |
O bloco urls evita adivinhar
A vossa integração só deve conhecer um único endereço: o da API. Os restantes — área do comerciante, script dos widgets, página pública do comerciante — são devolvidos aqui. Um módulo que os recompõe a partir de uma base única pressupõe que tudo vive no mesmo anfitrião, o que deixa de ser verdade assim que um canal passa para um subdomínio, e produz ligações mortas em todos os comerciantes já instalados.
quota.remaining merece lugar na vossa interface: a zero, as encomendas continuam a ser aceites mas já não sai qualquer solicitação. Avisar aos 90 % de consumo evita ao comerciante descobri-lo nas suas estatísticas.
POST
/api/v1/cms/orders
Assinatura exigida
O endpoint central. Regista uma encomenda e agenda o pedido de avaliação. Todo o resto da plataforma decorre desta chamada: sem ela não há solicitação, nem avaliação, nem nota.
Idempotente por external_order_id
Reenviar a mesma referência devolve a encomenda já registada com um estado 200 em vez de 201, sem criar duplicado e sem enviar um segundo correio ao cliente. O campo idempotent da resposta vale então true. Podem portanto repetir sem precaução após um corte de rede ou um tempo de espera excedido — é o comportamento a preferir a qualquer lógica de desduplicação caseira.
Quando chamar
No momento em que a experiência é vivida, não encomendada: na entrega, na expedição consoante o vosso ofício, ou na passagem ao estado que faça as vezes. É experienced_at que leva essa data, e é ela que faz arrancar o prazo de solicitação.
Corpo do pedido
Raiz
| Campo | Tipo | Obrig. | Descrição |
external_order_id | cadeia (100) | sim |
Referência da encomenda do vosso lado. Chave de idempotência e prova de compra conservada cinco anos (AFNOR §6.3). Deve ser estável no tempo. |
customer | objeto | sim |
Identidade do cliente a solicitar — ver a tabela seguinte. |
experienced_at | ISO 8601 | sim |
Data de entrega ou de consumo, com fuso explícito. Ver a caixa abaixo: não é a data da encomenda. |
source | objeto | sim |
Contexto técnico do envio — ver mais abaixo. |
items | array (200 máx.) | não |
Artigos. Sem eles, não será pedida qualquer avaliação de produto — apenas a do estabelecimento. |
amount | cadeia decimal | não |
Montante total, p. ex. "129.90". Nunca um número de vírgula flutuante. |
currency | ISO 4217 | não |
"EUR", "CHF"… |
channel | enumeração | não |
ecommerce_order (por omissão), pos_transaction, qr_scan, manual, csv_import, nfc. |
location_id | cadeia (100) | não |
Estabelecimento em causa, tal como declarado pelo comerciante. Um valor desconhecido faz o pedido falhar com um 422 em vez de ligar a encomenda ao ponto de venda errado. |
solicitation_delay_days | inteiro 0–365 | não |
Prazo próprio desta encomenda, que se sobrepõe à definição da conta. Útil quando um mesmo vendedor expede um ramo a solicitar amanhã e um colchão a solicitar daqui a um mês. Fora dos limites, o valor é ignorado e aplica-se a definição da conta — um valor aberrante não deve fazer perder uma encomenda. |
order_status_id | cadeia (20) | não |
Estado da encomenda do vosso lado no momento do envio. Puramente de diagnóstico — não o interpretamos — mas é a única informação que permite responder a «porque é que esta encomenda não desencadeou nada». |
order_status_label | cadeia (120) | não |
Etiqueta legível desse estado. |
experienced_at: a data de entrega, não a da encomenda
Uma encomenda feita no dia 1 e entregue no dia 6 leva o 6. Não é uma subtileza: dois ensaios aleatorizados sobre mais de 300 000 consumidores (Jung, Ryu, Han & Cho, Journal of Marketing, 2023) estabelecem que uma solicitação enviada antes de o cliente ter podido formar uma opinião tem um efeito negativo sobre a taxa de submissão. Ancorar o prazo na data da encomenda equivale a solicitar sistematicamente demasiado cedo, de todo o prazo de entrega.
É também uma das três datas apresentadas publicamente ao lado da avaliação (AFNOR §6.3).
customer
| Campo | Tipo | Obrig. | Descrição |
email | correio (255) | sim |
O único dado pessoal em claro que aceitamos. Apagado após o prazo de submissão; só a sua impressão digital subsiste. |
country | ISO 3166-1 alpha-2 | não* |
*Fortemente recomendado. A Google calcula as suas notas de comerciante por país e descarta as avaliações cujo país seja desconhecido. Esta informação só existe no momento da encomenda: uma vez expurgado o endereço, é definitivamente irrecuperável, sem qualquer forma de recuperação. |
locale | fr, en, nl, de, it, es | não |
Língua do correio de solicitação. Na falta dela, a língua por omissão do comerciante — solicitar um cliente neerlandófono em francês faz cair a taxa de resposta. |
phone | cadeia (32) | não |
Telemóvel para a solicitação por SMS. Formato internacional (+33612345678) fortemente recomendado: é o único sem ambiguidade. Um número nacional é convertido a partir de country; sem país conhecido é descartado sem fazer a encomenda falhar. Ver o aviso abaixo. |
first_name, last_name | cadeia (100) | não |
Personalização da solicitação e nome apresentado do autor. |
company | cadeia (255) | não |
Denominação social, para uma encomenda profissional. |
postal_code, city | cadeia | não |
Expurgados ao mesmo tempo que o endereço de correio. |
Só enviem o número de telemóvel se o comerciante tiver subscrito o SMS. Sem essa opção, ele é recebido e conservado sem que qualquer mensagem saia: um dado pessoal recolhido sem finalidade, o que nenhuma das duas partes pode justificar em caso de fiscalização.
source — obrigatório
Este bloco não é estatística. Quando um comerciante escreve «as minhas avaliações deixaram de sair desde a atualização», a resposta já lá está: versão da plataforma, versão do módulo, acontecimento desencadeador. Torná-lo opcional equivaleria a nunca o ter — os integradores preenchem o que é exigido, não o que é sugerido.
| Campo | Tipo | Obrig. | Descrição |
platform | cadeia (50) | sim |
prestashop, woocommerce, shopify, magento, custom… |
platform_version | cadeia (30) | não |
P. ex. 8.1.6. |
plugin_version | cadeia (30) | não |
Versão da vossa integração. A incrementar a cada entrega. |
trigger | cadeia (100) | não |
Acontecimento na origem do envio, p. ex. woocommerce_order_status_completed. Permite compreender por que motivo uma encomenda sai demasiado cedo ou demasiado tarde. |
shop_uid | cadeia (80) | não* |
*Decisivo com várias lojas. Identificador gerado uma vez na instalação e conservado. Ver a caixa. |
shop_id | cadeia (50) | não |
Identificador de loja na plataforma. Serve de recurso quando shop_uid está ausente. |
shop_name | cadeia (255) | não |
Nome legível desta loja. Sem ele, o comerciante descobre na sua área um estabelecimento chamado «3» e tem de adivinhar qual é. |
shop_group_id, lang_id | cadeia | não |
Conservados para diagnóstico, nunca interpretados. lang_id não separa nada: a língua da avaliação vem de customer.locale. |
Várias lojas: shop_id não chega
Vale «1» em qualquer instalação de loja única. Um comerciante que explora dois sítios sob a mesma conta — uma marca por domínio, caso corrente — enviaria portanto «1» a partir dos dois: as duas lojas fundir-se-iam num só estabelecimento, as avaliações de uma apareceriam na página da outra, e o nome mantido seria o da última encomenda recebida. Defeito constatado em testes sobre dois PrestaShop reais.
shop_uid resolve o problema: gerem-no uma vez na instalação, conservem-no. Sobrevive a uma mudança de domínio tal como a uma renovação de chave — os outros dois discriminantes em que se pensa primeiro, e que ambos mudam.
items[] — facultativo, 200 artigos no máximo
| Campo | Tipo | Obrig. | Descrição |
external_product_id | cadeia (100) | sim |
Identificador do produto no vosso catálogo. |
name | cadeia (255) | sim |
Nome do produto tal como apresentado ao cliente. |
variant_id | cadeia (100) | não* |
*O campo mais importante desta lista. Sem ele, a cadeira vermelha e a cadeira azul partilham a mesma chave de produto: as suas avaliações misturam-se e «a perna partiu-se» já não designa nada. Corresponde a id_product_attribute (PrestaShop), à variação (WooCommerce), ao variant (Shopify). |
variant_label | cadeia (255) | não |
Etiqueta legível: «Cor: vermelho, Tamanho: L». |
gtin | de 8 a 14 algarismos | não* |
EAN-13 ou UPC-A convertido. Chave de agregação entre comerciantes, e exigência da Google para mostrar as estrelas nos seus resultados. |
upc, isbn, mpn | cadeia | não |
Mantidos separados do GTIN porque os catálogos os mantêm em colunas distintas. O ISBN é decisivo no livro, onde o GTIN está muitas vezes vazio. |
sku, brand | cadeia | não |
Referência interna e marca. |
category_id, category_name | cadeia | não |
Categoria principal no vosso catálogo. |
product_url, image_url | URL (500) | não |
Usadas no correio de solicitação: uma imagem de produto melhora nitidamente a taxa de submissão. |
images | lista de URL (10 máx.) | não |
Imagens suplementares. |
description | cadeia (5000) | não |
Recebida, nunca reapresentada tal e qual: é o vosso texto, não o do autor da avaliação. Serve para situar o produto na moderação. |
tags | lista (30 máx.) | não |
Palavras-chave do produto, 60 carateres cada uma. |
meta_title, meta_description | cadeia | não |
Metadados da ficha. |
quantity | inteiro > 0 | não |
1 por omissão. |
unit_price | cadeia decimal | não |
P. ex. "19.90". Em cadeia, como todos os montantes. |
Exemplo 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"
}
}
Encomenda registada:
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
}
Mesmo pedido reproduzido:
HTTP/1.1 200 OK
{ "…": "…", "idempotent": true }
O endereço de correio nunca é devolvido, mesmo que o acabem de transmitir: qualquer dado devolvido é um dado que pode escapar para os vossos próprios registos.
Valores possíveis de status
| Valor | Significado |
pending | Recebida, a aguardar agendamento. |
scheduled | Solicitação agendada. |
solicited | Pedido de avaliação enviado ao cliente. |
reviewed | O cliente submeteu a sua avaliação. |
cancelled | Anulada antes do envio. |
expired | Prazo de submissão decorrido sem avaliação. |
Erros
Este endpoint é o único servido pela API Platform: os seus erros de validação chegam portanto sob a forma de lista de violations, e não no envelope { "error": … } do resto da API. O vosso código deve aceitar as duas 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."
}
]
}
| Estado | Causa | A fazer |
| 401 |
Chave ausente, inválida, ou assinatura recusada. |
Ver §2. |
| 422 |
Falta um campo ou está mal formado — ver violations. |
Corrigir o campo indicado por propertyPath. |
| 422 |
experienced_at está no futuro (para além de um dia de margem). |
Verificar o fuso horário do servidor: é quase sempre daí que vem o desvio. |
| 422 |
experienced_at remonta a mais de 90 dias. |
Ver a caixa abaixo. Para retomar um histórico, contactem o apoio. |
| 422 |
Nenhum estabelecimento corresponde a location_id. |
Criar o estabelecimento na área do comerciante, ou omitir o campo. |
| 415 |
Cabeçalho Content-Type ausente ou inesperado. |
Enviar Content-Type: application/json. |
Porque são recusadas as encomendas com mais de 90 dias
O cenário de sinistro é conhecido: instala-se um módulo e empurra três anos de histórico de uma só vez. Milhares de convites saem para endereços caducados, a taxa de devolução dispara — e como todos os correios saem do nosso domínio, é a entregabilidade de todos os comerciantes que se desmorona, não apenas a do recém-chegado.
A recusa é pronunciada à entrada, com uma mensagem explícita, em vez de no agendamento: o integrador compreende imediatamente em vez de ver as suas encomendas desaparecerem em silêncio.
GET
/api/v1/cms/reviews
Chave de API
Plano pago
Lista as avaliações do comerciante, da mais recente à mais antiga, com a resposta publicada e a eventual denúncia de cada uma. É este endpoint que permite trazer as avaliações para um ERP, um CRM ou uma ferramenta de apoio ao cliente.
| Parâmetro | Por omissão | Descrição |
type | merchant |
merchant para as avaliações do estabelecimento, product para as avaliações de produto. |
status | todos |
published, pending, awaiting_email, rejected, disputed, withdrawn. Um valor desconhecido é ignorado — o filtro deixa então de se aplicar, em vez de devolver um erro. |
page | 1 | Número de página. |
per_page | 25 |
De 1 a 100. Acima disso, o valor é reduzido 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
}
As três datas, e porque são três
experienced_at (a experiência vivida), submitted_at (a submissão) e published_at (a colocação em linha) são três coisas diferentes, e a AFNOR impõe que se possam distinguir. Um integrador que as confunde mostra «há 3 dias» numa experiência de há três semanas. published_at vale null enquanto a avaliação não estiver publicada.
order_reference retoma o vosso external_order_id: é ele que liga a avaliação à encomenda no vosso sistema. Vale null para uma avaliação submetida fora de uma solicitação.
Sincronização incremental
Consultem com status=published e comparem published_at com a última passagem: trazer todo o histórico em cada execução funciona nos primeiros meses, e depois torna-se um pedido de vários milhares de linhas de hora a hora. A paginação começa em 1 e o campo total dá o número de avaliações correspondentes ao filtro, não o número de páginas.
| Estado | Código | Causa |
| 402 | plan_required |
O plano do comerciante não inclui a API de avaliações. As avaliações continuam legíveis sem chave através da API pública — o que não é a mesma coisa: essa serve a apresentação pública, não a exportação. |
POST
/api/v1/cms/reviews/{uuid}/response
Assinatura exigida
Plano pago
Publica uma resposta pública a uma avaliação do estabelecimento, ou atualiza a que já existe. Uma avaliação leva apenas uma resposta: reenviar substitui o texto.
| Campo | Tipo | Obrig. | Descrição |
content | cadeia | sim |
Texto da resposta. Truncado a 3000 carateres sem erro — verifiquem o comprimento do vosso lado se o corte vos incomodar. |
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
}
}
Leiam published antes de anunciar seja o que for
O comerciante define na sua área se as respostas escritas por um programa saem diretamente ou aguardam a sua releitura. Essa definição vive na conta e não é um parâmetro do pedido: se quem chama pudesse escolher por si próprio se deve ser relido, a garantia não valeria nada.
Consequência para a vossa interface: uma resposta aceite não é necessariamente visível. published: false significa «guardada como rascunho, a validar na área do comerciante» — digam-no, em vez de mostrar um «publicado» que a página pública irá desmentir.
201 na criação, 200 na atualização; o campo created retoma a mesma informação no corpo. Uma resposta já publicada continua a sê-lo: uma atualização nunca a devolve a rascunho, o que a faria desaparecer da página sem que ninguém o tivesse decidido.
| Estado | Código | Causa |
| 402 | plan_required | Plano sem resposta às avaliações. |
| 404 | review_not_found |
Identificador desconhecido, mal formado, ou pertencente a outro comerciante — os três casos são indistinguíveis, e é a compartimentação que o impõe. |
| 422 | content_required | content ausente ou vazio. |
POST
/api/v1/cms/reviews/{uuid}/report
Assinatura exigida
Denuncia uma avaliação para moderação. A avaliação passa ao estado «contestada» e o processo entra na fila de instrução.
| Campo | Tipo | Obrig. | Descrição |
reason | enumeração | sim |
Motivo, a escolher na lista abaixo. |
detail | cadeia | não |
Precisões para o moderador, truncadas a 1000 carateres. É aqui que se escreve «encomenda n.º X, nunca entregue nesse endereço» — uma denúncia fundamentada é instruída mais depressa. |
Motivos admissíveis
| Valor | Quando o invocar |
inappropriate_content | Injúria, discurso de ódio, conteúdo ilícito. |
spam_or_advertising | Publicidade, ligação comercial, conteúdo automatizado. |
off_topic | Sem relação com a experiência vivida — a transportadora, o tempo. |
conflict_of_interest | Concorrente, antigo empregado, avaliação remunerada. |
personal_data_disclosure | A avaliação expõe dados pessoais. |
Uma nota baixa não é um motivo
Nenhum motivo permite contestar uma avaliação por causa da sua nota, e isso não é um esquecimento: é essa proibição que faz a diferença entre uma plataforma de avaliações e uma montra. Uma denúncia mal fundamentada é recusada, e a avaliação continua em linha.
HTTP/1.1 202 Accepted
{
"review_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"report": {
"reason": "off_topic",
"status": "pending",
"review_remains_visible": true
}
}
review_remains_visible vale sempre true, e o campo existe para que nenhuma interface seja concebida supondo o contrário: a avaliação permanece pública durante toda a instrução. Retirá-la perante uma simples denúncia equivaleria a deixar o comerciante desvalorizar o que lhe desagrada — a Google proíbe-o explicitamente, a AFNOR também. A resposta é um 202: o pedido fica registado, não decidido.
| Estado | Código | Causa |
| 404 | review_not_found | Identificador desconhecido, mal formado, ou fora da vossa conta. |
| 409 | already_reported | Já existe uma denúncia aberta sobre esta avaliação. |
| 422 | invalid_reason |
Motivo ausente ou fora da lista. A mensagem recorda os valores admitidos. |
5. API pública
Só de leitura, sem autenticação, em /api/v1/public/. É o que os widgets consomem, e o que qualquer apresentação à medida pode consumir.
O {slug} dos caminhos é o identificador público do comerciante — o da sua página pública, visível em urls.profile devolvido por /cms/me.
O que protege uma API sem chave
Não há identidade a verificar: este código corre junto dos visitantes de uma loja, nenhum segredo aí pode viver. A proteção assenta portanto em três outras coisas, que é preciso conhecer antes de integrar.
-
Daqui não sai qualquer dado sensível. Nem correio, nem impressão digital de correio, nem referência de encomenda, nem identificador interno. Um widget mostra avaliações públicas; tudo o que sai por este canal é legível por qualquer pessoa.
-
Débito limitado a 60 pedidos por minuto e por endereço IP, em janela deslizante. Uma ficha de produto faz duas chamadas: isso deixa 30 carregamentos por minuto a partir do mesmo endereço — amplo para um visitante, apertado para um aspirador de conteúdos. Ver §9.
-
CORS restrito aos domínios declarados do comerciante. Um caráter universal
* autorizaria qualquer sítio — concorrente, comparador, contrafator — a mostrar as avaliações de qualquer comerciante como se fossem suas.
CORS: o que é preciso declarar para que o navegador aceite a resposta
O cabeçalho Access-Control-Allow-Origin só é colocado se a origem que chama corresponder a um domínio ligado ao comerciante indicado no URL. Os subdomínios são aceites: um domínio declarado como exemplo.pt autoriza www.exemplo.pt e loja.exemplo.pt.
Sintoma típico de um domínio não declarado: o pedido parte, o servidor responde 200, e o navegador bloqueia a leitura na consola. A solução está na área do comerciante, não no código.
Uma chamada de servidor para servidor não é abrangida: sem cabeçalho Origin, não há controlo CORS. Esse caso é coberto pela limitação de débito. O CORS protege o navegador de outro sítio, nunca o próprio dado.
Cache
Todas as respostas são públicas e guardadas em cache: 60 segundos para as avaliações e as notas, 300 segundos para as definições de apresentação. É o que permite absorver o tráfego de uma loja em promoção sem dimensionar para o pico. Não construam uma apresentação que pressuponha o aparecimento instantâneo de uma avaliação publicada.
GET
/api/v1/public/merchants/{slug}/score
Sem autenticação
Nota global da loja e distribuição por nota.
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 é devolvido explicitamente em vez de subentendido: um integrador que programa «em 10» porque o seu anterior prestador o era produz uma apresentação falsa que ninguém relê. count só conta as avaliações publicamente visíveis.
404 merchant_not_found se o slug for desconhecido.
GET
/api/v1/public/merchants/{slug}/reviews
Sem autenticação
Avaliações sobre o estabelecimento, da mais recente à mais antiga.
| Parâmetro | Por omissão | Descrição |
page | 1 | Número de página. |
per_page | 10 |
De 1 a 50. Acima disso, reduzido 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
}
A ordem cronológica é imposta, não escolhida
Não existe parâmetro de ordenação, e não existirá: a AFNOR (§6.3) exige a ordem cronológica inversa como apresentação por omissão. Propor «os mais bem avaliados primeiro» como ordenação inicial seria uma apresentação enviesada. Uma ordenação em JavaScript sobre a página recebida é da vossa responsabilidade, não da nossa.
O que a vossa apresentação deve retomar
-
Duas datas no mínimo — a da experiência e a da publicação. É uma obrigação de apresentação, e só a API vo-las pode fornecer. As datas públicas são reduzidas ao dia (
2026-08-06): nenhum widget mostra a hora.
verified_purchase — a avaliação está ligada a uma encomenda real. É o que distingue uma avaliação recolhida de uma publicada espontaneamente.
-
disputed — a avaliação está contestada e a sua instrução está em curso. Continua apresentada (ver §4.6); assinalem-na em vez de a esconder.
reply — a resposta do comerciante faz parte da avaliação para o leitor. published_at leva aí a data da última modificação quando a houve: mostrar a data de origem sob um texto reescrito induziria em erro.
photos — vazio num comerciante cujo plano não as serve. As avaliações continuam completas, faltam apenas as imagens.
Neste canal não existe campo total: uma página vazia significa que não há mais nada para carregar. É o que faz o botão «ver mais» do widget.
GET
/api/v1/public/products/{slug}/{productId}/score
Sem autenticação
Nota de um produto. {productId} é o vosso identificador de catálogo, o transmitido em external_product_id — nunca o reescrevemos. Lembrem-se de o codificar se contiver carateres reservados.
| Parâmetro | Por omissão | Descrição |
variant | — |
Restringe a nota a uma variante. Se faltar, a nota abrange todas as variantes em conjunto — que é o comportamento certo enquanto o visitante não tiver escolhido o seu tamanho. |
with_variants | false |
Acrescenta o detalhe por variante. Custa um pedido a mais: não o ativem na ficha de produto, que é a página mais vista da loja. |
curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/score?with_variants=true'
HTTP/1.1 200 OK
{
"product": { "external_id": "REF-42", "variant_id": null },
"score": {
"average": 4.4,
"count": 27,
"distribution": { "1": 0, "2": 1, "3": 3, "4": 7, "5": 16 },
"scale": { "min": 1, "max": 5 }
},
"variants": [
{ "variant_id": "REF-42-ROUGE-L", "average": 4.8, "count": 12 },
{ "variant_id": "REF-42-BLEU-M", "average": 4.1, "count": 15 }
]
}
variants vale null quando with_variants não é pedido — é uma ausência de cálculo, não uma ausência de variantes.
GET
/api/v1/public/products/{slug}/{productId}/reviews
Sem autenticação
Avaliações de um produto. Mesma estrutura de resposta e mesmos parâmetros de paginação das avaliações do estabelecimento, mais o filtro variant — útil quando um seletor de tamanho quer mostrar apenas as avaliações da variante escolhida.
curl 'https://louis.guide/api/v1/public/products/tissufiesta/REF-42/reviews?variant=REF-42-ROUGE-L&per_page=5'
Num comerciante cujo plano não inclua a apresentação das avaliações de produto, prevejam uma apresentação que se reduza de forma limpa em vez de uma moldura vazia: o widget, por seu lado, desaparece da página.
GET
/api/v1/public/merchants/{slug}/photos
Sem autenticação
Fotografias de clientes aprovadas, sem o texto das avaliações. É o que alimenta um carrossel: sem este endpoint seria preciso carregar cinquenta avaliações completas — texto, datas, notas — para delas guardar apenas as imagens, numa ficha de produto que já carrega o tema do comerciante.
| Parâmetro | Por omissão | Descrição |
produit | — |
Restringe a um produto. Se faltar, devolve as fotografias de toda a loja — o que alimenta um carrossel de página inicial. |
limite | 24 |
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
}
]
}
Os URL são absolutos: este JSON é lido por JavaScript executado no domínio da loja, onde um URL relativo apontaria para a própria loja. Sirvam-se de width e height para reservar o espaço antes do carregamento — sem isso a ficha de produto saltará sob os olhos do visitante.
Num comerciante cujo plano não serve as fotografias, a resposta é { "photos": [] } com um estado 200, nunca um erro: o carrossel desaparece de forma limpa em vez de mostrar uma moldura falhada.
GET
/api/v1/public/merchants/{slug}/display
Sem autenticação
Definições de apresentação decididas pelo comerciante na sua área. É o que permite colocar as etiquetas de uma vez por todas num tema, e depois acender, apagar ou deslocar um elemento sem tocar no código da loja.
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/pt/m/tissufiesta"
}
| Definição | Por omissão | Significado |
badge_flottant | false |
Distintivo de nota fixado num canto do ecrã. Apagado por omissão: sobrepõe-se à página do comerciante, e nada deve aparecer no sítio dele sem que o tenha pedido. |
badge_cote | droite | droite ou gauche. |
badge_decalage | 16 | Afastamento em píxeis face à margem. |
seuil_avis | 1 |
Número de avaliações abaixo do qual a apresentação desaparece. Ver §6: mostrar «nenhuma avaliação» é pior do que não mostrar nada. |
etoiles_fiche | true | Estrelas na ficha de produto. |
etoiles_vignettes | true | Estrelas nas miniaturas das listagens. |
onglet_avis | true | Separador «Avaliações» da ficha de produto. |
bloc_accueil | true | Bloco de avaliações na página inicial. |
display está sempre completo, valores por omissão incluídos: o vosso código não tem de conhecer os nossos valores por omissão, nem de os copiar — no dia em que um mudar, ele acompanha.
accent_color vale null quando o comerciante não escolheu cor ou quando o seu plano já não o permite. Prevejam sempre uma cor de recurso do vosso lado: é o que o widget faz, cujo tom só existe se estiver declarado.
GET
/api/v1/public/health
Sem autenticação
Disponibilidade do serviço. A consultar por uma sonda ou pelo controlo de saúde de um módulo.
HTTP/1.1 200 OK
{ "status": "ok" }
Deliberadamente mínimo: nenhum acesso à base de dados, nenhuma dependência externa. Uma lentidão da base não deve desencadear um falso alerta de indisponibilidade — e inversamente, este endpoint nada diz sobre o estado da base. Para verificar que uma chave funciona, é /cms/ping que é preciso chamar.
6. Widgets de apresentação
Quatro elementos HTML a colocar num tema. Um único script a carregar, nenhuma dependência, nenhuma configuração: o endereço da API é deduzido do URL do próprio 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>
O script é carregado uma vez por página, onde se quiser; os elementos podem ser colocados antes dele. É servido em /widget/v1/: uma evolução incompatível sairia em /v2/ e este continuaria a ser servido tal e qual — vive em temas que ninguém irá atualizar.
Os quatro elementos
| Elemento | O que mostra | Onde o colocar |
<avis-score> |
Nota média, estrelas, número de avaliações. |
Ficha de produto, cabeçalho da loja, página «sobre nós». |
<avis-liste> |
Avaliações paginadas, com fotografias e respostas do comerciante. |
Separador «Avaliações» de uma ficha de produto, página dedicada. |
<avis-carrousel> |
Apenas as fotografias dos clientes, clicáveis. |
Ficha de produto, página inicial. |
<avis-flottant> |
Distintivo de nota fixado num canto, clicável. |
O modelo comum, uma única vez para todo o sítio. |
Atributos
| Atributo | Elementos | Por omissão | Função |
marchand | todos | — |
Obrigatório. Identificador público do comerciante, o da sua página pública. |
produit |
score, lista, carrossel | — |
O vosso identificador de catálogo (external_product_id). Se faltar, o elemento abrange toda a loja. |
langue | todos | lang da página |
Idioma das etiquetas. Na falta dele, o atributo lang do documento — que o tema já preenche — e depois o francês. Só fr e en estão realmente traduzidos; qualquer outro valor recai no francês em vez de mostrar etiquetas traduzidas a meio. |
mini | score | 1 |
Número de avaliações abaixo do qual o elemento desaparece. Em 3, uma ficha que só tem duas avaliações não mostra nada em vez de uma nota fundada em quase nada. |
par-page | liste | 5 |
Avaliações carregadas de cada vez; um botão «Ver mais» carrega o resto. |
max | carrousel | 12 |
Número de fotografias, 50 no máximo. |
Exemplo completo numa ficha de produto
<!-- 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>
Um widget que não mostra nada não está necessariamente avariado
Três situações fazem desaparecer um elemento, e de cada vez é intencional: nenhuma avaliação (ou menos do que o limiar), nenhuma fotografia para o carrossel, e qualquer erro de rede ou de servidor.
Mostrar «Ainda sem avaliações» numa ficha de produto é pior do que não mostrar nada: o visitante conclui que ninguém encomendou. E uma faixa de erro na loja de um comerciante porque a nossa API tosse seria indefensável — o elemento retira-se da maquetagem, a ficha de produto fica intacta.
Consequência prática para o integrador: não construam uma maquetagem que reserve uma altura fixa para um widget. Ele pode não ocupar nada.
O que o comerciante comanda sem vós
Os elementos leem /display no carregamento. Duas definições vêm daí em vez de um atributo, e isso é deliberado: o comerciante pode colocar as suas etiquetas de uma vez por todas e depois mudar de ideias a partir da sua área sem reabrir o tema.
-
O distintivo flutuante — aceso ou apagado, à direita ou à esquerda, com o seu afastamento.
<avis-flottant> colocado no modelo não mostra nada enquanto o comerciante não o tiver ativado. Está apagado por omissão: nada deve aparecer no sítio dele sem que o tenha pedido.
-
A cor de marca — aplicada às superfícies que o admitem. As estrelas mantêm o seu âmbar, tal como o verde de «Compra verificada» e o âmbar de «Contestada»: essas cores transportam um sentido, não decoram, e repintá-las tornaria a nota ilegível num comerciante cuja marca seja amarelo-pálido ou branca.
As definições são pedidas uma única vez por página, mesmo com quatro elementos: o pedido em curso é partilhado. É isso que evita que o widget seja o script que atrasa a ficha de produto — crítica fundada que se pode fazer à maioria dos módulos de avaliações.
Isolamento face ao tema
Cada elemento apresenta o seu conteúdo num shadow DOM: o CSS do tema não transborda para o widget, e o do widget não transborda para a loja. Nenhum dos dois seria aceitável no sentido contrário.
Corolário a conhecer antes de tentar: as vossas regras CSS não alcançarão o interior dos widgets. A única personalização prevista é a cor de marca, definida na área do comerciante. Uma apresentação realmente à medida passa pela API pública — é exatamente para isso que está documentada.
Antes de colar seja o que for
Em PrestaShop e WooCommerce, o módulo coloca ele próprio estas etiquetas, no sítio certo do tema. A colagem manual destina-se às outras plataformas e aos temas à medida — ver os módulos.
7. Servidor MCP
MCP (Model Context Protocol) expõe as mesmas capacidades da API, sob uma forma que um assistente de IA pode descobrir sozinho. Onde um programador lê uma documentação, escreve a autenticação e interpreta o JSON, o assistente pede a lista das ferramentas, lê as suas descrições e chama-as.
Em concreto: o comerciante liga o seu assistente a este servidor e depois escreve «que avaliações ainda não têm resposta?» ou «responde a esta pedindo desculpa pelo atraso». Ninguém escreveu código de integração.
| Valor |
| Endereço | https://louis.guide/api/v1/mcp |
| Transporte | JSON-RPC 2.0 sobre HTTP, em POST |
| Versão do protocolo | 2024-11-05 |
| Servidor anunciado | avis-clients, versão 1.0.0 |
| Capacidades | tools — nem recursos, nem prompts |
| Autenticação |
Token portador (§7.1) ou chave de API + assinatura HMAC (§7.2) |
| Plano | Pago — caso contrário, erro JSON-RPC -32001 |
7.1 Ligar um assistente: o token
É a via normal, e a única que não exige nada instalado. O comerciante cria um token na sua área — Definições · Recolha, secção «Ligar um assistente» — e depois cola-o na configuração do seu assistente juntamente com o endereço do servidor.
POST https://louis.guide/api/v1/mcp
Authorization: Bearer mcp_live_…
Content-Type: application/json
Forma habitual dos ficheiros de configuração de um cliente MCP:
{
"mcpServers": {
"avis-clients": {
"url": "https://louis.guide/api/v1/mcp",
"headers": { "Authorization": "Bearer mcp_live_…" }
}
}
}
Verificação num único comando, antes de ligar seja o que for:
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"}'
O que o token pode e o que não pode
| Propriedade | Comportamento |
| Alcance |
Unicamente /api/v1/mcp. Apresentado na API CMS, nem sequer é examinado: nem transmissão de encomendas, nem exportação de avaliações, nem ligação. |
| Escrita |
Proibida por omissão. O comerciante assinala explicitamente «autorizar a redação de respostas» na criação. Sem isso, a ferramenta repondre_a_un_avis nem sequer aparece em tools/list — o assistente não a proporá, portanto. |
| Tempo de vida |
Um ano, depois deixa de valer. Volta a criar-se em dez segundos. |
| Revogação |
Imediata e definitiva, token a token, sem tocar nas chaves de API nem nos módulos do comerciante. |
| Conservação |
Mostrado uma só vez. Só guardamos uma impressão digital: ninguém o pode voltar a mostrar, nós incluídos. |
| Número |
Três tokens válidos no máximo por conta. |
Um token portador viaja: tratem-no como uma palavra-passe
Ao contrário do segredo HMAC, parte em cada pedido e vive na configuração de um serviço que não controlamos. É o preço da ligação direta, e é por isso que é compartimentado, expira, é revogável e mudo na escrita por omissão. Nunca o ponham num URL nem num repositório de código: os URL acabam nos registos de todos os intermediários atravessados.
As tentativas estão limitadas a 20 falhas por quarto de hora e por endereço IP — para além disso, a resposta é um 429.
7.2 Alternativa: chave de API e assinatura HMAC
O mesmo endpoint aceita a autenticação descrita em §2: chave de API e assinatura HMAC. Tem uma vantagem real — o segredo nunca sai do servidor do comerciante — e um inconveniente que a reserva aos integradores: nenhum cliente MCP sabe recalcular um HMAC a cada chamada, apenas colocam cabeçalhos fixos.
É preciso portanto uma ponte: um pequeno programa lançado pelo assistente, que recebe o JSON-RPC na sua entrada padrão, o assina, o envia e devolve a resposta. Node.js 18 ou mais recente, sem dependências. Guardem-no 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');
}
}
});
Declaração do lado do cliente MCP (forma habitual dos ficheiros de configuração):
{
"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"
}
}
}
}
O segredo não sai da máquina. Serve para assinar localmente; o que parte pela rede é a assinatura. Um caminho absoluto é indispensável: o assistente não lança o programa a partir da pasta onde o escreveram.
Para verificar a ponte antes de ligar seja o que for, enviem-lhe uma linha à mão. Deve voltar uma lista de ferramentas:
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étodo | Efeito |
initialize |
Anuncia a versão do protocolo, as capacidades e a identidade do servidor. |
tools/list | Catálogo das ferramentas e dos seus esquemas de entrada. |
tools/call | Executa uma ferramenta — params.name e params.arguments. |
notifications/initialized, ping | Confirmados com um resultado vazio. |
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 As três ferramentas
Duas leem, uma escreve. A linha de separação não é técnica: ler avaliações não apresenta qualquer risco, ao passo que publicar uma resposta faz o comerciante falar em público numa página que alojamos — uma formulação infeliz numa avaliação sensível, e é uma captura de ecrã que circula.
É por isso que tools/list devolve apenas duas ferramentas quando quem chama apresenta um token só de leitura. Não fixem portanto a lista no código: peçam-na, e anunciem ao comerciante apenas o que ela contém.
ferramenta
lister_avis
Avaliações publicadas sobre o estabelecimento, da mais recente à mais antiga. A ferramenta que o assistente chama para «mostra-me os clientes insatisfeitos» ou «o que ainda não tem resposta?».
| Argumento | Tipo | Por omissão | Efeito |
note_max | inteiro 1–5 | — |
Só devolve as avaliações cuja nota seja inferior ou igual. |
sans_reponse | booleano | false |
Afasta as avaliações às quais já foi publicada uma resposta. |
limite | inteiro 1–50 | 20 |
Número de avaliações lidas. |
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "lister_avis",
"arguments": { "note_max": 3, "sans_reponse": true, "limite": 10 }
}
}
O resultado é um bloco de texto que contém JSON — é a forma que o protocolo prevê para um resultado estruturado, e a que os assistentes sabem ler:
{
"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 depois do limite, não antes. Pedir 20 avaliações sem resposta lê as últimas 20 avaliações publicadas e retira depois as já tratadas: o resultado pode conter muitas menos, e total di-lo. Aumentem limite para alargar a janela de leitura.
Aqui só surgem as avaliações publicadas: nem as pendentes, nem as recusadas, nem as retiradas. Para essas existe GET /cms/reviews com o seu filtro status.
ferramenta
resume_reputation
Visão de conjunto, sem argumento. O que o assistente chama para «como está a minha reputação?».
{
"boutique": "Tissu Fiesta",
"note_moyenne": 4.6,
"avis_publies": 318,
"repartition": { "1": 4, "2": 6, "3": 18, "4": 91, "5": 199 },
"avis_produit": 127
}
avis_publies conta as avaliações sobre o estabelecimento; avis_produit conta em separado as que dizem respeito a um artigo. Somá-las daria um total que não corresponde a nenhuma nota apresentada.
ferramenta
repondre_a_un_avis
Escrita pública
| Argumento | Tipo | Obrig. | Efeito |
avis_id | cadeia | sim | Identificador da avaliação, tal como devolvido por lister_avis. |
contenu | cadeia | sim |
Texto da resposta, truncado a 3000 carateres. |
{
"avis_id": "9f1c2b3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
"publiee": false,
"message": "Resposta guardada como rascunho. O comerciante tem de a validar na sua área antes de ela aparecer."
}
Publicada ou em rascunho: foi o comerciante que decidiu, não a chamada
O comerciante define na sua área se as respostas redigidas por um assistente partem diretamente ou aguardam a sua releitura. Nenhum dos dois comportamentos é correto em absoluto: quem recebe duas avaliações por semana quer reler, quem recebe duzentas quer que sigam.
Essa definição não é um parâmetro do pedido, e isso é essencial: se o assistente pudesse escolher por si próprio se deve ser relido, a garantia deixaria de valer alguma coisa. O campo publiee e o campo message dizem o que realmente aconteceu — um assistente deve comunicá-lo tal e qual ao comerciante.
Uma avaliação já respondida vê a sua resposta substituída. Uma resposta já pública continua a sê-lo: uma reescrita nunca a devolve a rascunho, o que a faria desaparecer da página sem decisão de ninguém.
O que um assistente deve saber antes de redigir
-
Responder na língua da avaliação — o campo
langue existe para isso. Uma resposta em francês sob uma avaliação em neerlandês diz ao leitor que ela não foi lida.
-
Nunca prometer um gesto comercial que não se possa honrar: reembolso, troca, desconto. Esta resposta é pública e oponível ao comerciante.
-
Nenhuma ferramenta modifica nem elimina uma avaliação, e não haverá nenhuma. Um assistente a quem se peça para «mandar retirar» uma avaliação só a pode denunciar, com um motivo admissível (§4.6) — a nota não é um deles.
7.5 Erros
Sempre um estado HTTP 200, incluindo em caso de erro: em JSON-RPC o erro viaja no corpo. Um 4xx faria o cliente crer que o transporte falhou, e a maioria voltaria a tentar em vez de mostrar a mensagem.
A única exceção: a autenticação, recusada antes de chegar à camada JSON-RPC. Responde com o envelope de erro habitual da API.
| Estado | Código | Causa |
| 401 | invalid_mcp_token |
Token desconhecido, revogado ou expirado — indistinguíveis de propósito. O comerciante cria um novo na sua área. |
| 401 | códigos de §2 |
Via HMAC: chave ausente, assinatura ou marca temporal recusadas. |
| 429 | too_many_attempts |
Mais de 20 falhas de autenticação em quinze minutos a partir do mesmo endereço. Aguardem em vez de repetir em ciclo. |
| Código | Significado | A fazer |
-32001 |
O plano do comerciante não inclui o acesso MCP. |
Passar a um plano pago; as avaliações continuam publicamente legíveis. |
-32601 | Método JSON-RPC desconhecido. | Verificar method. |
-32602 | Ferramenta desconhecida. | Chamar tools/list, não fixar os nomes no código. |
-32603 |
Erro da ponte local — rede, segredo ausente. |
Este código vem da ponte acima, não do servidor. |
Os erros de negócio de uma ferramenta não são erros JSON-RPC: a resposta continua a ser um resultado, com isError: true e um objeto { "erreur": "…" } no texto. É o caso de uma avaliação não encontrada, de um conteúdo vazio, ou de uma resposta tentada com um token só de leitura. O assistente pode assim explicá-lo ao comerciante em vez de anunciar uma avaria.
8. Webhooks recebidos
Não existe webhook de saída
A plataforma não vos chama: não emite qualquer notificação para o vosso servidor aquando da publicação de uma avaliação, de uma resposta ou de uma decisão de moderação. Para acompanhar a atividade, consultem GET /api/v1/cms/reviews ao vosso ritmo, filtrando por status=published e comparando published_at com a vossa última passagem.
Uma passagem de hora a hora convém à quase totalidade dos usos: as avaliações não chegam ao segundo, e o ritmo de publicação de uma loja conta-se em unidades por dia. Consultar de minuto a minuto não fará aparecer nada mais depressa.
Os dois endpoints abaixo existem para chamadores precisos — o nosso operador de SMS e o nosso prestador de pagamentos. Nenhum integrador tem de os chamar, e nenhum o pode fazer: ambos estão fechados por um segredo que não é distribuído.
POST
/api/v1/stripe/webhook
Assinatura Stripe
Recebe os acontecimentos de subscrição: checkout.session.completed, customer.subscription.created, .updated e .deleted. É o que faz uma conta passar ao plano pago, e portanto o que abre a API de avaliações e o acesso MCP.
A assinatura da carga útil é a única coisa que protege esta rota: sem ela, qualquer pessoa poderia enviar «subscrição ativa» e oferecer-se o plano pago com um único pedido curl. É verificada antes de qualquer leitura do conteúdo, e um segredo ausente faz o pedido falhar em vez de o deixar passar.
Os acontecimentos não tratados são confirmados com um 200 ({ "ignored": … }): a Stripe considera qualquer resposta que não seja 2xx como uma falha e repete durante três dias, com intervalos crescentes. Responder 404 a um tipo de acontecimento que não nos serve provocaria milhares de reenvios inúteis, e depois a desativação do ponto terminal do lado deles. Inversamente, uma verdadeira falha de tratamento responde de facto 500 — aí queremos que a Stripe repita em vez de deixar em plano gratuito um comerciante que pagou.
POST
/api/v1/sms/inbound/{token}
Token partilhado
Recebe os SMS de entrada, ou seja os «STOP». O operador trata a palavra-chave do seu lado e deixa de entregar — mas sem este endpoint nada saberíamos: continuaríamos a enviar-lhe mensagens faturadas e nunca recebidas, a oposição desapareceria no dia de uma mudança de operador, e não poderíamos provar tê-la honrado, embora o ónus da prova nos caiba.
O token viaja no caminho, o que é mais fraco do que uma assinatura — mas é o que as interfaces dos operadores franceses sabem configurar. Daí que este endpoint não possa fazer outra coisa senão acrescentar uma oposição: o pior que uma chamada fraudulenta produz é impedir o envio de SMS para um número. Incómodo, nunca perigoso, e reversível a partir do back office.
A palavra-chave é procurada como primeira palavra da mensagem, não em qualquer sítio dentro dela: quem escreve «isto tem de parar, aquela loja é péssima» não está a pedir para ser retirado, e retirá-lo mesmo assim tirar-lhe-ia o canal por onde é legitimamente contactado. A oposição é registada para todos os comerciantes: a mensagem recebida não diz de que loja se trata — a pessoa responde ao número de envio — e adivinhar seria ao mesmo tempo falso e perigoso.
Só corta o canal SMS. O correio continua a sair: é ele que leva a ligação de gestão da avaliação e as menções obrigatórias, e uma oposição manifestada num canal não vale para o outro.
9. Limites de débito
Os limites são calculados numa janela deslizante: sem contador que volte a zero à hora certa, e portanto sem rajada possível no início de um período.
| Canal | Limite | Chave | Porquê este número |
API pública /api/v1/public/ |
60 / minuto |
Endereço IP |
Uma ficha de produto faz duas chamadas: isso deixa 30 carregamentos por minuto a partir do mesmo endereço. Amplo para um visitante, apertado para um aspirador de conteúdos. |
Disponibilidade /public/health |
nenhum |
— |
Excluído de propósito: é consultado continuamente pela supervisão, e limitá-lo faria subir falsos alertas de indisponibilidade. |
Abertura de ligação POST /pairing |
10 / hora |
Endereço IP |
Cada chamada cria uma linha na base de dados sem qualquer autenticação. Dez chegam largamente a um integrador que recomeça. |
Sondagem POST /pairing/{code} |
240 / 15 minutos |
Endereço IP |
Generoso de propósito: o módulo consulta de cinco em cinco segundos enquanto o comerciante cria a conta, confirma o endereço e valida. |
| Autenticação MCP por token |
20 falhas / 15 minutos |
Endereço IP |
Conta apenas as falhas: uma ligação que funciona nunca lhe toca. Trava a varredura de tokens encontrados noutro lado e evita que um cliente mal configurado afogue os registos. |
| Submissão de uma avaliação |
10 / minuto |
Token do convite |
Por token e não por IP: vários clientes de uma mesma empresa partilham muitas vezes um único endereço de saída, e limitá-los em conjunto puniria submissões legítimas. |
| Denúncia pública de uma avaliação |
5 / hora |
Endereço IP |
Aberta a qualquer leitor (obrigação DSA), logo a qualquer robô. Cada envio cria uma linha na fila de moderação. |
A API CMS não é limitada, o que não autoriza tudo
Hoje não se aplica qualquer limite de débito aos endpoints assinados (/api/v1/cms/ e MCP): estão autenticados, e o volume real é balizado pela quota de solicitações do comerciante. Trate mesmo assim o 429 — poderá ser acrescentado um limite, e uma integração que não o saiba ler avariará no dia em que ele aparecer.
Na prática: transmita as encomendas à medida que surgem em vez de em lotes noturnos de vários milhares, e consulte as avaliações de hora a hora em vez de minuto a minuto (§8). Um volume anómalo é visível do nosso lado e desencadeia um contacto, não um corte silencioso.
O que devolve uma ultrapassagem
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Remaining: 0
{
"error": {
"code": "rate_limit_exceeded",
"message": "Demasiados pedidos. Tente novamente daqui a alguns instantes."
}
}
Retry-After dá o número de segundos a aguardar. Respeite-o: repetir de imediato apenas consome a janela seguinte. Uma espera exponencial, limitada a um minuto, chega para todos os casos aqui descritos.
A sondagem de ligação é a exceção e responde { "status": "rate_limited" }: é o mesmo acontecimento, expresso no vocabulário de um endpoint que o módulo consulta em ciclo.
10. Códigos de erro comuns
Dois formatos, e só um a tratar na maioria dos casos
Em toda a API, um erro leva o mesmo envelope:
{
"error": {
"code": "invalid_api_key",
"message": "Chave de API desconhecida, revogada ou expirada."
}
}
O code é estável e destina-se ao vosso programa; a message destina-se ao humano que depura e pode ser reformulada sem aviso. Nunca construam a vossa lógica sobre o texto da mensagem.
Uma única exceção: POST /cms/orders, servido por uma camada diferente, devolve os seus erros de validação sob a forma de uma lista de violations. Um cliente robusto lê pois error.code se existir, e recorre a violations caso contrário.
Estados HTTP
| Estado | Sentido | Repetir? |
| 200 | Sucesso. Em JSON-RPC, o eventual erro está no corpo. | — |
| 201 | Criado — encomenda registada, resposta publicada. | — |
| 202 | Aceite mas não decidido: a denúncia entra em fila. | — |
| 400 | Pedido ilegível. | Não, corrija. |
| 401 | Chave ausente, inválida, ou assinatura recusada. | Não, salvo relógio a ressincronizar. |
| 402 | O plano do comerciante não inclui esta função. | Não. |
| 404 | Recurso desconhecido — ou fora da vossa conta. | Não. |
| 409 | Conflito: a ação já foi realizada. | Não, é um estado, não uma avaria. |
| 415 | Content-Type ausente ou inesperado. | Não, envie JSON. |
| 422 | Pedido bem formado mas recusado: campo em falta, valor fora dos limites. | Não, corrija. |
| 429 | Débito ultrapassado. | Sim, após Retry-After. |
| 5xx | Incidente do nosso lado. | Sim, com espera crescente. |
Resumo dos códigos
| Código | Estado | Onde | Causa e solução |
missing_api_key | 401 | CMS, MCP |
Cabeçalho X-Api-Key ausente. |
invalid_api_key | 401 | CMS, MCP |
Chave desconhecida, revogada ou expirada — as três deliberadamente indistinguíveis. Verifique-a na área do comerciante. |
missing_signature, missing_timestamp |
401 | CMS, MCP |
Escrita não assinada. Ver §2.1. |
invalid_timestamp | 401 | CMS, MCP |
X-Timestamp não é uma marca temporal Unix em segundos — milissegundos ou uma data ISO, na maior parte das vezes. |
timestamp_out_of_range | 401 | CMS, MCP |
Mais de 300 s de desvio. A mensagem dá o valor exato: sincronize o relógio (NTP). |
signature_mismatch | 401 | CMS, MCP |
Percorra as quatro armadilhas do §2.3, por ordem. |
invalid_mcp_token | 401 | MCP |
Token portador desconhecido, revogado ou expirado — indistinguíveis. O comerciante cria um novo a partir da sua área (§7.1). |
too_many_attempts | 429 | MCP |
Demasiadas falhas de autenticação a partir do mesmo endereço. |
plan_required | 402 | Avaliações, resposta |
Função incluída a partir do plano pago. A apresentação pública das avaliações continua gratuita. |
merchant_not_found | 404 | API pública |
Identificador público desconhecido. Verifique o slug, não o nome comercial. |
review_not_found | 404 | Resposta, denúncia |
Identificador desconhecido, mal formado, ou pertencente a outro comerciante: a compartimentação impõe não os distinguir. |
already_reported | 409 | Denúncia |
Já existe um processo aberto sobre esta avaliação. |
content_required | 422 | Resposta |
content ausente ou vazio após limpeza. |
invalid_reason | 422 | Denúncia |
Motivo fora da lista. Uma nota baixa não é um motivo admissível (§4.6). |
invalid_request | 422 | Ligação |
shop_domain ausente ou inutilizável. |
rate_limit_exceeded | 429 | API pública |
Ver §9 e o cabeçalho Retry-After. |
-32001 | 200 | MCP |
Plano sem acesso MCP (um erro JSON-RPC, não HTTP). |
-32601, -32602 | 200 | MCP |
Método ou ferramenta desconhecidos. Passe por tools/list. |
Três sintomas, e por onde começar
| Sintoma | Causa mais frequente |
| «Ontem funcionava tudo, hoje está tudo em 401.» |
O relógio do servidor derivou. GET /cms/ping devolve server_time: compare-o com o vosso antes de procurar noutro lado. |
| «O ping passa, mas todas as minhas escritas falham.» |
A chave está boa, a assinatura não — que é justamente o que essa divisão de regimes permite concluir. O corpo foi quase sempre recodificado depois de assinado (§2.3). |
| «O widget não mostra nada, mas a API responde 200 na consola.» |
Domínio não declarado do lado do comerciante: o navegador bloqueia a leitura por falta de cabeçalho CORS. Ou, simplesmente, ainda não há avaliações: um widget vazio retira-se da página (§6). |
Se nada disto encaixar
Escreva-nos a partir da área do comerciante juntando três coisas: o caminho chamado, a marca temporal do pedido e o código de erro recebido. Com esses três elementos o pedido encontra-se nos registos; sem eles, a única resposta possível é pedir-vos que os forneçam.
↑ Voltar ao início