Ir para o conteúdo

Documentação da API

Tudo o que a plataforma expõe: transmissão das encomendas, leitura e resposta às avaliações, apresentação pública e ligação de um assistente de IA. Dezanove endpoints, três canais, uma só chave.

Um comerciante normalmente não tem nada para programar. Os módulos PrestaShop e WooCommerce fazem tudo o que aqui é descrito — ver os módulos. Esta página destina-se aos programadores que integram uma plataforma sem módulo, um ERP, um CRM ou uma ferramenta interna.

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çãoEndereço
API (todos os canais)https://louis.guide
Área do comerciantehttps://louis.guide/app
Script dos widgetshttps://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

  1. Crie uma conta de comerciante em a área do comerciante.
  2. Confirme o endereço de correio eletrónico e abra em seguida a secção das chaves de API.
  3. 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 HTTPCabeçalhos exigidosPorquê
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çalhoConteú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çoRegra 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.

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

CampoTipoObrigatórioDescrição
shop_domaincadeiasim Domínio da loja, p. ex. loja.exemplo.pt.
platformcadeianão prestashop, woocommerce, custom… unknown por omissão.
shop_namecadeianão Nome legível da loja, reutilizado na criação da conta.
platform_versioncadeianão Versão da plataforma, p. ex. 8.1.6.
plugin_versioncadeianão Versão do módulo que chama.
shop_uidcadeianã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

EstadoCódigoCausa
422invalid_requestshop_domain ausente ou inutilizável.
429rate_limitedMais 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.

CampoTipoObrigatórioDescrição
poll_tokencadeiasim 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

ValorSignificadoQue fazer
pendingO comerciante ainda não decidiu.Continuar a sondar.
approvedValidado. A resposta leva as credenciais.Guardá-las, parar a sondagem.
rejectedO comerciante recusou.Parar e comunicar-lho.
expiredQuinze 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_limitedDemasiadas 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.

CapacidadeO 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

