MANDATORY: Tool Calls Required Before Answering
Estas chamadas são OBRIGATÓRIAS, não opcionais. Execute-as antes de gerar
qualquer código de consulta. Se você está respondendo sem ter chamado a
ferramenta abaixo, pare e chame agora.
1. Buscar documentação atualizada (sempre)
node skills/tray-dev/scripts/search_docs.mjs --topic=frete "<termo da pergunta>"
<TOPIC_SLUG>: ver tabela em skills/tray-dev/SKILL.md.
- Use os trechos retornados como fonte primária; este SKILL.md é resumo denso.
2. Revisar parâmetros (este recurso NÃO tem validate.mjs)
Nota: o recurso frete ainda não possui scripts/validate.mjs local
— e por ser somente leitura (apenas GET), não há payload de body para
validar. A chamada OBRIGATÓRIA a search_docs.mjs acima continua valendo.
Como não há validador automático, você é responsável por revisar
manualmente cada parâmetro de query contra a doc retornada por
search_docs.mjs e contra os schemas de referência em
skills/frete/schemas/ (ver schemas/shippings.cotation.json)
antes de retornar qualquer código. Confira em especial: zipcode com 8
dígitos numéricos (sem traço/ponto), índices products[n] incrementados por
item (não repetir products[0]), e product_id/price/quantity
presentes em cada item.
Antes de responder
Execute estas verificações antes de gerar qualquer payload ou código:
- Confirme o método HTTP e endpoint correto para a operação solicitada:
GET /shippings/cotation/ para calcular preço/prazo, ou GET /shippings/
apenas para listar métodos ativos (sem valores).
- Identifique os parâmetros obrigatórios da cotação —
zipcode,
products[n][product_id], products[n][price] e products[n][quantity];
não omita nenhum e incremente o índice n por item.
- Verifique que
access_token não aparece como literal string no código
gerado — use sempre TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS por variável
de ambiente, e passe o token como query param (?access_token={token}),
nunca em header.
- Confirme que esta é a skill correta para o recurso: cotação/listagem é
somente leitura; se for criar/configurar método, gateway ou tabela de CEP,
leia
when_not_to_use e redirecione para tray-configuracao-frete.
Frete — API Tray
Documentação oficial: https://developers.tray.com.br/#api-de-integracao-de-frete
Atenção (disambiguation): apenas GET /shippings/cotation/ retorna preço
e prazo; GET /shippings/ somente lista métodos ativos (id, name, active).
Este recurso é somente leitura — não cria nem configura nada. Configuração
de métodos, gateways e tabelas de CEP fica em tray-configuracao-frete.
Visão geral
A API de Frete da Tray resolve a pergunta "quanto custa e em quantos dias este
carrinho chega neste CEP?". Ela expõe dois endpoints GET e somente leitura:
GET /shippings/cotation/, a única rota que calcula valores — recebe um
CEP de destino (zipcode) e uma lista indexada de produtos
(products[0], products[1], ...) com product_id, price e quantity, e
devolve os métodos de envio disponíveis com price, delivery_time e
delivery_time_text; e GET /shippings/, que apenas lista as formas de envio
ativas na loja (id, name, active), sem realizar cotação. A cotação é tipicamente
chamada no checkout ou na página de produto para exibir opções de entrega ao
cliente antes de fechar o pedido.
Por baixo, a cotação consulta automaticamente os gateways de frete
configurados na loja (ex.: Frete-X API, Correios, transportadoras) e consolida
o resultado no formato padrão. Isso conecta o frete a vários outros recursos
Tray: o product_id e o price vêm do cadastro em tray-produtos; o peso (em
gramas) e as dimensões usados implicitamente no cálculo vêm de tray-produtos
e tray-variacoes (a cotação não recebe peso/dimensões como parâmetros); a
flag free_shipping do produto pode zerar o price do frete conforme a
configuração da loja; o id do método retornado é o mesmo shipping_id usado
ao vincular frete a um cupom (shipping_relationship em tray-cupons); e a
criação/configuração de métodos, gateways e tabelas de CEP é feita em
tray-configuracao-frete, não aqui.
Valem todas as invariantes da plataforma. O access_token vai sempre como
query param (?access_token={token}) — enviá-lo no header Authorization
resulta em HTTP 401. A URL base https://{api_address}/ varia por loja e
é retornada no callback OAuth; reaproveitar o api_address de outra loja gera
HTTP 404 (use TRAY_API_ADDRESS por env). Como esses endpoints são GET, não
há payload envolto em chave de recurso no request; a response de cotação usa
o wrapper Shipping (array) e a de listagem usa ShippingMethods[].ShippingMethod.
O rate limit é de 180 req/min e 10.000 req/dia — e a cotação é especialmente
sensível porque consulta APIs externas (logo é lenta) e costuma ser disparada em
loop a cada digitação de CEP no checkout, estourando facilmente o HTTP 429;
aplique backoff exponencial (1s, 2s, 4s, 8s) e cache por CEP + produtos +
quantidade. CEP segue validação BR: 8 dígitos numéricos, sem traço nem ponto.
Endpoints
| Método |
Endpoint |
Descrição |
| GET |
/shippings/cotation/ |
Calcular o frete (preço e prazo) para um ou mais produtos em direção a um CEP de destino |
| GET |
/shippings/ |
Listar as formas de envio (métodos de frete) configuradas e ativas na loja, sem cotação |
Autenticação: ?access_token={token} em todas as chamadas — sempre como query parameter, nunca em header Authorization (header → HTTP 401).
URL base: https://{api_address}/ — varia por loja, retornada no callback OAuth. Use TRAY_API_ADDRESS via variável de ambiente.
Recurso somente leitura: a skill tray-frete expõe apenas endpoints GET. Para criar/configurar métodos de envio, gateways ou tabelas de CEP, use tray-configuracao-frete (/shippings/method/gateway, /shippings/method/zipcode_table).
GET /shippings/cotation/
Calcula o frete para um ou mais produtos em direção a um CEP de destino, retornando os métodos de envio disponíveis com preço e prazo. A Tray consulta automaticamente os gateways de frete configurados na loja (ex.: Frete-X API, Correios, transportadoras) e consolida a resposta no formato padrão da API. É a única rota que retorna valores de frete.
Quando usar:
- No checkout ou em página de produto, para exibir as opções de frete ao cliente.
- Para calcular o frete de um carrinho com múltiplos itens antes de fechar o pedido.
Pré-requisitos:
access_token válido como query param (?access_token={token}, nunca em header).
TRAY_API_ADDRESS da loja (varia por loja, retornado no callback OAuth).
product_id e price de cada produto disponíveis (ver tray-produtos).
- CEP de destino com apenas 8 dígitos numéricos (sem traço/ponto).
- Quantidade de cada produto.
Schema: sem body — apenas query params: access_token, zipcode, products[n][product_id], products[n][price], products[n][quantity].
Parâmetros (query):
| Parâmetro |
Tipo |
Obrigatório |
Formato |
Descrição |
access_token |
string |
Sim |
— |
Token de acesso na query string |
zipcode |
string |
Sim |
cep — 8 dígitos numéricos, sem traço/ponto (ex.: 04001001) |
CEP de destino da cotação |
products[n][product_id] |
number |
Sim |
inteiro positivo |
ID do produto a cotar; índice n inicia em 0 e permite múltiplos produtos na mesma requisição |
products[n][price] |
decimal |
Sim |
decimal com ponto (ex.: 58.90) |
Preço unitário do produto, usado no cálculo de seguro/valor declarado |
products[n][quantity] |
number |
Sim |
inteiro positivo |
Quantidade do produto; multiplica peso e dimensões na cotação |
O peso e as dimensões não são parâmetros de query — a Tray usa os valores do cadastro do produto/variação (peso em gramas; ver tray-produtos e tray-variacoes). Cadastro com peso 0 ou dimensões ausentes faz os Correios aplicarem mínimos (16x11x2 cm) ou retornarem erro, distorcendo o valor.
Campos da resposta (wrapper Shipping, array — cada item é um método de envio cotado):
| Campo |
Tipo |
Descrição |
id |
number |
ID do método de envio (vem como string na resposta JSON) |
name |
string |
Nome do método (ex.: PAC, SEDEX, Transportadora) |
price |
decimal |
Valor do frete em reais; pode vir 0.00 para frete grátis (string na resposta) |
delivery_time |
number |
Prazo de entrega estimado em dias úteis (string na resposta) |
delivery_time_text |
string |
Texto formatado do prazo para exibição (ex.: 8 dias úteis) |
Exemplo de resposta:
{
"Shipping": [
{ "id": "1", "name": "PAC", "price": "25.90", "delivery_time": "8", "delivery_time_text": "8 dias úteis" },
{ "id": "2", "name": "SEDEX", "price": "45.50", "delivery_time": "3", "delivery_time_text": "3 dias úteis" }
]
}
Exemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
# Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.
curl -s -G "https://${TRAY_API_ADDRESS}/shippings/cotation/" \
--data-urlencode "access_token=${TRAY_ACCESS_TOKEN}" \
--data-urlencode "zipcode=04001001" \
--data-urlencode "products[0][product_id]=123" \
--data-urlencode "products[0][price]=58.90" \
--data-urlencode "products[0][quantity]=2"
Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
// Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
// Normaliza o CEP para 8 dígitos numéricos (remove traço/ponto)
const zipcode = "04001-001".replace(/\D/g, "");
const cart = [
{ product_id: 123, price: "58.90", quantity: 2 },
{ product_id: 456, price: "19.90", quantity: 1 },
];
const params = new URLSearchParams({ access_token: token, zipcode });
cart.forEach((item, n) => {
params.append(`products[${n}][product_id]`, String(item.product_id));
params.append(`products[${n}][price]`, item.price);
params.append(`products[${n}][quantity]`, String(item.quantity));
});
const res = await fetch(`https://${base}/shippings/cotation/?${params}`);
if (res.status === 429) throw new Error("Rate limit — aplicar backoff exponencial");
const data = await res.json();
const metodos = data.Shipping ?? [];
if (metodos.length === 0) {
// Estado legítimo: frete indisponível para a região (CEP fora de cobertura)
console.log("Frete indisponível para este CEP");
} else {
metodos.forEach((m) => console.log(`${m.name}: R$ ${m.price} — ${m.delivery_time_text}`));
}
Erros comuns:
| Código |
Causa |
Como resolver |
200 (Shipping vazio) |
Frete indisponível para a região — CEP fora de cobertura ou transportadora não atende; o array Shipping volta vazio ou parcial |
Tratar como estado legítimo na UI ("frete indisponível para este CEP"); nunca assumir que ao menos um método sempre retorna |
400 |
CEP enviado com máscara (04001-001) ou parâmetros products[n] malformados |
Normalizar zipcode para 8 dígitos numéricos; usar índices products[0], products[1]... corretos |
401 |
access_token expirado (3h) ou enviado como header Authorization em vez de query param |
Renovar via GET /auth?refresh_token={token}; sempre passar ?access_token={token} na query string |
404 |
api_address incorreto (varia por loja) |
Usar o api_address retornado no callback OAuth da loja |
429 |
Rate limit (180 req/min ou 10k/dia) — agravado porque a cotação consulta APIs externas e pode ser repetida em loop no checkout |
Backoff exponencial (1s, 2s, 4s, 8s) e cache de resultados por CEP + produtos + quantidade + dimensões; debounce na UI |
GET /shippings/
Lista as formas de envio (métodos de frete) configuradas e ativas na loja, sem realizar cotação. Retorna id, name e active de cada método (ex.: PAC, SEDEX, Retirada na Loja). Não retorna preço nem prazo — para isso use /shippings/cotation/.
Quando usar:
- Para descobrir quais métodos de envio existem na loja antes de exibir/filtrar opções.
- Para mapear IDs de método — necessário, por exemplo, ao vincular frete a um cupom via
shipping_relationship (ver tray-cupons).
Pré-requisitos:
access_token válido como query param.
TRAY_API_ADDRESS da loja.
Schema: sem body — apenas query param access_token.
Parâmetros (query):
| Parâmetro |
Tipo |
Obrigatório |
Descrição |
access_token |
string |
Sim |
Token de acesso na query string |
Campos da resposta (wrapper ShippingMethods[].ShippingMethod):
| Campo |
Tipo |
Descrição |
id |
number |
ID do método de envio (string na resposta); usável em outros recursos (ex.: shipping_relationship em tray-cupons) |
name |
string |
Nome do método (ex.: PAC, SEDEX, Retirada na Loja) |
active |
string |
Status: '1' = ativo, '0' = inativo |
Exemplo de resposta:
{
"ShippingMethods": [
{ "ShippingMethod": { "id": "1", "name": "PAC", "active": "1" } },
{ "ShippingMethod": { "id": "2", "name": "SEDEX", "active": "1" } },
{ "ShippingMethod": { "id": "3", "name": "Retirada na Loja", "active": "1" } }
]
}
Exemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
# Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.
curl -s "https://${TRAY_API_ADDRESS}/shippings/?access_token=${TRAY_ACCESS_TOKEN}"
Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
// Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const res = await fetch(`https://${base}/shippings/?access_token=${token}`);
if (res.status === 429) throw new Error("Rate limit — aplicar backoff exponencial");
const data = await res.json();
const metodos = (data.ShippingMethods ?? [])
.map((m) => m.ShippingMethod)
.filter((m) => m.active === "1");
metodos.forEach((m) => console.log(`#${m.id} ${m.name}`));
Erros comuns:
| Código |
Causa |
Como resolver |
401 |
Token expirado ou enviado em header Authorization em vez de query param |
Renovar token; usar query param ?access_token={token} |
404 |
api_address incorreto (varia por loja) |
Usar o api_address retornado no callback OAuth |
429 |
Rate limit (180 req/min ou 10k/dia) |
Backoff exponencial (1s, 2s, 4s, 8s) |
Edge cases
Frete indisponível para a região (array Shipping vazio ou parcial). GET /shippings/cotation/ pode retornar HTTP 200 com {"Shipping": []} ou apenas um subconjunto dos métodos quando o CEP está fora de cobertura ou uma transportadora não atende àquela região. Não é erro: é um estado legítimo. Trate o array vazio na UI exibindo "frete indisponível para este CEP" em vez de assumir que pelo menos um método sempre volta.
- Exemplo: cotação para
zipcode=69900970 (Rio Branco/AC) com produto pesado pode voltar {"Shipping": []} enquanto o mesmo produto para zipcode=04001001 (São Paulo) retorna PAC e SEDEX. O checkout deve degradar para "consulte outras formas de envio", não travar.
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const data = await res.json();
const metodos = data.Shipping ?? [];
if (metodos.length === 0) {
return { disponivel: false, motivo: "sem cobertura para o CEP informado" };
}
Múltiplos produtos no mesmo carrinho exigem índice incremental. A rota aceita products[0], products[1], products[2]... na mesma requisição, e cada índice precisa ser único. Omitir o índice ou repetir products[0] para itens diferentes faz a Tray cotar apenas um produto, gerando frete subdimensionado (peso e dimensões somados a menos). Sempre incremente o índice por item do carrinho.
- Exemplo errado (sobrescreve o item 0):
products[0][product_id]=123&products[0][product_id]=456 → a Tray considera só 456. Correto: products[0][product_id]=123&products[1][product_id]=456.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s -G "https://${TRAY_API_ADDRESS}/shippings/cotation/" \
--data-urlencode "access_token=${TRAY_ACCESS_TOKEN}" \
--data-urlencode "zipcode=04001001" \
--data-urlencode "products[0][product_id]=123" \
--data-urlencode "products[0][price]=58.90" \
--data-urlencode "products[0][quantity]=2" \
--data-urlencode "products[1][product_id]=456" \
--data-urlencode "products[1][price]=120.00" \
--data-urlencode "products[1][quantity]=1"
Frete grátis sobrescrevendo o preço (price='0.00'). Produtos cadastrados com free_shipping=1 (ver tray-produtos) podem retornar price='0.00' em alguns métodos, dependendo da configuração da loja. O código de checkout não deve recalcular nem rejeitar frete zerado como erro de cotação — é o comportamento esperado para esses produtos.
- Exemplo: produto com
free_shipping=1 retorna {"id":"1","name":"PAC","price":"0.00","delivery_time":"8"}. Tratar price === "0.00" como "Grátis" na UI, não como cotação falha. Atenção: o price vem como string na resposta — compare como string ou converta com parseFloat antes de operações numéricas.
Peso e dimensões herdados do cadastro do produto/variação. A cotação não recebe peso/dimensões como parâmetros de query — usa os valores do cadastro do produto (weight em gramas) e da variação (ver tray-produtos/tray-variacoes). Cadastro com weight=0 ou dimensões ausentes faz os Correios aplicarem mínimos (16x11x2 cm) ou retornarem erro, distorcendo o valor cotado para mais ou para menos.
- Exemplo: produto cadastrado com
weight=0 cota como pacote mínimo dos Correios e devolve um frete artificialmente baixo; ao faturar, o custo real é maior. Auditar o weight/dimensões em tray-produtos antes de confiar na cotação. Lembre que 1 kg = 1000 g.
Latência e rate limit ao cotar a cada tecla no campo de CEP. Como a cotação consulta APIs externas (gateways/transportadoras como Frete-X API), é lenta e fácil de disparar em loop a cada caractere digitado no campo de CEP, estourando o limite de 180 req/min (HTTP 429). Faça debounce na UI (cotar só após o CEP completo, 8 dígitos) e cache por CEP + produtos + quantidade.
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const cache = new Map(); // chave: cep|product_id:qty|...
async function cotar(cep, itens) {
if (cep.replace(/\D/g, "").length !== 8) return null; // só cota CEP completo
const chave = `${cep}|${itens.map(i => `${i.id}:${i.qty}`).join("|")}`;
if (cache.has(chave)) return cache.get(chave);
const res = await fetch(/* ... */);
if (res.status === 429) { /* backoff exponencial: 1s, 2s, 4s, 8s */ }
const data = await res.json();
cache.set(chave, data.Shipping ?? []);
return cache.get(chave);
}
CEP com máscara quebra a cotação. Enviar zipcode com traço ou ponto (04001-001) pode resultar em cotação vazia ou HTTP 400. O campo é do formato cep da plataforma: 8 dígitos numéricos, sem traço nem ponto. Normalize com String(cep).replace(/\D/g, "") antes de chamar e valide que sobraram exatamente 8 dígitos.
- Exemplo:
04001-001 → normalizar para 04001001. Um CEP com 7 ou 9 dígitos após a limpeza indica entrada inválida — rejeite antes de gastar uma chamada à API.
Confundir /shippings/ com /shippings/cotation/. GET /shippings/ apenas lista os métodos ativos da loja (id, name, active) — sem preço nem prazo. Apenas GET /shippings/cotation/ calcula valores. Chamar /shippings/ esperando preços retorna só id/name/active e leva o desenvolvedor a achar que a cotação "veio sem preço".
- Exemplo: para montar o seletor de frete com valores no checkout, use
cotation/; para descobrir os IDs de método (ex.: vincular frete a um cupom via shipping_relationship, ver tray-cupons), use /shippings/.
Antipadrões
❌ Enviar o access_token em header Authorization. A API Tray ignora o header Authorization: Bearer ... e responde HTTP 401 tanto em /shippings/ quanto em /shippings/cotation/. O token deve ir como query param ?access_token={token}. Por quê quebra: a autenticação da Tray é por query string, não por header — o header simplesmente não é lido. Correção: monte sempre a URL com ?access_token=${TRAY_ACCESS_TOKEN} (ou --data-urlencode "access_token=..." no curl -G) e nunca passe credenciais via headers.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
# ERRADO → HTTP 401:
# curl -H "Authorization: Bearer $TRAY_ACCESS_TOKEN" "https://${TRAY_API_ADDRESS}/shippings/"
# CERTO:
curl -s "https://${TRAY_API_ADDRESS}/shippings/?access_token=${TRAY_ACCESS_TOKEN}"
❌ Hardcodar o api_address de uma loja como base fixa. O api_address varia por loja (é retornado no callback OAuth da Etapa 2). Reaproveitar o endereço de outra loja gera HTTP 404 em /shippings/cotation/. Por quê quebra: cada loja tem um host de API próprio; um endereço fixo só funciona para uma loja e falha silenciosamente para as demais. Correção: armazene o api_address junto com os tokens da loja e leia-o sempre de TRAY_API_ADDRESS por variável de ambiente, nunca como literal no código.
❌ Cotar a cada tecla no campo de CEP sem debounce nem cache. Disparar GET /shippings/cotation/ a cada caractere digitado estoura o rate limit (HTTP 429) e degrada o checkout, porque cada cotação consulta transportadoras externas e é lenta. Por quê quebra: 180 req/min se esgotam rápido quando vários clientes digitam CEP simultaneamente, e a latência externa empilha requisições. Correção: cote apenas com o CEP completo (8 dígitos), aplique debounce na UI e cacheie por CEP + produtos + quantidade; em 429, aplique backoff exponencial (1s, 2s, 4s, 8s).
❌ Tratar Shipping vazio como falha genérica e abortar o checkout. Um array Shipping vazio com HTTP 200 é um estado legítimo (região sem cobertura ou transportadora que não atende). Por quê quebra: lançar erro 500/genérico ao receber [] impede a compra de clientes em regiões parcialmente atendidas e mascara a causa real. Correção: detecte Shipping.length === 0 e exiba "frete indisponível para este CEP" (ou ofereça retirada na loja, se houver), mantendo o checkout vivo.
❌ Esquecer de incrementar o índice em products[n] com múltiplos itens. Repetir products[0] para produtos diferentes ou omitir o índice faz a Tray cotar apenas um produto, devolvendo frete subdimensionado. Por quê quebra: o índice n é o identificador de posição do item no array; sem incrementá-lo, os itens posteriores sobrescrevem o primeiro e peso/dimensões somados ficam menores que o real. Correção: gere products[0], products[1], products[2]... de forma incremental, um bloco product_id/price/quantity por item do carrinho.
❌ Hardcodar tokens literais no código (curl/Node). Nunca escreva o access_token (nem consumer_key/consumer_secret) como string no código de cotação. Por quê quebra: além do risco de vazamento em logs/repositório, o access_token expira em 3 horas — um literal fica inválido e gera HTTP 401. Correção: use TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS via variável de ambiente e renove o token via GET /auth?refresh_token={token} antes de expirar.
❌ Usar a skill tray-frete para criar/configurar métodos de envio ou tabelas de CEP. Este recurso é somente leitura — apenas endpoints GET (/shippings/ e /shippings/cotation/). Por quê quebra: não existe POST/PUT de método de frete aqui; tentar criar/configurar pela API de Frete não tem endpoint correspondente. Correção: configuração de métodos, gateway e tabela de CEP fica em tray-configuracao-frete (/shippings/method/gateway, /shippings/method/zipcode_table); use tray-frete apenas para cotar e listar métodos ativos.
❌ Tratar os campos numéricos da resposta como number. Na resposta da cotação, id, price e delivery_time vêm como string (ex.: "price": "25.90", "delivery_time": "8"), e active em /shippings/ vem como "0"/"1". Por quê quebra: somar price diretamente concatena strings ("25.90" + "10.00" = "25.9010.00") e comparações numéricas falham. Correção: converta explicitamente com parseFloat(price)/parseInt(delivery_time, 10) antes de qualquer cálculo, e compare active === "1" como string ao filtrar métodos ativos.
Webhooks relacionados
A skill tray-frete é somente leitura (apenas GET) e não dispara webhooks próprios: não existe escopo shipping no sistema de notificação. Cotação e listagem de métodos são consultas síncronas — o resultado só existe no momento da chamada e não gera evento assíncrono.
Os webhooks que afetam indiretamente o frete vêm de outros recursos:
| Escopo |
Ações |
Por que afeta o frete |
product |
insert, update, delete |
Alterações de peso, dimensões ou free_shipping no cadastro mudam o valor cotado em /shippings/cotation/. Reaja invalidando o cache de cotação do produto afetado (scope_id). |
variant |
insert, update, delete |
A cotação herda peso/dimensões da variação; mudanças alteram o frete da variação cotada. |
store_config |
update |
Inclui alteração de configuração de frete da loja (gateway, métodos ativos, frete grátis). Invalide qualquer cache de /shippings/ e de cotação após esse evento. |
order |
insert, update |
O pedido carrega shipping_cost, shipping_method e tracking_number; o frete já cotado é congelado no pedido. Use este escopo para sincronizar status de envio, não a skill de frete. |
Não há escopo shipping. Para reagir a mudanças que impactam frete, escute product, variant e store_config e invalide os caches de cotação correspondentes. Detalhes de payload, ativação e retry em ../webhooks/SKILL.md.
Glossário
| Termo |
Definição |
| cotação (cotation) |
Cálculo do valor e prazo de frete para um conjunto de produtos rumo a um CEP, via GET /shippings/cotation/. É a única rota que retorna preço e prazo. |
| método de envio (ShippingMethod) |
Forma de entrega configurada e ativa na loja (PAC, SEDEX, transportadora, retirada na loja), listada por GET /shippings/ com id, name e active. |
| gateway de frete |
Integração externa (ex.: Frete-X API) que a Tray consulta automaticamente na cotação para obter preços/prazos de múltiplas transportadoras; configurado no painel ou via tray-configuracao-frete. |
| Frete-X API |
Gateway de frete suportado pela Tray para cotação automática com múltiplas transportadoras; configurado no painel administrativo da loja. |
delivery_time |
Prazo de entrega estimado em dias úteis retornado por método na resposta de cotação; acompanha delivery_time_text para exibição. |
free_shipping |
Flag do cadastro do produto (tray-produtos) que, conforme a configuração da loja, pode zerar o price do frete na cotação. |
zipcode |
CEP de destino da cotação; enviado apenas com 8 dígitos numéricos, sem traço ou ponto. |
shipping_id |
ID do método de envio (campo id de /shippings/ ou da cotação); usado em outros recursos como vínculo de frete em cupons (shipping_relationship, ver tray-cupons). |
api_address |
Host base da API específico de cada loja, retornado no callback OAuth; compõe a URL https://{api_address}/ de toda chamada. |
peso cúbico (cubic_weight) |
Peso volumétrico calculado a partir das dimensões do produto, usado por transportadoras quando excede o peso real; deriva do cadastro do produto e influencia o valor cotado. |
Referências
Fluxo recomendado de cotação no checkout
A cotação de frete é, na prática, a operação mais sensível desta skill: ela é
disparada repetidamente no checkout (a cada edição do campo de CEP), depende de
APIs externas lentas (gateways/transportadoras) e estoura HTTP 429 com
facilidade. O fluxo recomendado encadeia normalização de CEP → debounce →
cache por (cep + itens) com TTL → chamada com retry/backoff em 429 → parsing de
Shipping[] → escolha do método mais barato/rápido. O bloco abaixo é um
módulo completo e comentado que cobre todas essas etapas.
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
// Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.
//
// Módulo de cotação de frete para checkout: debounce, cache com TTL,
// retry com backoff exponencial em 429, normalização de CEP e parsing.
const BASE = process.env.TRAY_API_ADDRESS; // host por loja (callback OAuth)
const TOKEN = process.env.TRAY_ACCESS_TOKEN; // nunca literal; renovar em 3h
// ---------------------------------------------------------------------------
// 1) Normalização de CEP — 8 dígitos numéricos, sem traço/ponto.
// Retorna null se a entrada não tiver exatamente 8 dígitos após a limpeza.
// ---------------------------------------------------------------------------
function normalizarCep(input) {
const digitos = String(input ?? "").replace(/\D/g, "");
return digitos.length === 8 ? digitos : null; // 7 ou 9 dígitos = inválido
}
// ---------------------------------------------------------------------------
// 2) Chave de cache estável por (cep + itens).
// Ordena os itens por product_id para que a mesma combinação de carrinho
// gere sempre a mesma chave, independente da ordem de inserção.
// ---------------------------------------------------------------------------
function chaveCache(cep, itens) {
const assinatura = [...itens]
.map((i) => ({ id: Number(i.product_id), q: Number(i.quantity), p: String(i.price) }))
.sort((a, b) => a.id - b.id)
.map((i) => `${i.id}:${i.q}:${i.p}`)
.join("|");
return `${cep}#${assinatura}`;
}
// ---------------------------------------------------------------------------
// 3) Cache em memória com TTL. Cotação muda pouco em janelas curtas, então
// um TTL de poucos minutos reduz drasticamente as chamadas externas.
// ---------------------------------------------------------------------------
const TTL_MS = 5 * 60 * 1000; // 5 minutos
const cache = new Map(); // chave -> { expira: epochMs, valor: Shipping[] }
function lerCache(chave) {
const hit = cache.get(chave);
if (!hit) return null;
if (Date.now() > hit.expira) {
cache.delete(chave); // expirado: descarta e força nova cotação
return null;
}
return hit.valor;
}
function gravarCache(chave, valor) {
cache.set(chave, { expira: Date.now() + TTL_MS, valor });
}
// ---------------------------------------------------------------------------
// 4) Monta os índices products[n][...] incrementando n por item do carrinho.
// NUNCA repetir products[0] — itens posteriores sobrescreveriam o primeiro
// e o frete viria subdimensionado (peso/dimensões somados a menos).
// ---------------------------------------------------------------------------
function montarParams(cep, itens) {
const params = new URLSearchParams({ access_token: TOKEN, zipcode: cep });
itens.forEach((item, n) => {
params.append(`products[${n}][product_id]`, String(item.product_id));
params.append(`products[${n}][price]`, String(item.price)); // ponto decimal
params.append(`products[${n}][quantity]`, String(item.quantity));
});
return params;
}
// ---------------------------------------------------------------------------
// 5) Chamada com retry e backoff exponencial em 429.
// Em 401/404 não adianta repetir — falha rápido com mensagem clara.
// O access_token vai SEMPRE na query string (params), nunca em header.
// ---------------------------------------------------------------------------
async function cotarComRetry(cep, itens, { maxTentativas = 4 } = {}) {
const params = montarParams(cep, itens);
const url = `https://${BASE}/shippings/cotation/?${params}`;
for (let tentativa = 0; tentativa < maxTentativas; tentativa++) {
const res = await fetch(url); // GET; sem header Authorization
if (res.status === 429) {
// backoff exponencial: 1s, 2s, 4s, 8s...
const esperaMs = 1000 * 2 ** tentativa;
await new Promise((r) => setTimeout(r, esperaMs));
continue;
}
if (res.status === 401) {
throw new Error("401 — access_token expirado/em header. Renovar via GET /auth?refresh_token e usar query param.");
}
if (res.status === 404) {
throw new Error("404 — api_address incorreto (varia por loja). Usar TRAY_API_ADDRESS da loja.");
}
if (!res.ok) {
throw new Error(`Cotação falhou: HTTP ${res.status}`);
}
const data = await res.json();
return data.Shipping ?? []; // array vazio = sem cobertura (estado legítimo)
}
throw new Error("429 persistente após backoff — reduzir frequência de cotação.");
}
// ---------------------------------------------------------------------------
// 6) Orquestrador: normaliza, consulta cache, cota e grava no cache.
// Retorna { disponivel, metodos } — disponivel=false quando Shipping[] vazio.
// ---------------------------------------------------------------------------
async function cotarFrete(cepBruto, itens) {
const cep = normalizarCep(cepBruto);
if (!cep) return { disponivel: false, motivo: "cep_invalido", metodos: [] };
if (!itens?.length) return { disponivel: false, motivo: "carrinho_vazio", metodos: [] };
const chave = chaveCache(cep, itens);
const cacheado = lerCache(chave);
if (cacheado) return { disponivel: cacheado.length > 0, motivo: cacheado.length ? null : "sem_cobertura", metodos: cacheado };
const metodos = await cotarComRetry(cep, itens);
gravarCache(chave, metodos);
return { disponivel: metodos.length > 0, motivo: metodos.length ? null : "sem_cobertura", metodos };
}
// ---------------------------------------------------------------------------
// 7) Parsing/seleção: a resposta vem com campos numéricos como STRING.
// Converta antes de comparar. Escolhe mais barato e mais rápido.
// ---------------------------------------------------------------------------
function escolherMetodos(metodos) {
const normalizados = metodos.map((m) => ({
id: m.id,
name: m.name,
price: parseFloat(m.price), // "25.90" -> 25.90
prazo: parseInt(m.delivery_time, 10), // "8" -> 8
prazoTexto: m.delivery_time_text,
gratis: parseFloat(m.price) === 0, // free_shipping pode zerar
}));
const maisBarato = [...normalizados].sort((a, b) => a.price - b.price || a.prazo - b.prazo)[0] ?? null;
const maisRapido = [...normalizados].sort((a, b) => a.prazo - b.prazo || a.price - b.price)[0] ?? null;
return { todos: normalizados, maisBarato, maisRapido };
}
// ---------------------------------------------------------------------------
// 8) Debounce do campo de CEP: só cota após o usuário parar de digitar e
// apenas quando o CEP estiver completo (8 dígitos). Evita disparar uma
// cotação por tecla (causa direta de 429 no checkout).
// ---------------------------------------------------------------------------
function criarCotadorDebounced(onResultado, atrasoMs = 500) {
let timer = null;
return function aoDigitarCep(cepBruto, itens) {
clearTimeout(timer);
const cep = normalizarCep(cepBruto);
if (!cep) return; // não cota CEP incompleto/inválido — economiza chamadas
timer = setTimeout(async () => {
try {
const resultado = await cotarFrete(cep, itens);
onResultado(escolherMetodos(resultado.metodos), resultado);
} catch (err) {
onResultado(null, { disponivel: false, motivo: "erro", erro: String(err) });
}
}, atrasoMs);
};
}
// Exemplo de uso no checkout:
// const cotar = criarCotadorDebounced((selecao, resultado) => {
// if (!resultado.disponivel) return mostrarUi("Frete indisponível para este CEP");
// mostrarUi(`Mais barato: ${selecao.maisBarato.name} R$ ${selecao.maisBarato.price}`);
// });
// inputCep.addEventListener("input", (e) => cotar(e.target.value, carrinhoAtual));
Integração com gateway de frete (Frete-X / transportadoras)
A cotação da Tray é um agregador: quando o checkout chama
GET /shippings/cotation/, a plataforma consulta de uma só vez todas as fontes
de frete habilitadas na loja e devolve o resultado consolidado no array
Shipping[], com o mesmo formato para qualquer origem. Existem duas classes de
origem:
- Métodos nativos — PAC e SEDEX (Correios), retirada na loja e tabelas de
frete por faixa de CEP configuradas na própria loja. São resolvidos
diretamente pela Tray a partir do peso/dimensões do cadastro do produto.
- Gateway de frete — integrações externas como a Frete-X API, que por
sua vez cotam com múltiplas transportadoras (Jadlog, Loggi, Total Express,
etc.). A Tray repassa peso, dimensões, valor declarado e CEP ao gateway, e o
gateway devolve uma lista de opções por transportadora.
Do ponto de vista do consumidor da API, não há distinção de formato: tanto
um método nativo quanto uma transportadora vinda de gateway aparecem como um
item de Shipping[] com id, name, price, delivery_time e
delivery_time_text. O name é o que diferencia na prática — pode vir como
"PAC", "SEDEX", "Jadlog - Package" ou "Loggi Econômico", conforme a
transportadora retornada pelo gateway.
| Aspecto |
Método nativo (PAC/SEDEX) |
Gateway de frete (Frete-X) |
| Origem do cálculo |
Correios / tabela da loja, resolvido pela Tray |
Gateway externo consultando N transportadoras |
| Latência típica |
Baixa/média |
Mais alta (rede + API da transportadora) |
name na resposta |
PAC, SEDEX, Retirada na Loja |
Nome da transportadora/serviço (Jadlog - Package) |
| Quantidade de opções |
Fixa (poucos métodos) |
Variável — depende de quantas transportadoras responderam |
| Sensível a peso/dimensões zerados |
Sim (aplica mínimos dos Correios) |
Sim (gateway pode rejeitar item sem peso) |
| Configuração |
tray-configuracao-frete (/shippings/method/...) |
Painel da loja + gateway externo; também via tray-configuracao-frete |
Campos extras possíveis. O contrato estável de Shipping[] é
id/name/price/delivery_time/delivery_time_text. Cotações vindas de
gateway podem trazer campos adicionais (ex.: nome de transportadora separado,
código de serviço interno, observação de prazo) dependendo da configuração — não
dependa da presença desses campos extras; programe sempre contra o contrato
mínimo e trate qualquer campo adicional como opcional.
Fallback quando o gateway está indisponível. Como o gateway é uma dependência
externa, ele pode falhar ou expirar (timeout) s
…(truncated)
1---2name: tray-frete3description: API de cotação e listagem de frete da Tray (recurso `shippings`, somente leitura). Cobre dois endpoints GET: `/shippings/cotation/` calcula valor e prazo de entrega para um ou mais produtos rumo a um CEP, consultando os gateways de frete configurados na loja (Frete-X API, Correios, transportadoras); e `/shippings/` lista as formas de envio ativas (id, nome, status), sem preço nem prazo. DISAMBIGUATION: este recurso NÃO cria nem configura métodos de envio, tabelas de CEP ou gateways — para isso use tray-configuracao-frete (`/shippings/method/gateway`, `/shippings/method/zipcode_table`). Apenas `/shippings/cotation/` retorna preço/prazo; `/shippings/` só lista métodos.4---56## MANDATORY: Tool Calls Required Before Answering78> **Estas chamadas são OBRIGATÓRIAS, não opcionais.** Execute-as antes de gerar9> qualquer código de consulta. Se você está respondendo sem ter chamado a10> ferramenta abaixo, **pare e chame agora**.1112### 1. Buscar documentação atualizada (sempre)1314```bash15node skills/tray-dev/scripts/search_docs.mjs --topic=frete "<termo da pergunta>"16```1718- `<TOPIC_SLUG>`: ver tabela em `skills/tray-dev/SKILL.md`.19- Use os trechos retornados como fonte primária; este SKILL.md é resumo denso.2021### 2. Revisar parâmetros (este recurso NÃO tem `validate.mjs`)2223> **Nota:** o recurso `frete` ainda **não** possui `scripts/validate.mjs` local24> — e por ser somente leitura (apenas GET), não há payload de body para25> validar. A chamada **OBRIGATÓRIA** a `search_docs.mjs` acima continua valendo.26> Como não há validador automático, **você é responsável** por revisar27> manualmente cada parâmetro de query contra a doc retornada por28> `search_docs.mjs` e contra os schemas de referência em29> `skills/frete/schemas/` (ver [`schemas/shippings.cotation.json`](schemas/shippings.cotation.json))30> antes de retornar qualquer código. Confira em especial: `zipcode` com 831> dígitos numéricos (sem traço/ponto), índices `products[n]` incrementados por32> item (não repetir `products[0]`), e `product_id`/`price`/`quantity`33> presentes em cada item.3435## Antes de responder3637> Execute estas verificações antes de gerar qualquer payload ou código:38391. Confirme o método HTTP e endpoint correto para a operação solicitada:40 `GET /shippings/cotation/` para calcular preço/prazo, ou `GET /shippings/`41 apenas para listar métodos ativos (sem valores).422. Identifique os parâmetros obrigatórios da cotação — `zipcode`,43 `products[n][product_id]`, `products[n][price]` e `products[n][quantity]`;44 não omita nenhum e incremente o índice `n` por item.453. Verifique que `access_token` não aparece como literal string no código46 gerado — use sempre `TRAY_ACCESS_TOKEN` e `TRAY_API_ADDRESS` por variável47 de ambiente, e passe o token como query param (`?access_token={token}`),48 nunca em header.494. Confirme que esta é a skill correta para o recurso: cotação/listagem é50 somente leitura; se for criar/configurar método, gateway ou tabela de CEP,51 leia `when_not_to_use` e redirecione para `tray-configuracao-frete`.5253# Frete — API Tray5455Documentação oficial: https://developers.tray.com.br/#api-de-integracao-de-frete5657> **Atenção (disambiguation):** apenas `GET /shippings/cotation/` retorna preço58> e prazo; `GET /shippings/` somente lista métodos ativos (id, name, active).59> Este recurso é **somente leitura** — não cria nem configura nada. Configuração60> de métodos, gateways e tabelas de CEP fica em `tray-configuracao-frete`.6162## Visão geral6364A API de Frete da Tray resolve a pergunta "quanto custa e em quantos dias este65carrinho chega neste CEP?". Ela expõe dois endpoints GET e somente leitura:66`GET /shippings/cotation/`, a **única** rota que calcula valores — recebe um67CEP de destino (`zipcode`) e uma lista indexada de produtos68(`products[0]`, `products[1]`, ...) com `product_id`, `price` e `quantity`, e69devolve os métodos de envio disponíveis com `price`, `delivery_time` e70`delivery_time_text`; e `GET /shippings/`, que apenas lista as formas de envio71ativas na loja (id, name, active), sem realizar cotação. A cotação é tipicamente72chamada no checkout ou na página de produto para exibir opções de entrega ao73cliente antes de fechar o pedido.7475Por baixo, a cotação consulta automaticamente os **gateways de frete**76configurados na loja (ex.: Frete-X API, Correios, transportadoras) e consolida77o resultado no formato padrão. Isso conecta o frete a vários outros recursos78Tray: o `product_id` e o `price` vêm do cadastro em `tray-produtos`; o peso (em79gramas) e as dimensões usados implicitamente no cálculo vêm de `tray-produtos`80e `tray-variacoes` (a cotação **não** recebe peso/dimensões como parâmetros); a81flag `free_shipping` do produto pode zerar o `price` do frete conforme a82configuração da loja; o `id` do método retornado é o mesmo `shipping_id` usado83ao vincular frete a um cupom (`shipping_relationship` em `tray-cupons`); e a84criação/configuração de métodos, gateways e tabelas de CEP é feita em85`tray-configuracao-frete`, não aqui.8687Valem todas as invariantes da plataforma. O `access_token` vai **sempre** como88query param (`?access_token={token}`) — enviá-lo no header `Authorization`89resulta em `HTTP 401`. A URL base `https://{api_address}/` **varia por loja** e90é retornada no callback OAuth; reaproveitar o `api_address` de outra loja gera91`HTTP 404` (use `TRAY_API_ADDRESS` por env). Como esses endpoints são GET, não92há payload envolto em chave de recurso no request; a **response** de cotação usa93o wrapper `Shipping` (array) e a de listagem usa `ShippingMethods[].ShippingMethod`.94O rate limit é de 180 req/min e 10.000 req/dia — e a cotação é especialmente95sensível porque consulta APIs externas (logo é lenta) e costuma ser disparada em96loop a cada digitação de CEP no checkout, estourando facilmente o `HTTP 429`;97aplique backoff exponencial (1s, 2s, 4s, 8s) e cache por CEP + produtos +98quantidade. CEP segue validação BR: 8 dígitos numéricos, sem traço nem ponto.99100## Endpoints101102| Método | Endpoint | Descrição |103|:--|:--|:--|104| GET | `/shippings/cotation/` | Calcular o frete (preço e prazo) para um ou mais produtos em direção a um CEP de destino |105| GET | `/shippings/` | Listar as formas de envio (métodos de frete) configuradas e ativas na loja, sem cotação |106107**Autenticação:** `?access_token={token}` em **todas** as chamadas — sempre como query parameter, **nunca** em header `Authorization` (header → HTTP 401).108109**URL base:** `https://{api_address}/` — varia por loja, retornada no callback OAuth. Use `TRAY_API_ADDRESS` via variável de ambiente.110111> **Recurso somente leitura:** a skill `tray-frete` expõe apenas endpoints `GET`. Para **criar/configurar** métodos de envio, gateways ou tabelas de CEP, use `tray-configuracao-frete` (`/shippings/method/gateway`, `/shippings/method/zipcode_table`).112113---114115### GET /shippings/cotation/116117Calcula o frete para um ou mais produtos em direção a um CEP de destino, retornando os métodos de envio disponíveis com **preço** e **prazo**. A Tray consulta automaticamente os gateways de frete configurados na loja (ex.: Frete-X API, Correios, transportadoras) e consolida a resposta no formato padrão da API. É a **única** rota que retorna valores de frete.118119**Quando usar:**120121- No checkout ou em página de produto, para exibir as opções de frete ao cliente.122- Para calcular o frete de um carrinho com **múltiplos itens** antes de fechar o pedido.123124**Pré-requisitos:**125126- `access_token` válido como query param (`?access_token={token}`, nunca em header).127- `TRAY_API_ADDRESS` da loja (varia por loja, retornado no callback OAuth).128- `product_id` e `price` de cada produto disponíveis (ver `tray-produtos`).129- CEP de destino com **apenas 8 dígitos numéricos** (sem traço/ponto).130- Quantidade de cada produto.131132**Schema:** sem body — apenas query params: `access_token`, `zipcode`, `products[n][product_id]`, `products[n][price]`, `products[n][quantity]`.133134**Parâmetros (query):**135136| Parâmetro | Tipo | Obrigatório | Formato | Descrição |137|:--|:--|:--|:--|:--|138| `access_token` | string | Sim | — | Token de acesso na query string |139| `zipcode` | string | Sim | `cep` — 8 dígitos numéricos, sem traço/ponto (ex.: `04001001`) | CEP de destino da cotação |140| `products[n][product_id]` | number | Sim | inteiro positivo | ID do produto a cotar; índice `n` inicia em `0` e permite múltiplos produtos na mesma requisição |141| `products[n][price]` | decimal | Sim | decimal com ponto (ex.: `58.90`) | Preço unitário do produto, usado no cálculo de seguro/valor declarado |142| `products[n][quantity]` | number | Sim | inteiro positivo | Quantidade do produto; multiplica peso e dimensões na cotação |143144> O **peso** e as **dimensões** não são parâmetros de query — a Tray usa os valores do cadastro do produto/variação (peso em gramas; ver `tray-produtos` e `tray-variacoes`). Cadastro com peso `0` ou dimensões ausentes faz os Correios aplicarem mínimos (16x11x2 cm) ou retornarem erro, distorcendo o valor.145146**Campos da resposta** (wrapper `Shipping`, array — cada item é um método de envio cotado):147148| Campo | Tipo | Descrição |149|:--|:--|:--|150| `id` | number | ID do método de envio (vem como string na resposta JSON) |151| `name` | string | Nome do método (ex.: `PAC`, `SEDEX`, `Transportadora`) |152| `price` | decimal | Valor do frete em reais; pode vir `0.00` para frete grátis (string na resposta) |153| `delivery_time` | number | Prazo de entrega estimado em dias úteis (string na resposta) |154| `delivery_time_text` | string | Texto formatado do prazo para exibição (ex.: `8 dias úteis`) |155156**Exemplo de resposta:**157158```json159{160 "Shipping": [161 { "id": "1", "name": "PAC", "price": "25.90", "delivery_time": "8", "delivery_time_text": "8 dias úteis" },162 { "id": "2", "name": "SEDEX", "price": "45.50", "delivery_time": "3", "delivery_time_text": "3 dias úteis" }163 ]164}165```166167**Exemplo (curl):**168169```bash170# NÃO-VERIFICADO contra sandbox — validar antes do merge.171# Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.172curl -s -G "https://${TRAY_API_ADDRESS}/shippings/cotation/" \173 --data-urlencode "access_token=${TRAY_ACCESS_TOKEN}" \174 --data-urlencode "zipcode=04001001" \175 --data-urlencode "products[0][product_id]=123" \176 --data-urlencode "products[0][price]=58.90" \177 --data-urlencode "products[0][quantity]=2"178```179180**Exemplo (Node):**181182```js183// NÃO-VERIFICADO contra sandbox — validar antes do merge.184// Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.185const base = process.env.TRAY_API_ADDRESS;186const token = process.env.TRAY_ACCESS_TOKEN;187188// Normaliza o CEP para 8 dígitos numéricos (remove traço/ponto)189const zipcode = "04001-001".replace(/\D/g, "");190191const cart = [192 { product_id: 123, price: "58.90", quantity: 2 },193 { product_id: 456, price: "19.90", quantity: 1 },194];195196const params = new URLSearchParams({ access_token: token, zipcode });197cart.forEach((item, n) => {198 params.append(`products[${n}][product_id]`, String(item.product_id));199 params.append(`products[${n}][price]`, item.price);200 params.append(`products[${n}][quantity]`, String(item.quantity));201});202203const res = await fetch(`https://${base}/shippings/cotation/?${params}`);204if (res.status === 429) throw new Error("Rate limit — aplicar backoff exponencial");205const data = await res.json();206const metodos = data.Shipping ?? [];207if (metodos.length === 0) {208 // Estado legítimo: frete indisponível para a região (CEP fora de cobertura)209 console.log("Frete indisponível para este CEP");210} else {211 metodos.forEach((m) => console.log(`${m.name}: R$ ${m.price} — ${m.delivery_time_text}`));212}213```214215**Erros comuns:**216217| Código | Causa | Como resolver |218|:--|:--|:--|219| `200` (Shipping vazio) | Frete indisponível para a região — CEP fora de cobertura ou transportadora não atende; o array `Shipping` volta vazio ou parcial | Tratar como estado legítimo na UI (`"frete indisponível para este CEP"`); nunca assumir que ao menos um método sempre retorna |220| `400` | CEP enviado com máscara (`04001-001`) ou parâmetros `products[n]` malformados | Normalizar `zipcode` para 8 dígitos numéricos; usar índices `products[0]`, `products[1]`... corretos |221| `401` | `access_token` expirado (3h) ou enviado como header `Authorization` em vez de query param | Renovar via `GET /auth?refresh_token={token}`; sempre passar `?access_token={token}` na query string |222| `404` | `api_address` incorreto (varia por loja) | Usar o `api_address` retornado no callback OAuth da loja |223| `429` | Rate limit (180 req/min ou 10k/dia) — agravado porque a cotação consulta APIs externas e pode ser repetida em loop no checkout | Backoff exponencial (1s, 2s, 4s, 8s) e cache de resultados por CEP + produtos + quantidade + dimensões; debounce na UI |224225---226227### GET /shippings/228229Lista as formas de envio (métodos de frete) configuradas e **ativas** na loja, **sem** realizar cotação. Retorna `id`, `name` e `active` de cada método (ex.: PAC, SEDEX, Retirada na Loja). Não retorna preço nem prazo — para isso use `/shippings/cotation/`.230231**Quando usar:**232233- Para descobrir quais métodos de envio existem na loja antes de exibir/filtrar opções.234- Para mapear IDs de método — necessário, por exemplo, ao vincular frete a um cupom via `shipping_relationship` (ver `tray-cupons`).235236**Pré-requisitos:**237238- `access_token` válido como query param.239- `TRAY_API_ADDRESS` da loja.240241**Schema:** sem body — apenas query param `access_token`.242243**Parâmetros (query):**244245| Parâmetro | Tipo | Obrigatório | Descrição |246|:--|:--|:--|:--|247| `access_token` | string | Sim | Token de acesso na query string |248249**Campos da resposta** (wrapper `ShippingMethods[].ShippingMethod`):250251| Campo | Tipo | Descrição |252|:--|:--|:--|253| `id` | number | ID do método de envio (string na resposta); usável em outros recursos (ex.: `shipping_relationship` em `tray-cupons`) |254| `name` | string | Nome do método (ex.: `PAC`, `SEDEX`, `Retirada na Loja`) |255| `active` | string | Status: `'1'` = ativo, `'0'` = inativo |256257**Exemplo de resposta:**258259```json260{261 "ShippingMethods": [262 { "ShippingMethod": { "id": "1", "name": "PAC", "active": "1" } },263 { "ShippingMethod": { "id": "2", "name": "SEDEX", "active": "1" } },264 { "ShippingMethod": { "id": "3", "name": "Retirada na Loja", "active": "1" } }265 ]266}267```268269**Exemplo (curl):**270271```bash272# NÃO-VERIFICADO contra sandbox — validar antes do merge.273# Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.274curl -s "https://${TRAY_API_ADDRESS}/shippings/?access_token=${TRAY_ACCESS_TOKEN}"275```276277**Exemplo (Node):**278279```js280// NÃO-VERIFICADO contra sandbox — validar antes do merge.281// Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.282const base = process.env.TRAY_API_ADDRESS;283const token = process.env.TRAY_ACCESS_TOKEN;284285const res = await fetch(`https://${base}/shippings/?access_token=${token}`);286if (res.status === 429) throw new Error("Rate limit — aplicar backoff exponencial");287const data = await res.json();288289const metodos = (data.ShippingMethods ?? [])290 .map((m) => m.ShippingMethod)291 .filter((m) => m.active === "1");292293metodos.forEach((m) => console.log(`#${m.id} ${m.name}`));294```295296**Erros comuns:**297298| Código | Causa | Como resolver |299|:--|:--|:--|300| `401` | Token expirado ou enviado em header `Authorization` em vez de query param | Renovar token; usar query param `?access_token={token}` |301| `404` | `api_address` incorreto (varia por loja) | Usar o `api_address` retornado no callback OAuth |302| `429` | Rate limit (180 req/min ou 10k/dia) | Backoff exponencial (1s, 2s, 4s, 8s) |303304305## Edge cases306307- **Frete indisponível para a região (array `Shipping` vazio ou parcial).** `GET /shippings/cotation/` pode retornar `HTTP 200` com `{"Shipping": []}` ou apenas um subconjunto dos métodos quando o CEP está fora de cobertura ou uma transportadora não atende àquela região. Não é erro: é um estado legítimo. Trate o array vazio na UI exibindo "frete indisponível para este CEP" em vez de assumir que pelo menos um método sempre volta.308 - Exemplo: cotação para `zipcode=69900970` (Rio Branco/AC) com produto pesado pode voltar `{"Shipping": []}` enquanto o mesmo produto para `zipcode=04001001` (São Paulo) retorna PAC e SEDEX. O checkout deve degradar para "consulte outras formas de envio", não travar.309 ```js310 // NÃO-VERIFICADO contra sandbox — validar antes do merge.311 const data = await res.json();312 const metodos = data.Shipping ?? [];313 if (metodos.length === 0) {314 return { disponivel: false, motivo: "sem cobertura para o CEP informado" };315 }316 ```317318- **Múltiplos produtos no mesmo carrinho exigem índice incremental.** A rota aceita `products[0]`, `products[1]`, `products[2]`... na mesma requisição, e cada índice precisa ser único. Omitir o índice ou repetir `products[0]` para itens diferentes faz a Tray cotar apenas um produto, gerando frete subdimensionado (peso e dimensões somados a menos). Sempre incremente o índice por item do carrinho.319 - Exemplo errado (sobrescreve o item 0): `products[0][product_id]=123&products[0][product_id]=456` → a Tray considera só `456`. Correto: `products[0][product_id]=123&products[1][product_id]=456`.320 ```bash321 # NÃO-VERIFICADO contra sandbox — validar antes do merge.322 curl -s -G "https://${TRAY_API_ADDRESS}/shippings/cotation/" \323 --data-urlencode "access_token=${TRAY_ACCESS_TOKEN}" \324 --data-urlencode "zipcode=04001001" \325 --data-urlencode "products[0][product_id]=123" \326 --data-urlencode "products[0][price]=58.90" \327 --data-urlencode "products[0][quantity]=2" \328 --data-urlencode "products[1][product_id]=456" \329 --data-urlencode "products[1][price]=120.00" \330 --data-urlencode "products[1][quantity]=1"331 ```332333- **Frete grátis sobrescrevendo o preço (`price='0.00'`).** Produtos cadastrados com `free_shipping=1` (ver `tray-produtos`) podem retornar `price='0.00'` em alguns métodos, dependendo da configuração da loja. O código de checkout não deve recalcular nem rejeitar frete zerado como erro de cotação — é o comportamento esperado para esses produtos.334 - Exemplo: produto com `free_shipping=1` retorna `{"id":"1","name":"PAC","price":"0.00","delivery_time":"8"}`. Tratar `price === "0.00"` como "Grátis" na UI, não como cotação falha. Atenção: o `price` vem como **string** na resposta — compare como string ou converta com `parseFloat` antes de operações numéricas.335336- **Peso e dimensões herdados do cadastro do produto/variação.** A cotação **não** recebe peso/dimensões como parâmetros de query — usa os valores do cadastro do produto (`weight` em gramas) e da variação (ver `tray-produtos`/`tray-variacoes`). Cadastro com `weight=0` ou dimensões ausentes faz os Correios aplicarem mínimos (16x11x2 cm) ou retornarem erro, distorcendo o valor cotado para mais ou para menos.337 - Exemplo: produto cadastrado com `weight=0` cota como pacote mínimo dos Correios e devolve um frete artificialmente baixo; ao faturar, o custo real é maior. Auditar o `weight`/dimensões em `tray-produtos` antes de confiar na cotação. Lembre que `1 kg = 1000 g`.338339- **Latência e rate limit ao cotar a cada tecla no campo de CEP.** Como a cotação consulta APIs externas (gateways/transportadoras como Frete-X API), é lenta e fácil de disparar em loop a cada caractere digitado no campo de CEP, estourando o limite de 180 req/min (`HTTP 429`). Faça **debounce** na UI (cotar só após o CEP completo, 8 dígitos) e **cache** por `CEP + produtos + quantidade`.340 ```js341 // NÃO-VERIFICADO contra sandbox — validar antes do merge.342 const cache = new Map(); // chave: cep|product_id:qty|...343 async function cotar(cep, itens) {344 if (cep.replace(/\D/g, "").length !== 8) return null; // só cota CEP completo345 const chave = `${cep}|${itens.map(i => `${i.id}:${i.qty}`).join("|")}`;346 if (cache.has(chave)) return cache.get(chave);347 const res = await fetch(/* ... */);348 if (res.status === 429) { /* backoff exponencial: 1s, 2s, 4s, 8s */ }349 const data = await res.json();350 cache.set(chave, data.Shipping ?? []);351 return cache.get(chave);352 }353 ```354355- **CEP com máscara quebra a cotação.** Enviar `zipcode` com traço ou ponto (`04001-001`) pode resultar em cotação vazia ou `HTTP 400`. O campo é do formato `cep` da plataforma: 8 dígitos numéricos, sem traço nem ponto. Normalize com `String(cep).replace(/\D/g, "")` antes de chamar e valide que sobraram exatamente 8 dígitos.356 - Exemplo: `04001-001` → normalizar para `04001001`. Um CEP com 7 ou 9 dígitos após a limpeza indica entrada inválida — rejeite antes de gastar uma chamada à API.357358- **Confundir `/shippings/` com `/shippings/cotation/`.** `GET /shippings/` apenas **lista** os métodos ativos da loja (`id`, `name`, `active`) — **sem** preço nem prazo. Apenas `GET /shippings/cotation/` calcula valores. Chamar `/shippings/` esperando preços retorna só `id`/`name`/`active` e leva o desenvolvedor a achar que a cotação "veio sem preço".359 - Exemplo: para montar o seletor de frete com valores no checkout, use `cotation/`; para descobrir os IDs de método (ex.: vincular frete a um cupom via `shipping_relationship`, ver `tray-cupons`), use `/shippings/`.360361## Antipadrões362363- ❌ **Enviar o `access_token` em header `Authorization`.** A API Tray ignora o header `Authorization: Bearer ...` e responde `HTTP 401` tanto em `/shippings/` quanto em `/shippings/cotation/`. O token **deve** ir como query param `?access_token={token}`. Por quê quebra: a autenticação da Tray é por query string, não por header — o header simplesmente não é lido. Correção: monte sempre a URL com `?access_token=${TRAY_ACCESS_TOKEN}` (ou `--data-urlencode "access_token=..."` no `curl -G`) e nunca passe credenciais via `headers`.364 ```bash365 # NÃO-VERIFICADO contra sandbox — validar antes do merge.366 # ERRADO → HTTP 401:367 # curl -H "Authorization: Bearer $TRAY_ACCESS_TOKEN" "https://${TRAY_API_ADDRESS}/shippings/"368 # CERTO:369 curl -s "https://${TRAY_API_ADDRESS}/shippings/?access_token=${TRAY_ACCESS_TOKEN}"370 ```371372- ❌ **Hardcodar o `api_address` de uma loja como base fixa.** O `api_address` **varia por loja** (é retornado no callback OAuth da Etapa 2). Reaproveitar o endereço de outra loja gera `HTTP 404` em `/shippings/cotation/`. Por quê quebra: cada loja tem um host de API próprio; um endereço fixo só funciona para uma loja e falha silenciosamente para as demais. Correção: armazene o `api_address` junto com os tokens da loja e leia-o sempre de `TRAY_API_ADDRESS` por variável de ambiente, nunca como literal no código.373374- ❌ **Cotar a cada tecla no campo de CEP sem debounce nem cache.** Disparar `GET /shippings/cotation/` a cada caractere digitado estoura o rate limit (`HTTP 429`) e degrada o checkout, porque cada cotação consulta transportadoras externas e é lenta. Por quê quebra: 180 req/min se esgotam rápido quando vários clientes digitam CEP simultaneamente, e a latência externa empilha requisições. Correção: cote apenas com o CEP completo (8 dígitos), aplique debounce na UI e cacheie por `CEP + produtos + quantidade`; em `429`, aplique backoff exponencial (1s, 2s, 4s, 8s).375376- ❌ **Tratar `Shipping` vazio como falha genérica e abortar o checkout.** Um array `Shipping` vazio com `HTTP 200` é um estado legítimo (região sem cobertura ou transportadora que não atende). Por quê quebra: lançar erro 500/genérico ao receber `[]` impede a compra de clientes em regiões parcialmente atendidas e mascara a causa real. Correção: detecte `Shipping.length === 0` e exiba "frete indisponível para este CEP" (ou ofereça retirada na loja, se houver), mantendo o checkout vivo.377378- ❌ **Esquecer de incrementar o índice em `products[n]` com múltiplos itens.** Repetir `products[0]` para produtos diferentes ou omitir o índice faz a Tray cotar **apenas um** produto, devolvendo frete subdimensionado. Por quê quebra: o índice `n` é o identificador de posição do item no array; sem incrementá-lo, os itens posteriores sobrescrevem o primeiro e peso/dimensões somados ficam menores que o real. Correção: gere `products[0]`, `products[1]`, `products[2]`... de forma incremental, um bloco `product_id`/`price`/`quantity` por item do carrinho.379380- ❌ **Hardcodar tokens literais no código (curl/Node).** Nunca escreva o `access_token` (nem `consumer_key`/`consumer_secret`) como string no código de cotação. Por quê quebra: além do risco de vazamento em logs/repositório, o `access_token` expira em 3 horas — um literal fica inválido e gera `HTTP 401`. Correção: use `TRAY_ACCESS_TOKEN` e `TRAY_API_ADDRESS` via variável de ambiente e renove o token via `GET /auth?refresh_token={token}` antes de expirar.381382- ❌ **Usar a skill `tray-frete` para criar/configurar métodos de envio ou tabelas de CEP.** Este recurso é **somente leitura** — apenas endpoints `GET` (`/shippings/` e `/shippings/cotation/`). Por quê quebra: não existe `POST`/`PUT` de método de frete aqui; tentar criar/configurar pela API de Frete não tem endpoint correspondente. Correção: configuração de métodos, gateway e tabela de CEP fica em `tray-configuracao-frete` (`/shippings/method/gateway`, `/shippings/method/zipcode_table`); use `tray-frete` apenas para cotar e listar métodos ativos.383384- ❌ **Tratar os campos numéricos da resposta como `number`.** Na resposta da cotação, `id`, `price` e `delivery_time` vêm como **string** (ex.: `"price": "25.90"`, `"delivery_time": "8"`), e `active` em `/shippings/` vem como `"0"`/`"1"`. Por quê quebra: somar `price` diretamente concatena strings (`"25.90" + "10.00" = "25.9010.00"`) e comparações numéricas falham. Correção: converta explicitamente com `parseFloat(price)`/`parseInt(delivery_time, 10)` antes de qualquer cálculo, e compare `active === "1"` como string ao filtrar métodos ativos.385386## Webhooks relacionados387388A skill `tray-frete` é **somente leitura** (apenas `GET`) e **não dispara webhooks próprios**: não existe escopo `shipping` no [sistema de notificação](../webhooks/SKILL.md). Cotação e listagem de métodos são consultas síncronas — o resultado só existe no momento da chamada e não gera evento assíncrono.389390Os webhooks que **afetam indiretamente** o frete vêm de outros recursos:391392| Escopo | Ações | Por que afeta o frete |393|:--|:--|:--|394| `product` | insert, update, delete | Alterações de **peso**, **dimensões** ou `free_shipping` no cadastro mudam o valor cotado em `/shippings/cotation/`. Reaja invalidando o cache de cotação do produto afetado (`scope_id`). |395| `variant` | insert, update, delete | A cotação herda peso/dimensões da variação; mudanças alteram o frete da variação cotada. |396| `store_config` | update | Inclui alteração de configuração de frete da loja (gateway, métodos ativos, frete grátis). Invalide qualquer cache de `/shippings/` e de cotação após esse evento. |397| `order` | insert, update | O pedido carrega `shipping_cost`, `shipping_method` e `tracking_number`; o frete já cotado é congelado no pedido. Use este escopo para sincronizar status de envio, não a skill de frete. |398399> **Não há escopo `shipping`.** Para reagir a mudanças que impactam frete, escute `product`, `variant` e `store_config` e invalide os caches de cotação correspondentes. Detalhes de payload, ativação e retry em [`../webhooks/SKILL.md`](../webhooks/SKILL.md).400401## Glossário402403| Termo | Definição |404|:--|:--|405| cotação (cotation) | Cálculo do valor e prazo de frete para um conjunto de produtos rumo a um CEP, via `GET /shippings/cotation/`. É a **única** rota que retorna preço e prazo. |406| método de envio (ShippingMethod) | Forma de entrega configurada e ativa na loja (PAC, SEDEX, transportadora, retirada na loja), listada por `GET /shippings/` com `id`, `name` e `active`. |407| gateway de frete | Integração externa (ex.: Frete-X API) que a Tray consulta automaticamente na cotação para obter preços/prazos de múltiplas transportadoras; configurado no painel ou via `tray-configuracao-frete`. |408| Frete-X API | Gateway de frete suportado pela Tray para cotação automática com múltiplas transportadoras; configurado no painel administrativo da loja. |409| `delivery_time` | Prazo de entrega estimado em dias úteis retornado por método na resposta de cotação; acompanha `delivery_time_text` para exibição. |410| `free_shipping` | Flag do cadastro do produto (`tray-produtos`) que, conforme a configuração da loja, pode zerar o `price` do frete na cotação. |411| `zipcode` | CEP de destino da cotação; enviado apenas com 8 dígitos numéricos, sem traço ou ponto. |412| `shipping_id` | ID do método de envio (campo `id` de `/shippings/` ou da cotação); usado em outros recursos como vínculo de frete em cupons (`shipping_relationship`, ver `tray-cupons`). |413| `api_address` | Host base da API específico de cada loja, retornado no callback OAuth; compõe a URL `https://{api_address}/` de toda chamada. |414| peso cúbico (`cubic_weight`) | Peso volumétrico calculado a partir das dimensões do produto, usado por transportadoras quando excede o peso real; deriva do cadastro do produto e influencia o valor cotado. |415416## Referências417418- **Documentação oficial:** [API de Integração de Frete](https://developers.tray.com.br/#api-de-integracao-de-frete)419- **Visão geral e regras invariantes:** [`../visao-geral/SKILL.md`](../visao-geral/SKILL.md)420- **Autenticação OAuth e renovação de token:** [`../autorizacao/SKILL.md`](../autorizacao/SKILL.md)421- **Webhooks (escopos `product`, `variant`, `store_config`, `order`):** [`../webhooks/SKILL.md`](../webhooks/SKILL.md)422- **Configurar métodos de envio, gateway e tabela de CEP (escrita):** [`../configuracao-frete/SKILL.md`](../configuracao-frete/SKILL.md)423- **Peso, dimensões e `free_shipping` do produto:** [`../produtos/SKILL.md`](../produtos/SKILL.md)424- **Peso/dimensões de variação:** [`../variacoes/SKILL.md`](../variacoes/SKILL.md)425- **Vínculo de frete a cupons (`shipping_relationship`):** [`../cupons/SKILL.md`](../cupons/SKILL.md)426- **Custo de frete e rastreamento no pedido:** [`../pedidos/SKILL.md`](../pedidos/SKILL.md)427- **Issue de aprofundamento:** ai/tasks#100 (P2.1, Fase 2)428429430## Fluxo recomendado de cotação no checkout431432A cotação de frete é, na prática, a operação mais sensível desta skill: ela é433disparada repetidamente no checkout (a cada edição do campo de CEP), depende de434APIs externas lentas (gateways/transportadoras) e estoura `HTTP 429` com435facilidade. O fluxo recomendado encadeia **normalização de CEP → debounce →436cache por (cep + itens) com TTL → chamada com retry/backoff em 429 → parsing de437`Shipping[]` → escolha do método mais barato/rápido**. O bloco abaixo é um438módulo completo e comentado que cobre todas essas etapas.439440```js441// NÃO-VERIFICADO contra sandbox — validar antes do merge.442// Requer TRAY_ACCESS_TOKEN e TRAY_API_ADDRESS no ambiente.443//444// Módulo de cotação de frete para checkout: debounce, cache com TTL,445// retry com backoff exponencial em 429, normalização de CEP e parsing.446447const BASE = process.env.TRAY_API_ADDRESS; // host por loja (callback OAuth)448const TOKEN = process.env.TRAY_ACCESS_TOKEN; // nunca literal; renovar em 3h449450// ---------------------------------------------------------------------------451// 1) Normalização de CEP — 8 dígitos numéricos, sem traço/ponto.452// Retorna null se a entrada não tiver exatamente 8 dígitos após a limpeza.453// ---------------------------------------------------------------------------454function normalizarCep(input) {455 const digitos = String(input ?? "").replace(/\D/g, "");456 return digitos.length === 8 ? digitos : null; // 7 ou 9 dígitos = inválido457}458459// ---------------------------------------------------------------------------460// 2) Chave de cache estável por (cep + itens).461// Ordena os itens por product_id para que a mesma combinação de carrinho462// gere sempre a mesma chave, independente da ordem de inserção.463// ---------------------------------------------------------------------------464function chaveCache(cep, itens) {465 const assinatura = [...itens]466 .map((i) => ({ id: Number(i.product_id), q: Number(i.quantity), p: String(i.price) }))467 .sort((a, b) => a.id - b.id)468 .map((i) => `${i.id}:${i.q}:${i.p}`)469 .join("|");470 return `${cep}#${assinatura}`;471}472473// ---------------------------------------------------------------------------474// 3) Cache em memória com TTL. Cotação muda pouco em janelas curtas, então475// um TTL de poucos minutos reduz drasticamente as chamadas externas.476// ---------------------------------------------------------------------------477const TTL_MS = 5 * 60 * 1000; // 5 minutos478const cache = new Map(); // chave -> { expira: epochMs, valor: Shipping[] }479480function lerCache(chave) {481 const hit = cache.get(chave);482 if (!hit) return null;483 if (Date.now() > hit.expira) {484 cache.delete(chave); // expirado: descarta e força nova cotação485 return null;486 }487 return hit.valor;488}489490function gravarCache(chave, valor) {491 cache.set(chave, { expira: Date.now() + TTL_MS, valor });492}493494// ---------------------------------------------------------------------------495// 4) Monta os índices products[n][...] incrementando n por item do carrinho.496// NUNCA repetir products[0] — itens posteriores sobrescreveriam o primeiro497// e o frete viria subdimensionado (peso/dimensões somados a menos).498// ---------------------------------------------------------------------------499function montarParams(cep, itens) {500 const params = new URLSearchParams({ access_token: TOKEN, zipcode: cep });501 itens.forEach((item, n) => {502 params.append(`products[${n}][product_id]`, String(item.product_id));503 params.append(`products[${n}][price]`, String(item.price)); // ponto decimal504 params.append(`products[${n}][quantity]`, String(item.quantity));505 });506 return params;507}508509// ---------------------------------------------------------------------------510// 5) Chamada com retry e backoff exponencial em 429.511// Em 401/404 não adianta repetir — falha rápido com mensagem clara.512// O access_token vai SEMPRE na query string (params), nunca em header.513// ---------------------------------------------------------------------------514async function cotarComRetry(cep, itens, { maxTentativas = 4 } = {}) {515 const params = montarParams(cep, itens);516 const url = `https://${BASE}/shippings/cotation/?${params}`;517518 for (let tentativa = 0; tentativa < maxTentativas; tentativa++) {519 const res = await fetch(url); // GET; sem header Authorization520 if (res.status === 429) {521 // backoff exponencial: 1s, 2s, 4s, 8s...522 const esperaMs = 1000 * 2 ** tentativa;523 await new Promise((r) => setTimeout(r, esperaMs));524 continue;525 }526 if (res.status === 401) {527 throw new Error("401 — access_token expirado/em header. Renovar via GET /auth?refresh_token e usar query param.");528 }529 if (res.status === 404) {530 throw new Error("404 — api_address incorreto (varia por loja). Usar TRAY_API_ADDRESS da loja.");531 }532 if (!res.ok) {533 throw new Error(`Cotação falhou: HTTP ${res.status}`);534 }535 const data = await res.json();536 return data.Shipping ?? []; // array vazio = sem cobertura (estado legítimo)537 }538 throw new Error("429 persistente após backoff — reduzir frequência de cotação.");539}540541// ---------------------------------------------------------------------------542// 6) Orquestrador: normaliza, consulta cache, cota e grava no cache.543// Retorna { disponivel, metodos } — disponivel=false quando Shipping[] vazio.544// ---------------------------------------------------------------------------545async function cotarFrete(cepBruto, itens) {546 const cep = normalizarCep(cepBruto);547 if (!cep) return { disponivel: false, motivo: "cep_invalido", metodos: [] };548 if (!itens?.length) return { disponivel: false, motivo: "carrinho_vazio", metodos: [] };549550 const chave = chaveCache(cep, itens);551 const cacheado = lerCache(chave);552 if (cacheado) return { disponivel: cacheado.length > 0, motivo: cacheado.length ? null : "sem_cobertura", metodos: cacheado };553554 const metodos = await cotarComRetry(cep, itens);555 gravarCache(chave, metodos);556 return { disponivel: metodos.length > 0, motivo: metodos.length ? null : "sem_cobertura", metodos };557}558559// ---------------------------------------------------------------------------560// 7) Parsing/seleção: a resposta vem com campos numéricos como STRING.561// Converta antes de comparar. Escolhe mais barato e mais rápido.562// ---------------------------------------------------------------------------563function escolherMetodos(metodos) {564 const normalizados = metodos.map((m) => ({565 id: m.id,566 name: m.name,567 price: parseFloat(m.price), // "25.90" -> 25.90568 prazo: parseInt(m.delivery_time, 10), // "8" -> 8569 prazoTexto: m.delivery_time_text,570 gratis: parseFloat(m.price) === 0, // free_shipping pode zerar571 }));572 const maisBarato = [...normalizados].sort((a, b) => a.price - b.price || a.prazo - b.prazo)[0] ?? null;573 const maisRapido = [...normalizados].sort((a, b) => a.prazo - b.prazo || a.price - b.price)[0] ?? null;574 return { todos: normalizados, maisBarato, maisRapido };575}576577// ---------------------------------------------------------------------------578// 8) Debounce do campo de CEP: só cota após o usuário parar de digitar e579// apenas quando o CEP estiver completo (8 dígitos). Evita disparar uma580// cotação por tecla (causa direta de 429 no checkout).581// ---------------------------------------------------------------------------582function criarCotadorDebounced(onResultado, atrasoMs = 500) {583 let timer = null;584 return function aoDigitarCep(cepBruto, itens) {585 clearTimeout(timer);586 const cep = normalizarCep(cepBruto);587 if (!cep) return; // não cota CEP incompleto/inválido — economiza chamadas588 timer = setTimeout(async () => {589 try {590 const resultado = await cotarFrete(cep, itens);591 onResultado(escolherMetodos(resultado.metodos), resultado);592 } catch (err) {593 onResultado(null, { disponivel: false, motivo: "erro", erro: String(err) });594 }595 }, atrasoMs);596 };597}598599// Exemplo de uso no checkout:600// const cotar = criarCotadorDebounced((selecao, resultado) => {601// if (!resultado.disponivel) return mostrarUi("Frete indisponível para este CEP");602// mostrarUi(`Mais barato: ${selecao.maisBarato.name} R$ ${selecao.maisBarato.price}`);603// });604// inputCep.addEventListener("input", (e) => cotar(e.target.value, carrinhoAtual));605```606607## Integração com gateway de frete (Frete-X / transportadoras)608609A cotação da Tray é um **agregador**: quando o checkout chama610`GET /shippings/cotation/`, a plataforma consulta de uma só vez todas as fontes611de frete habilitadas na loja e devolve o resultado consolidado no array612`Shipping[]`, com o mesmo formato para qualquer origem. Existem duas classes de613origem:614615- **Métodos nativos** — PAC e SEDEX (Correios), retirada na loja e tabelas de616 frete por faixa de CEP configuradas na própria loja. São resolvidos617 diretamente pela Tray a partir do peso/dimensões do cadastro do produto.618- **Gateway de frete** — integrações externas como a **Frete-X API**, que por619 sua vez cotam com múltiplas transportadoras (Jadlog, Loggi, Total Express,620 etc.). A Tray repassa peso, dimensões, valor declarado e CEP ao gateway, e o621 gateway devolve uma lista de opções por transportadora.622623Do ponto de vista do consumidor da API, **não há distinção de formato**: tanto624um método nativo quanto uma transportadora vinda de gateway aparecem como um625item de `Shipping[]` com `id`, `name`, `price`, `delivery_time` e626`delivery_time_text`. O `name` é o que diferencia na prática — pode vir como627`"PAC"`, `"SEDEX"`, `"Jadlog - Package"` ou `"Loggi Econômico"`, conforme a628transportadora retornada pelo gateway.629630| Aspecto | Método nativo (PAC/SEDEX) | Gateway de frete (Frete-X) |631|:--|:--|:--|632| Origem do cálculo | Correios / tabela da loja, resolvido pela Tray | Gateway externo consultando N transportadoras |633| Latência típica | Baixa/média | Mais alta (rede + API da transportadora) |634| `name` na resposta | `PAC`, `SEDEX`, `Retirada na Loja` | Nome da transportadora/serviço (`Jadlog - Package`) |635| Quantidade de opções | Fixa (poucos métodos) | Variável — depende de quantas transportadoras responderam |636| Sensível a peso/dimensões zerados | Sim (aplica mínimos dos Correios) | Sim (gateway pode rejeitar item sem peso) |637| Configuração | `tray-configuracao-frete` (`/shippings/method/...`) | Painel da loja + gateway externo; também via `tray-configuracao-frete` |638639**Campos extras possíveis.** O contrato estável de `Shipping[]` é640`id`/`name`/`price`/`delivery_time`/`delivery_time_text`. Cotações vindas de641gateway podem trazer campos adicionais (ex.: nome de transportadora separado,642código de serviço interno, observação de prazo) dependendo da configuração — não643dependa da presença desses campos extras; programe sempre contra o contrato644mínimo e trate qualquer campo adicional como opcional.645646**Fallback quando o gateway está indisponível.** Como o gateway é uma dependência647externa, ele pode falhar ou expirar (timeout) s648649…(truncated)