MANDATORY: Tool Calls Required Before Answering
Estas chamadas são OBRIGATÓRIAS, não opcionais. Execute-as antes de gerar qualquer código ou payload. 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=cupons "<termo da pergunta>"
<TOPIC_SLUG>: ver tabela emskills/tray-dev/SKILL.md.- Use os trechos retornados como fonte primária; este SKILL.md é resumo denso.
2. Revisar campos (este recurso ainda NÃO tem validate.mjs)
Nota: o recurso
cuponsainda não possuiscripts/validate.mjslocal. A chamada OBRIGATÓRIA asearch_docs.mjsacima continua valendo. Como não há validador automático, você é responsável por revisar manualmente cada campo obrigatório (code,value,type) contra a doc retornada porsearch_docs.mjse contra os schemas de referência emskills/cupons/schemas/antes de retornar qualquer código. Confira em especial: chave de recursoDiscountCouponpresente no body,codesem espaços nem acentos,typeem$/%, e coerência entreusage_counter_limiteusage_counter_limit_customer.
Antes de responder
Execute estas verificações antes de gerar qualquer payload ou código:
- Confirme o método HTTP e o endpoint correto para a operação solicitada
(CRUD em
/discount_coupons, consulta de relacionamento em/discount_coupons/<tipo>_relationship/:id, ou criação de vínculo em/discount_coupons/create_relationship/:id). - Identifique os campos obrigatórios listados neste documento —
code,valueetypenunca podem faltar na criação; não omita nenhum. - Verifique que
access_tokennão aparece como literal string no código gerado — use sempreTRAY_ACCESS_TOKENeTRAY_API_ADDRESSpor variável de ambiente. - Confirme que esta é a skill correta para o recurso: cupom é desconto por
código no checkout; se for preço permanente por grupo, leia
when_not_to_usee redirecione paratray-listas-preco-b2b.
Cupons de Desconto — API Tray
Documentação oficial: https://developers.tray.com.br/#api-de-cupom
Atenção (disambiguation): o endpoint base é
/discount_coupons(não/coupons) e o wrapper de payload/resposta éDiscountCoupon(nãoCoupon). Trocar o wrapper ou o caminho é a causa #1 deHTTP 400/404neste recurso.
Visão geral
Um cupom de desconto (discount_coupon) é um código textual que o cliente
digita no checkout para obter uma vantagem: desconto percentual (type=%),
desconto em valor fixo em reais (type=$), frete grátis/desconto de frete
(freight_application), ou ainda um produto como brinde (coupon_type=presente).
A API expõe 21 endpoints: cinco de CRUD (GET /discount_coupons,
GET /discount_coupons/:id, POST /discount_coupons, PUT /discount_coupons/:id,
DELETE /discount_coupons/:id), seis de consulta de relacionamento por tipo
(customer_relationship, product_relationship, category_relationship,
brand_relationship, shipping_relationship, gift_relationship) e o endpoint
único de criação de vínculos POST /discount_coupons/create_relationship/:id,
cujo tipo de relacionamento é definido pela chave-wrapper do corpo. O cupom é o
mecanismo de desconto promocional transacional da Tray — distinto de preço
promocional direto no produto (campo promotional_price, ver tray-produtos) e
de preço permanente por grupo de cliente (ver tray-listas-preco-b2b).
O recurso se conecta a vários outros pontos do fluxo Tray. Os campos
coupon_type (loja/cliente/troca/presente) e local_application
(loja/produtos/categorias/marcas) determinam qual endpoint de
relacionamento se aplica e quais IDs externos precisam existir antes: vincular
clientes exige customer_id válidos (ver tray-clientes); vincular produtos,
product_id (ver tray-produtos); categorias, category_id (ver
tray-categorias); marcas, brand_id (ver tray-marcas); e relacionamento de
frete exige shipping_id de um método de envio já existente na loja (ver
tray-frete/tray-configuracao-frete). O cupom de troca (coupon_type=troca) é
gerado a partir de um order_id existente (ver tray-pedidos). O uso do cupom
materializa-se em um pedido — o desconto aplicado aparece nos campos
coupon_code/discount do pedido (ver tray-pedidos); não há escopo de webhook
próprio para cupom, então alterações de pedido com cupom chegam via escopo
order (ver tray-webhooks).
Invariantes da plataforma que valem para toda chamada deste recurso:
(1) o access_token é passado sempre como query parameter
(?access_token={token}), nunca em header Authorization — token em header
retorna HTTP 401; (2) a URL base é https://{api_address}/, e o
api_address varia por loja (retornado no callback OAuth) — usar o
endereço errado retorna HTTP 404; (3) todo body de POST/PUT deve estar
envolto na chave de recurso DiscountCoupon; (4) listagens paginam com limit
(padrão 30, máximo 50) e page, lendo paging.total para iterar;
(5) datas usam YYYY-MM-DD (validade starts_at/ends_at, horário de
Brasília) e timestamps YYYY-MM-DD HH:MM:SS; (6) o rate limit é 180 req/min e
10.000 req/dia — HTTP 429 exige backoff exponencial (1s, 2s, 4s, 8s) e, em
vínculos em massa via create_relationship, lotes de no máximo 100 registros
por chamada com pausa entre eles. Como o recurso ainda não tem validate.mjs,
valide manualmente code (alfanumérico, sem espaços/acentos, único na loja),
type ($ ou %), value (decimal) e a coerência
usage_counter_limit >= usage_counter_limit_customer antes de enviar.
Endpoints
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /discount_coupons |
Listar cupons de desconto da loja com paginação e filtros |
| GET | /discount_coupons/:id |
Consultar os detalhes de um cupom específico |
| POST | /discount_coupons |
Criar um novo cupom de desconto |
| PUT | /discount_coupons/:id |
Atualizar os dados de um cupom existente |
| DELETE | /discount_coupons/:id |
Excluir um cupom de desconto |
Autenticação: ?access_token={token} em todas as chamadas — sempre como query parameter, nunca em header Authorization. URL base https://{api_address}/ varia por loja (retornada no callback OAuth). Payloads de POST/PUT envolvidos na chave de recurso "DiscountCoupon".
GET /discount_coupons
Quando usar: para listar cupons existentes, descobrir o
coupon_typee olocal_applicationde cada cupom (que indicam qual endpoint de relacionamento consultar) e auditar campanhas ativas.Pré-requisitos:
access_tokenválido como query param.TRAY_API_ADDRESSda loja (varia por loja, retornado no callback OAuth).
Schema do request: sem body — apenas query params de paginação/filtro (ver tabela abaixo).
Schema da response: resposta JSON padrão da API Tray (ver exemplo abaixo).
Paginação:
limit(padrão 30, máximo 50),page.Campos da resposta:
Campo Tipo Descrição paging.totalinteger Total de cupons disponíveis paging.pageinteger Página atual paging.limitinteger Itens por página solicitados paging.maxLimitinteger Teto de itens por página (50) DiscountCoupons[].DiscountCoupon.idstring ID do cupom DiscountCoupons[].DiscountCoupon.codestring Código digitado no checkout DiscountCoupons[].DiscountCoupon.valuedecimal Valor do desconto DiscountCoupons[].DiscountCoupon.typestring $(reais) ou%(percentual)DiscountCoupons[].DiscountCoupon.starts_atdate Início da validade DiscountCoupons[].DiscountCoupon.ends_atdate Fim da validade DiscountCoupons[].DiscountCoupon.coupon_typestring loja/cliente/troca/presenteDiscountCoupons[].DiscountCoupon.local_applicationstring loja/produtos/marcas/categoriasDiscountCoupons[].DiscountCoupon.freight_applicationstring nao_aplicavel/desconto/frete_gratisDiscountCoupons[].DiscountCoupon.usage_counter_limitinteger Limite total de usos DiscountCoupons[].DiscountCoupon.usage_counter_limit_customerinteger Limite de usos por cliente Exemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X GET \ "https://${TRAY_API_ADDRESS}/discount_coupons?access_token=${TRAY_ACCESS_TOKEN}&limit=30&page=1"Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge. const res = await fetch( `https://${process.env.TRAY_API_ADDRESS}/discount_coupons` + `?access_token=${process.env.TRAY_ACCESS_TOKEN}&limit=30&page=1`, ); if (res.status === 429) { /* backoff exponencial: 1s, 2s, 4s, 8s */ } const data = await res.json(); // data.paging.total para paginar; data.DiscountCoupons[].DiscountCouponErros comuns:
Código Causa Como resolver 401 access_tokenexpirado (3h) ou enviado como headerAuthorizationem vez de query paramRenovar via GET /auth?refresh_token={token}; sempre passar?access_token={token}na query string404 api_addressincorreto (varia por loja)Usar o api_addressretornado no callback OAuth da loja429 Rate limit (180 req/min ou 10k/dia) Backoff exponencial (1s, 2s, 4s, 8s)
GET /discount_coupons/:id
Quando usar: para obter a configuração completa de um único cupom (validade, valor, tipo, limites) antes de editar ou diagnosticar por que não foi aplicado.
Pré-requisitos:
access_tokenválido.iddo cupom (obtido viaGET /discount_coupons).
Schema do request: sem body — apenas parâmetro de path
:id.Schema da response: resposta JSON padrão da API Tray (ver exemplo abaixo).
Campos da resposta:
Campo Tipo Descrição DiscountCoupon.idstring ID do cupom DiscountCoupon.codestring Código do cupom DiscountCoupon.valuedecimal Valor do desconto DiscountCoupon.typestring $(reais) ou%(percentual)DiscountCoupon.starts_atdate Início da validade ( YYYY-MM-DD)DiscountCoupon.ends_atdate Fim da validade ( YYYY-MM-DD)DiscountCoupon.coupon_typestring loja/cliente/troca/presenteDiscountCoupon.local_applicationstring loja/produtos/marcas/categoriasDiscountCoupon.freight_applicationstring nao_aplicavel/desconto/frete_gratisExemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X GET \ "https://${TRAY_API_ADDRESS}/discount_coupons/7?access_token=${TRAY_ACCESS_TOKEN}"Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge. const couponId = 7; const res = await fetch( `https://${process.env.TRAY_API_ADDRESS}/discount_coupons/${couponId}` + `?access_token=${process.env.TRAY_ACCESS_TOKEN}`, ); if (res.status === 404) { /* id inexistente ou api_address errado */ } const data = await res.json(); // data.DiscountCouponErros comuns:
Código Causa Como resolver 401 Token expirado ou enviado em header Renovar token; usar query param 404 ID de cupom inexistente ou api_addresserradoConfirmar o idvia listagem e oapi_addressda loja429 Rate limit (180 req/min ou 10k/dia) Backoff exponencial (1s, 2s, 4s, 8s)
POST /discount_coupons
Quando usar: ao cadastrar uma nova campanha de desconto (percentual ou valor fixo), definindo janela de validade, valor, faixa de pedido e limites de uso.
Pré-requisitos:
access_tokenválido.codeúnico, sem espaços nem acentos.- definir
valueetype($ou%).
Schema do request:
schemas/discount_coupons.create.jsonSchema da response: resposta JSON padrão da API Tray (ver exemplo abaixo).
Content-Type:
application/x-www-form-urlencodedcom wrapper["DiscountCoupon"]["campo"](a estrutura JSON equivalente usa a chave de recurso"DiscountCoupon").Campos:
Campo Tipo Obrigatório Descrição codestring Sim Código digitado no checkout — alfanumérico, sem espaços nem acentos, único por loja valuedecimal Sim Valor do desconto (ex.: 10.00), interpretado conformetypetypestring Sim $= valor fixo em reais /%= percentualdescriptionstring Sim Descrição do cupom (obrigatória na criação) — nome da campanha coupon_typestring Não loja/cliente/troca/presentestarts_atdate Não Início da validade ( YYYY-MM-DD)ends_atdate Não Fim da validade ( YYYY-MM-DD)value_startdecimal Não Valor mínimo do produto/pedido elegível; vazio = sem restrição value_enddecimal Não Valor máximo do produto/pedido elegível; vazio = sem restrição usage_counter_limitinteger Não Limite total de usos (todos os clientes); deve ser >= usage_counter_limit_customerusage_counter_limit_customerinteger Não Limite de usos por cliente; deve ser <= usage_counter_limitcumulative_discountnumber Não 1= acumula com desconto progressivo /0= exclusivolocal_applicationstring Não loja/produtos/marcas/categoriasfreight_applicationstring Não nao_aplicavel/desconto/frete_gratis⚠️ Obrigatórios na criação (
POST):code,description,valueetype. Omitir qualquer um resulta em HTTP 400.Exemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X POST \ "https://${TRAY_API_ADDRESS}/discount_coupons?access_token=${TRAY_ACCESS_TOKEN}" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode '["DiscountCoupon"]["code"]=PROMO10' \ --data-urlencode '["DiscountCoupon"]["description"]=Promo Abril' \ --data-urlencode '["DiscountCoupon"]["starts_at"]=2026-04-01' \ --data-urlencode '["DiscountCoupon"]["ends_at"]=2026-04-30' \ --data-urlencode '["DiscountCoupon"]["value"]=10.00' \ --data-urlencode '["DiscountCoupon"]["type"]=%' \ --data-urlencode '["DiscountCoupon"]["usage_counter_limit_customer"]=5'Estrutura JSON equivalente do body (chave de recurso
DiscountCoupon):{ "DiscountCoupon": { "code": "PROMO10", "description": "Promo Abril", "starts_at": "2026-04-01", "ends_at": "2026-04-30", "value": "10.00", "type": "%", "usage_counter_limit_customer": "5", "cumulative_discount": "0" } }Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge. const body = new URLSearchParams({ '["DiscountCoupon"]["code"]': 'PROMO10', '["DiscountCoupon"]["description"]': 'Promo Abril', '["DiscountCoupon"]["starts_at"]': '2026-04-01', '["DiscountCoupon"]["ends_at"]': '2026-04-30', '["DiscountCoupon"]["value"]': '10.00', '["DiscountCoupon"]["type"]': '%', '["DiscountCoupon"]["usage_counter_limit_customer"]': '5', }); const res = await fetch( `https://${process.env.TRAY_API_ADDRESS}/discount_coupons` + `?access_token=${process.env.TRAY_ACCESS_TOKEN}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, }, ); if (res.status === 429) { /* backoff exponencial */ } const data = await res.json(); // { "message": "Created", "id": "1", "code": 201 }Resposta de sucesso:
{ "message": "Created", "id": "1", "code": 201 }Erros comuns:
Código Causa Como resolver 400 Faltou a chave de recurso DiscountCoupon,codecom espaço/acento, ou campo obrigatório (code/value/type) ausenteEnvolver os dados na chave DiscountCoupon; normalizar ocode(alfanumérico); conferirvalueetype401 Token expirado ou enviado em header Renovar token; usar query param 429 Rate limit (180 req/min ou 10k/dia) Backoff exponencial (1s, 2s, 4s, 8s)
PUT /discount_coupons/:id
Quando usar: para prorrogar a validade (
ends_at), ajustar valor/limites de uso ou alterar a faixa de pedido de um cupom já criado. Para apenas "desativar" temporariamente, prefira ajustarends_ataqui em vez de excluir viaDELETE.Pré-requisitos:
access_tokenválido.iddo cupom.- payload com a chave
DiscountCoupon.
Schema do request:
schemas/discount_coupons.update.jsonSchema da response: resposta JSON padrão da API Tray (ver exemplo abaixo).
Content-Type:
application/x-www-form-urlencodedcom wrapperDiscountCoupon.Campos:
Campo Tipo Obrigatório Descrição valuedecimal Não Valor do desconto typestring Não $(reais) /%(percentual)starts_atdate Não Início da validade ( YYYY-MM-DD)ends_atdate Não Fim da validade ( YYYY-MM-DD)value_startdecimal Não Valor mínimo do produto/pedido elegível value_enddecimal Não Valor máximo do produto/pedido elegível usage_counter_limitinteger Não Limite total de usos; deve ser >= usage_counter_limit_customerusage_counter_limit_customerinteger Não Limite de usos por cliente; deve ser <= usage_counter_limitcumulative_discountnumber Não 1= acumula /0= exclusivoExemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X PUT \ "https://${TRAY_API_ADDRESS}/discount_coupons/7?access_token=${TRAY_ACCESS_TOKEN}" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode '["DiscountCoupon"]["ends_at"]=2026-05-31'Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge. const couponId = 7; const body = new URLSearchParams({ '["DiscountCoupon"]["ends_at"]': '2026-05-31', }); const res = await fetch( `https://${process.env.TRAY_API_ADDRESS}/discount_coupons/${couponId}` + `?access_token=${process.env.TRAY_ACCESS_TOKEN}`, { method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, }, ); if (res.status === 429) { /* backoff exponencial */ } const data = await res.json(); // { "message": "Saved", "id": "7", "code": 200 }Resposta de sucesso:
{ "message": "Saved", "id": "7", "code": 200 }Erros comuns:
Código Causa Como resolver 400 Falta da chave DiscountCouponou limites inconsistentes (usage_counter_limitmenor queusage_counter_limit_customer)Envolver na chave DiscountCoupon; garantir limite geral>=limite por cliente401 Token expirado ou enviado em header Renovar token; usar query param 404 ID inexistente Confirmar idvia listagem429 Rate limit (180 req/min ou 10k/dia) Backoff exponencial (1s, 2s, 4s, 8s)
DELETE /discount_coupons/:id
Quando usar: ao encerrar definitivamente uma campanha. Para apenas desativar temporariamente, prefira ajustar
ends_atviaPUT(preserva o histórico).Pré-requisitos:
access_tokenválido.iddo cupom.
Schema do request: sem body — apenas parâmetro de path
:id.Schema da response: resposta JSON padrão da API Tray (ver exemplo abaixo).
Campos:
Campo Tipo Obrigatório Descrição idstring Sim ID do cupom a excluir (passado na URL) Exemplo (curl):
# NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X DELETE \ "https://${TRAY_API_ADDRESS}/discount_coupons/7?access_token=${TRAY_ACCESS_TOKEN}"Exemplo (Node):
// NÃO-VERIFICADO contra sandbox — validar antes do merge. const couponId = 7; const res = await fetch( `https://${process.env.TRAY_API_ADDRESS}/discount_coupons/${couponId}` + `?access_token=${process.env.TRAY_ACCESS_TOKEN}`, { method: 'DELETE' }, ); if (res.status === 404) { /* id inexistente ou já excluído */ } const data = await res.json(); // { "message": "Deleted", "id": "7", "code": 200 }Resposta de sucesso:
{ "message": "Deleted", "id": "7", "code": 200 }Erros comuns:
Código Causa Como resolver 401 Token expirado ou enviado em header Renovar token; usar query param 404 ID inexistente ou já excluído Confirmar idvia listagem429 Rate limit (180 req/min ou 10k/dia) Backoff exponencial (1s, 2s, 4s, 8s)
Relacionamentos do cupom
Um cupom de desconto pode ser genérico (vale para toda a loja) ou restrito a um conjunto de clientes, produtos, categorias, marcas ou métodos de frete. Essas restrições — e o brinde de um cupom-presente — não vivem no corpo do cupom: são relacionamentos mantidos em endpoints próprios. Esta seção cobre os 6 endpoints GET de consulta de relacionamento e o endpoint POST único de criação.
Como descobrir qual relacionamento consultar
Dois campos do cupom (retornados em GET /discount_coupons e GET /discount_coupons/:id) determinam qual relacionamento é relevante — consulte-os antes de chamar qualquer endpoint de relacionamento, para não fazer requisições inúteis:
coupon_type— abrangência/comportamento do cupom:loja→ cupom genérico; combinado comlocal_applicationdefine o escopo (ver abaixo).cliente→ restrito a clientes vinculados → consultarcustomer_relationship.troca→ gerado a partir de um pedido (order_id); o vínculo é criado viaDiscountCouponCustomercomorder_id.presente→ entrega um produto como brinde → consultargift_relationship.
local_application— escopo do desconto dentro da loja:loja→ aplica a toda a loja; sem relacionamento de produto/categoria/marca.produtos→ consultarproduct_relationship.categorias→ consultarcategory_relationship.marcas→ consultarbrand_relationship.
freight_application— comportamento quanto ao frete:nao_aplicavel→ sem relacionamento de frete.descontooufrete_gratis→ consultarshipping_relationship.
Regra de ouro: um cupom
coupon_type=lojasem nenhum relacionamento é genérico e vale para toda a loja e todos os clientes. Já um cupomcoupon_type=clientesem relacionamento criado não se aplica a ninguém — o vínculo viacreate_relationshipé obrigatório para que ele funcione.
Tabela-resumo dos endpoints de relacionamento
| Método | Endpoint | Wrapper de resposta | Quando usar (campo gatilho) |
|---|---|---|---|
| GET | /discount_coupons/customer_relationship/:id |
DiscountCouponCustomers[].DiscountCouponCustomer.customer_id |
coupon_type=cliente |
| GET | /discount_coupons/product_relationship/:id |
DiscountCouponProducts[].DiscountCouponProduct.product_id |
local_application=produtos |
| GET | /discount_coupons/category_relationship/:id |
DiscountCouponCategories[].DiscountCouponCategory.category_id |
local_application=categorias |
| GET | /discount_coupons/brand_relationship/:id |
DiscountCouponBrands[].DiscountCouponBrand.brand_id |
local_application=marcas |
| GET | /discount_coupons/shipping_relationship/:id |
DiscountCouponShippings[].DiscountCouponShipping.shipping_id / .value |
freight_application=desconto ou frete_gratis |
| GET | /discount_coupons/gift_relationship/:id |
DiscountCouponGift (product_id do brinde) |
coupon_type=presente |
| POST | /discount_coupons/create_relationship/:id |
corpo define o tipo (ver abaixo) | criar qualquer vínculo; máx. 100 registros/chamada |
:idé sempre o ID do cupom (não o ID do cliente/produto/etc.). Todos os endpoints usam?access_token={token}na query string — nunca em headerAuthorization.
GET /discount_coupons/customer_relationship/:id — clientes vinculados
Quando usar: com coupon_type=cliente, para auditar quais customer_id podem usar o cupom. Útil ao depurar "cupom não aplica para o cliente X".
Pré-requisitos: access_token válido; id do cupom (via GET /discount_coupons); relacionamento de cliente já criado.
Campos da resposta: DiscountCouponCustomers[].DiscountCouponCustomer.customer_id.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s "https://${TRAY_API_ADDRESS}/discount_coupons/customer_relationship/7?access_token=${TRAY_ACCESS_TOKEN}"
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const couponId = 7;
const res = await fetch(
`https://${base}/discount_coupons/customer_relationship/${couponId}?access_token=${encodeURIComponent(token)}`
);
if (res.status === 401) throw new Error("Token expirado/invalido — renovar via GET /auth?refresh_token=...");
if (res.status === 404) throw new Error("Cupom inexistente — confirmar id via GET /discount_coupons");
const data = await res.json();
const customerIds = (data.DiscountCouponCustomers ?? []).map(c => c.DiscountCouponCustomer.customer_id);
console.log(customerIds);
Erros comuns: 404 (cupom inexistente — confirmar o id via listagem); 401 (token em header em vez de query param — mover para ?access_token=).
GET /discount_coupons/product_relationship/:id — produtos vinculados
Quando usar: com local_application=produtos, para verificar quais product_id recebem o desconto.
Pré-requisitos: access_token válido; id do cupom; relacionamento de produto já criado.
Campos da resposta: DiscountCouponProducts[].DiscountCouponProduct.product_id.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s "https://${TRAY_API_ADDRESS}/discount_coupons/product_relationship/7?access_token=${TRAY_ACCESS_TOKEN}"
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const res = await fetch(
`https://${base}/discount_coupons/product_relationship/7?access_token=${encodeURIComponent(token)}`
);
const data = await res.json();
const productIds = (data.DiscountCouponProducts ?? []).map(p => p.DiscountCouponProduct.product_id);
console.log(productIds);
Erros comuns: 404 (cupom inexistente — confirmar o id); 401 (token expirado/em header).
GET /discount_coupons/category_relationship/:id — categorias vinculadas
Quando usar: com local_application=categorias, para verificar quais category_id recebem o desconto. O cupom aplica-se a todos os produtos das categorias vinculadas.
Pré-requisitos: access_token válido; id do cupom; relacionamento de categoria já criado.
Campos da resposta: DiscountCouponCategories[].DiscountCouponCategory.category_id.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s "https://${TRAY_API_ADDRESS}/discount_coupons/category_relationship/7?access_token=${TRAY_ACCESS_TOKEN}"
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const res = await fetch(
`https://${base}/discount_coupons/category_relationship/7?access_token=${encodeURIComponent(token)}`
);
const data = await res.json();
const categoryIds = (data.DiscountCouponCategories ?? []).map(c => c.DiscountCouponCategory.category_id);
console.log(categoryIds);
Erros comuns: 404 (cupom inexistente — confirmar o id); 401 (token expirado/em header).
GET /discount_coupons/brand_relationship/:id — marcas vinculadas
Quando usar: com local_application=marcas, para verificar quais brand_id recebem o desconto. Aplica-se a todos os produtos das marcas vinculadas.
Pré-requisitos: access_token válido; id do cupom; relacionamento de marca já criado.
Campos da resposta: DiscountCouponBrands[].DiscountCouponBrand.brand_id.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s "https://${TRAY_API_ADDRESS}/discount_coupons/brand_relationship/7?access_token=${TRAY_ACCESS_TOKEN}"
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const res = await fetch(
`https://${base}/discount_coupons/brand_relationship/7?access_token=${encodeURIComponent(token)}`
);
const data = await res.json();
const brandIds = (data.DiscountCouponBrands ?? []).map(b => b.DiscountCouponBrand.brand_id);
console.log(brandIds);
Erros comuns: 404 (cupom inexistente — confirmar o id); 401 (token expirado/em header).
GET /discount_coupons/shipping_relationship/:id — fretes vinculados
Quando usar: com freight_application=desconto ou frete_gratis, para verificar a quais métodos de envio o cupom aplica desconto/frete grátis. O campo value no relacionamento indica o comportamento: 0 = frete grátis; n = R$ n de desconto no frete.
Pré-requisitos: access_token válido; id do cupom; relacionamento de frete já criado.
Campos da resposta: DiscountCouponShippings[].DiscountCouponShipping.shipping_id e .value.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s "https://${TRAY_API_ADDRESS}/discount_coupons/shipping_relationship/7?access_token=${TRAY_ACCESS_TOKEN}"
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const res = await fetch(
`https://${base}/discount_coupons/shipping_relationship/7?access_token=${encodeURIComponent(token)}`
);
const data = await res.json();
const shippings = (data.DiscountCouponShippings ?? []).map(s => ({
shipping_id: s.DiscountCouponShipping.shipping_id,
value: s.DiscountCouponShipping.value, // "0" = frete gratis
}));
console.log(shippings);
Erros comuns: 404 (cupom inexistente — confirmar o id); 401 (token expirado/em header).
GET /discount_coupons/gift_relationship/:id — cupom-presente
Quando usar: com coupon_type=presente, para identificar o produto associado como brinde. Um cupom-presente entrega um item em vez de reduzir o valor — não confunda com cupom de desconto monetário.
Pré-requisitos: access_token válido; id do cupom; relacionamento de presente já criado.
Campos da resposta: DiscountCouponGift com o product_id do brinde.
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s "https://${TRAY_API_ADDRESS}/discount_coupons/gift_relationship/7?access_token=${TRAY_ACCESS_TOKEN}"
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const res = await fetch(
`https://${base}/discount_coupons/gift_relationship/7?access_token=${encodeURIComponent(token)}`
);
if (res.status === 404) throw new Error("Cupom inexistente ou sem presente vinculado — confirmar id e coupon_type=presente");
const data = await res.json();
console.log(data.DiscountCouponGift);
Erros comuns: 404 (cupom inexistente ou sem presente vinculado — confirmar id e que coupon_type=presente); 401 (token expirado/em header).
POST /discount_coupons/create_relationship/:id — criar qualquer relacionamento
Endpoint único para todos os tipos de vínculo. O tipo é determinado pela chave-wrapper do corpo, não pela URL:
| Chave-wrapper do corpo | Tipo de relacionamento | Campo-chave |
|---|---|---|
DiscountCouponCustomer |
clientes | customer_id (array) ou order_id (cupom de troca) |
DiscountCouponProduct |
produtos | product_id |
DiscountCouponCategory |
categorias | category_id |
DiscountCouponBrand |
marcas | brand_id |
DiscountCouponShipping |
frete | shipping_id, ou value (0=frete grátis, n=desconto de R$ n) |
Quando usar: depois de criar o cupom (POST /discount_coupons), para restringi-lo a clientes/produtos/categorias/marcas/fretes específicos, configurar frete grátis/desconto de frete, ou gerar cupom de troca a partir de um order_id.
Pré-requisitos: access_token válido; id do cupom já criado; IDs dos recursos a vincular; máximo 100 registros por chamada — lotes maiores retornam 400, divida e respeite o rate limit (180 req/min).
Content-Type: application/json (cada chave-wrapper define o tipo de relacionamento).
# NÃO-VERIFICADO contra sandbox — validar antes do merge.
curl -s -X POST \
"https://${TRAY_API_ADDRESS}/discount_coupons/create_relationship/7?access_token=${TRAY_ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-d '{"DiscountCouponProduct":[{"product_id":"456"},{"product_id":"789"}]}'
// NÃO-VERIFICADO contra sandbox — validar antes do merge.
const base = process.env.TRAY_API_ADDRESS;
const token = process.env.TRAY_ACCESS_TOKEN;
const couponId = 7;
// Divide em lotes de até 100 para respeitar o limite do endpoint.
function chunk(arr, size = 100) {
const out = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
async function vincularClientes(customerIds) {
for (const lote of chunk(customerIds, 100)) {
const body = { DiscountCouponCustomer: lote.map(id => ({ customer_id: String(id) })) };
const res = await fetch(
`https://${base}/discount_coupons/create_relationship/${couponId}?access_token=${encodeURIComponent(token)}`,
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
);
if (res.status === 400) throw new Error("Lote >100 ou chave-wrapper/IDs invalidos — revisar payload");
if (res.status === 404) throw new Error("Cupom inexistente — confirmar id do cupom na URL");
if (res.status === 429) { /* backoff exponencial: 1s, 2s, 4s, 8s... */ }
// pausa entre lotes para respeitar o rate limit (180 req/min)
await new Promise(r => setTimeout(r, 500));
}
}
Frete grátis vs. desconto de frete: envie value=0 para zerar o frete ou value=n para R$ n de desconto:
{ "DiscountCouponShipping": { "value": "0" } }
Cupom de troca a partir de pedido: use DiscountCouponCustomer com order_id:
{ "DiscountCouponCustomer": { "order_id": "10" } }
Resposta de sucesso: { "message": "Created", "id": "...", "code": 201 }.
Erros comuns:
400— lote acima de 100 registros, chave-wrapper incorreta, ou IDs inexistentes → dividir em lotes de até 100; usar a chave correta (DiscountCouponCustomer/Product/Category/Brand/Shipping); validar os IDs.404— cupom (idna URL) inexistente → confirmar oiddo cupom.429— rate limit em cargas grandes de vinculação → pausar entre lotes; backoff exponencial.
Anti-pattern: criar um cupom
coupon_type=clientee esquecer de criar o relacionamento. Sem ocreate_relationshipcom oscustomer_id, o cupom não se aplica a ninguém. O mesmo vale paralocal_application=produtos/categorias/marcassem o respectivo vínculo.
Edge cases
Os cupons da Tray combinam várias dimensões ortogonais — janela de validade, contadores de uso, tipo de aplicação, frete e relacionamentos — e cada combinação tem comportamento próprio. Os casos abaixo cobrem as armadilhas mais frequentes. Em todos, o access_token vai como query param (?access_token={TRAY_ACCESS_TOKEN}) e a URL base usa {TRAY_API_ADDRESS} (varia por loja, retornado no callback OAuth).
Janela de validade (
starts_at/ends_at): o cupom só é aceito no checkout dentro do intervalo[starts_at, ends_at], inclusive. Antes destarts_atou depois deends_atele é tratado como inexistente — não retorna erro de "cupom inválido por valor", simplesmente não se aplica. As datas usam o formatoYYYY-MM-DD(horário de Brasília, sem timezone). Para desativar temporariamente uma campanha, prefira encurtarends_atviaPUTa apagar o cupom comDELETE, pois oPUTpreserva o histórico e oidpara reativação futura.# Cupom válido só em abril/2026 — NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X POST "https://${TRAY_API_ADDRESS}/discount_coupons?access_token=${TRAY_ACCESS_TOKEN}" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode '["DiscountCoupon"]["code"]=ABRIL10' \ --data-urlencode '["DiscountCoupon"]["type"]=%' \ --data-urlencode '["DiscountCoupon"]["value"]=10.00' \ --data-urlencode '["DiscountCoupon"]["starts_at"]=2026-04-01' \ --data-urlencode '["DiscountCoupon"]["ends_at"]=2026-04-30' # "Desativar" antecipadamente sem perder histórico: curl -X PUT "https://${TRAY_API_ADDRESS}/discount_coupons/7?access_token=${TRAY_ACCESS_TOKEN}" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode '["DiscountCoupon"]["ends_at"]=2026-04-15' # NÃO-VERIFICADO contra sandbox — validar antes do merge.Limite total (
usage_counter_limit) vs. limite por cliente (usage_counter_limit_customer): os dois contadores operam em conjunto e precisam ser consistentes — o limite total deve ser>=ao limite por cliente. Se você defineusage_counter_limit_customer=2mas deixausage_counter_limit=1, o cupom esgota globalmente no primeiro uso, antes mesmo de um único cliente atingir suas 2 utilizações; a configuração é internamente contraditória e tende a retornarHTTP 400. Por convenção,usage_counter_limit=0(ou vazio) costuma indicar uso ilimitado no total. Ao depurar um "cupom inválido" reportado por um cliente, cheque os dois contadores: um cliente que já usou o cupom o número máximo de vezes (usage_counter_limit_customer) recebe recusa mesmo que o limite geral ainda tenha saldo.# 1000 usos no total, 2 por cliente (geral >= por cliente) — NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X POST "https://${TRAY_API_ADDRESS}/discount_coupons?access_token=${TRAY_ACCESS_TOKEN}" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode '["DiscountCoupon"]["code"]=BLACK20' \ --data-urlencode '["DiscountCoupon"]["type"]=%' \ --data-urlencode '["DiscountCoupon"]["value"]=20.00' \ --data-urlencode '["DiscountCoupon"]["usage_counter_limit"]=1000' \ --data-urlencode '["DiscountCoupon"]["usage_counter_limit_customer"]=2' # NÃO-VERIFICADO contra sandbox — validar antes do merge.Cupom cumulativo vs. exclusivo (
cumulative_discount): comcumulative_discount=1o desconto do cupom soma-se a um desconto progressivo ou a outros descontos já aplicados ao pedido; comcumulative_discount=0ele é exclusivo e não empilha. Essa flag muda diretamente o valor final do pedido e é crítica em campanhas como Black Friday, onde já existe desconto progressivo ativo. Defina-a sempre explicitamente — não confie no default — para evitar empilhamento indevido (cliente paga menos do que a margem permite) ou bloqueio inesperado de promoções legítimas.# Cupom que NÃO acumula com outras promoções — NÃO-VERIFICADO contra sandbox — validar antes do merge. curl -X PUT "https://${TRAY_API_ADDRESS}/discount_coupons/7?access_token=${TRAY_ACCESS_TOKEN}" \ -H '
…(truncated)