CampoTipoObrig.Descrição
external_order_idcadeia (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.
customerobjetosim Identidade do cliente a solicitar — ver a tabela seguinte.
experienced_atISO 8601sim Data de entrega ou de consumo, com fuso explícito. Ver a caixa abaixo: não é a data da encomenda.
sourceobjetosim Contexto técnico do envio — ver mais abaixo.
itemsarray (200 máx.)não Artigos. Sem eles, não será pedida qualquer avaliação de produto — apenas a do estabelecimento.
amountcadeia decimalnão Montante total, p. ex. "129.90". Nunca um número de vírgula flutuante.
currencyISO 4217não "EUR", "CHF"…
channelenumeraçãonão ecommerce_order (por omissão), pos_transaction, qr_scan, manual, csv_import, nfc.
location_idcadeia (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_daysinteiro 0–365nã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_idcadeia (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_labelcadeia (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

CampoTipoObrig.Descrição
emailcorreio (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.
countryISO 3166-1 alpha-2nã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.
localefr, en, nl, de, it, esnã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.
phonecadeia (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_namecadeia (100)não Personalização da solicitação e nome apresentado do autor.
companycadeia (255)não Denominação social, para uma encomenda profissional.
postal_code, citycadeianã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.

CampoTipoObrig.Descrição
platformcadeia (50)sim prestashop, woocommerce, shopify, magento, custom…
platform_versioncadeia (30)não P. ex. 8.1.6.
plugin_versioncadeia (30)não Versão da vossa integração. A incrementar a cada entrega.
triggercadeia (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_uidcadeia (80)não* *Decisivo com várias lojas. Identificador gerado uma vez na instalação e conservado. Ver a caixa.
shop_idcadeia (50)não Identificador de loja na plataforma. Serve de recurso quando shop_uid está ausente.
shop_namecadeia (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_idcadeianã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

CampoTipoObrig.Descrição
external_product_idcadeia (100)sim Identificador do produto no vosso catálogo.
namecadeia (255)sim Nome do produto tal como apresentado ao cliente.
variant_idcadeia (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_labelcadeia (255)não Etiqueta legível: «Cor: vermelho, Tamanho: L».
gtinde 8 a 14 algarismosnã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, mpncadeianã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, brandcadeianão Referência interna e marca.
category_id, category_namecadeianão Categoria principal no vosso catálogo.
product_url, image_urlURL (500)não Usadas no correio de solicitação: uma imagem de produto melhora nitidamente a taxa de submissão.
imageslista de URL (10 máx.)não Imagens suplementares.
descriptioncadeia (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.
tagslista (30 máx.)não Palavras-chave do produto, 60 carateres cada uma.
meta_title, meta_descriptioncadeianão Metadados da ficha.
quantityinteiro > 0não 1 por omissão.
unit_pricecadeia decimalnã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

ValorSignificado
pendingRecebida, a aguardar agendamento.
scheduledSolicitação agendada.
solicitedPedido de avaliação enviado ao cliente.
reviewedO cliente submeteu a sua avaliação.
cancelledAnulada antes do envio.
expiredPrazo 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."
    }
  ]
}
EstadoCausaA 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âmetroPor omissãoDescrição
typemerchant merchant para as avaliações do estabelecimento, product para as avaliações de produto.
statustodos 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.
page1Número de página.
per_page25 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.

EstadoCódigoCausa
402plan_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.

CampoTipoObrig.Descrição
contentcadeiasim 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.

EstadoCódigoCausa
402plan_requiredPlano sem resposta às avaliações.
404review_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.
422content_requiredcontent 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.

CampoTipoObrig.Descrição
reasonenumeraçãosim Motivo, a escolher na lista abaixo.
detailcadeianã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

ValorQuando o invocar
inappropriate_contentInjúria, discurso de ódio, conteúdo ilícito.
spam_or_advertisingPublicidade, ligação comercial, conteúdo automatizado.
off_topicSem relação com a experiência vivida — a transportadora, o tempo.
conflict_of_interestConcorrente, antigo empregado, avaliação remunerada.
personal_data_disclosureA 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.

EstadoCódigoCausa
404review_not_foundIdentificador desconhecido, mal formado, ou fora da vossa conta.
409already_reportedJá existe uma denúncia aberta sobre esta avaliação.
422invalid_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âmetroPor omissãoDescrição
page1Número de página.
per_page10 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âmetroPor omissãoDescriçã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_variantsfalse 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âmetroPor omissãoDescrição
produit— Restringe a um produto. Se faltar, devolve as fotografias de toda a loja — o que alimenta um carrossel de página inicial.
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
    }
  ]
}

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çãoPor omissãoSignificado
badge_flottantfalse 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_cotedroitedroite ou gauche.
badge_decalage16Afastamento em píxeis face à margem.
seuil_avis1 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_fichetrueEstrelas na ficha de produto.
etoiles_vignettestrueEstrelas nas miniaturas das listagens.
onglet_avistrueSeparador «Avaliações» da ficha de produto.
bloc_accueiltrueBloco 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

ElementoO que mostraOnde 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

AtributoElementosPor omissãoFunção
marchandtodos— 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.
languetodoslang 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.
miniscore1 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-pageliste5 Avaliações carregadas de cada vez; um botão «Ver mais» carrega o resto.
maxcarrousel12 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çohttps://louis.guide/api/v1/mcp
TransporteJSON-RPC 2.0 sobre HTTP, em POST
Versão do protocolo2024-11-05
Servidor anunciadoavis-clients, versão 1.0.0
Capacidadestools — nem recursos, nem prompts
Autenticação Token portador (§7.1) ou chave de API + assinatura HMAC (§7.2)
PlanoPago — 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

PropriedadeComportamento
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étodoEfeito
initialize Anuncia a versão do protocolo, as capacidades e a identidade do servidor.
tools/listCatálogo das ferramentas e dos seus esquemas de entrada.
tools/callExecuta uma ferramenta — params.name e params.arguments.
notifications/initialized, pingConfirmados 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?».

ArgumentoTipoPor omissãoEfeito
note_maxinteiro 1–5— Só devolve as avaliações cuja nota seja inferior ou igual.
sans_reponsebooleanofalse Afasta as avaliações às quais já foi publicada uma resposta.
limiteinteiro 1–5020 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
ArgumentoTipoObrig.Efeito
avis_idcadeiasimIdentificador da avaliação, tal como devolvido por lister_avis.
contenucadeiasim 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.

EstadoCódigoCausa
401invalid_mcp_token Token desconhecido, revogado ou expirado — indistinguíveis de propósito. O comerciante cria um novo na sua área.
401códigos de §2 Via HMAC: chave ausente, assinatura ou marca temporal recusadas.
429too_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ódigoSignificadoA fazer
-32001 O plano do comerciante não inclui o acesso MCP. Passar a um plano pago; as avaliações continuam publicamente legíveis.
-32601Método JSON-RPC desconhecido.Verificar method.
-32602Ferramenta 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.

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

EstadoSentidoRepetir?
200Sucesso. Em JSON-RPC, o eventual erro está no corpo.—
201Criado — encomenda registada, resposta publicada.—
202Aceite mas não decidido: a denúncia entra em fila.—
400Pedido ilegível.Não, corrija.
401Chave ausente, inválida, ou assinatura recusada.Não, salvo relógio a ressincronizar.
402O plano do comerciante não inclui esta função.Não.
404Recurso desconhecido — ou fora da vossa conta.Não.
409Conflito: a ação já foi realizada.Não, é um estado, não uma avaria.
415Content-Type ausente ou inesperado.Não, envie JSON.
422Pedido bem formado mas recusado: campo em falta, valor fora dos limites.Não, corrija.
429Débito ultrapassado.Sim, após Retry-After.
5xxIncidente do nosso lado.Sim, com espera crescente.

Resumo dos códigos

CódigoEstadoOndeCausa e solução
missing_api_key401CMS, MCP Cabeçalho X-Api-Key ausente.
invalid_api_key401CMS, MCP Chave desconhecida, revogada ou expirada — as três deliberadamente indistinguíveis. Verifique-a na área do comerciante.
missing_signature, missing_timestamp 401CMS, MCP Escrita não assinada. Ver §2.1.
invalid_timestamp401CMS, MCP X-Timestamp não é uma marca temporal Unix em segundos — milissegundos ou uma data ISO, na maior parte das vezes.
timestamp_out_of_range401CMS, MCP Mais de 300 s de desvio. A mensagem dá o valor exato: sincronize o relógio (NTP).
signature_mismatch401CMS, MCP Percorra as quatro armadilhas do §2.3, por ordem.
invalid_mcp_token401MCP Token portador desconhecido, revogado ou expirado — indistinguíveis. O comerciante cria um novo a partir da sua área (§7.1).
too_many_attempts429MCP Demasiadas falhas de autenticação a partir do mesmo endereço.
plan_required402Avaliações, resposta Função incluída a partir do plano pago. A apresentação pública das avaliações continua gratuita.
merchant_not_found404API pública Identificador público desconhecido. Verifique o slug, não o nome comercial.
review_not_found404Resposta, denúncia Identificador desconhecido, mal formado, ou pertencente a outro comerciante: a compartimentação impõe não os distinguir.
already_reported409Denúncia Já existe um processo aberto sobre esta avaliação.
content_required422Resposta content ausente ou vazio após limpeza.
invalid_reason422Denúncia Motivo fora da lista. Uma nota baixa não é um motivo admissível (§4.6).
invalid_request422Ligação shop_domain ausente ou inutilizável.
rate_limit_exceeded429API pública Ver §9 e o cabeçalho Retry-After.
-32001200MCP Plano sem acesso MCP (um erro JSON-RPC, não HTTP).
-32601, -32602200MCP Método ou ferramenta desconhecidos. Passe por tools/list.

Três sintomas, e por onde começar

SintomaCausa 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