API Admin › Recursos
Produtos, coleções e marcas
O catálogo da loja: os produtos com as variantes e as fotos, as coleções (as categorias da loja, em árvore) e as marcas. O estoque e o preço em lote, que o ERP usa a cada sincronização, estão em Estoque e preço; os complementos de um produto (a lente e os tratamentos da armação), em Montagens; os arquivos do provador virtual da armação, em Provador virtual.
| Escopo | O que abre |
|---|---|
read_products | ler os produtos, as variantes, as coleções e as marcas |
write_products | criar, mudar e excluir tudo isso, enviar fotos, e o estoque e o preço |
read_content | ler os produtos, as coleções e as marcas (não GET /variants) |
write_content | mudar só os textos: nome, descrição, SEO, endereço e o texto das fotos |
O write_content é o escopo de um agente de SEO: os mesmos PATCH aceitam o corpo só com os campos de texto, e recusam o resto com 403 e os campos em fields (ver Só os textos).
Toda mudança de texto (o nome, a descrição, o SEO, o endereço e o texto das fotos), com qualquer um dos dois escopos, fica no histórico da integração por 180 dias, com o antes e o depois de cada campo. O lojista vê e desfaz no painel, na página da integração, em Mudanças; um agente, pelo servidor MCP. A mudança que não mexe em texto (o preço, o estoque, a situação) não entra.
As regras são as mesmas do painel: o que o painel recusa, a API recusa, com os campos em inglês. O que a API muda aparece na vitrine em até 60 segundos.
O objeto produto
{
"id": "0b6f2c1e-...",
"title": "Camiseta Básica",
"handle": "camiseta-basica",
"description": "Algodão penteado.",
"active": true,
"template_suffix": "",
"seo": { "title": "", "description": "" },
"options": [{ "name": "Tamanho", "attribute_id": null }],
"brand_id": null,
"mpn": "CB-2026",
"condition": "new",
"tax": { "ncm": "61091000", "cest": "", "origin": "0", "unit": "UN" },
"dimensions": {
"weight_grams": 300,
"height_cm": 3,
"width_cm": 25,
"length_cm": 30
},
"inventory_tracked": true,
"sold_separately": true,
"requires_prescription": false,
"prescription_type": "glasses",
"contact_lens_mode": "by_prescription",
"collection_ids": ["7d1e..."],
"specifications": [{ "attribute_id": "a3c9...", "value": "Algodão" }],
"variants": [
{
"id": "5e2a...",
"product_id": "0b6f2c1e-...",
"options": ["P"],
"sku": "CB-P",
"barcode": "7891000315507",
"price": 4990,
"compare_at_price": 5990,
"cost": 2100,
"inventory_quantity": 12,
"inventory_tracked": true,
"prescription_range": null,
"position": 0,
"updated_at": "2026-10-03T18:05:00.000Z"
}
],
"images": [
{
"id": "c81f...",
"src": "https://fotos.denaryx.com/.../c81f...-1600.webp",
"thumbnail_src": "https://fotos.denaryx.com/.../c81f...-480.webp",
"width": 1600,
"height": 1200,
"alt": "Camiseta de frente",
"position": 0,
"variant_option": null
}
],
"try_on": { "available": false, "type": null },
"created_at": "2026-10-01T12:00:00.000Z",
"updated_at": "2026-10-03T18:05:00.000Z"
}handle: o endereço na vitrine (/produtos/<handle>), único na loja.active: à venda na vitrine;falsedeixa o produto oculto.options: até 3 (Tamanho, Cor). Comattribute_id, a opção fica ligada a um atributo de lista da loja, e o valor de cada variante tem de estar na lista dele.condition:new,usedourefurbished.tax: os dados da nota fiscal (NCM de 8 dígitos, CEST de 7, a origem da mercadoria de0a8e a unidade comercial). A plataforma só guarda: quem emite a nota é o ERP.dimensions: as medidas da embalagem, em gramas e centímetros inteiros. Sem elas (null), o produto é vendido, mas o frete dele não é cotado.collection_ids: até 10; a primeira é a principal, que forma o caminho na vitrine.specifications: os valores dos atributos da loja, conforme o tipo do atributo: texto, número,true/false, o id do valor da lista ou a lista dos ids marcados. Uma coleção pode pedir atributos obrigatórios: o produto à venda nela precisa deles.variants: de 1 a 100. Os valores em dinheiro são centavos inteiros.compare_at_priceé o "de" da promoção (maior queprice, ounull);costnão aparece na vitrine. O SKU e o código de barras não se repetem na loja.inventory_quantityé o que está à venda agora: o checkout desconta a cada pedido. No produto sem controle de estoque, é sempre0(ver abaixo).inventory_tracked:falseno produto sob encomenda, que a loja fabrica ou encomenda a cada pedido (a lente de grau, a peça sob medida). Ele fica disponível enquanto está à venda (active), o checkout não desconta nem devolve estoque, o eventoinventory.updatednão sai por pedido, e os lotes de estoque recusam as variantes dele (422 inventory_not_tracked). Oinventory_quantitymandado num produto assim é ignorado. A variante repete o valor do produto eminventory_tracked.sold_separately:falseno complemento, vendido só junto com outro produto numa montagem (a lente da armação, o tratamento). Ele sai das listas, da busca, do feed do Google e do sitemap da vitrine, e a página dele não abre; a vitrine o mostra dentro da montagem do produto principal.requires_prescription: a lente de grau, que pede a receita do comprador. Só nela as variantes têmprescription_range.prescription_type: a receita que a lente pede:glasses(óculos, o padrão) oucontact_lens(lente de contato). A lente de contato é vendida sozinha, com as caixas de cada olho: precisa desold_separately: truee não entra em montagem (nem como complemento, nem pela categoria). O produto que está numa montagem não vira lente de contato (409). Só vale comrequires_prescription.prescription_range: a faixa de grau que a variante da lente atende, em dioptrias de 0,25 em 0,25:sphere_minesphere_max(o esférico, de -30 a +30),cylinder_max(o cilíndrico em valor absoluto, de 0 a 10) e, na multifocal,add_mineadd_max(a adição, de 0,5 a 4, as duas ou nenhuma).null: atende a qualquer grau. A lente atende quando o esférico e o esférico somado ao cilíndrico ficam na faixa e o cilíndrico não passa do máximo. É o lojista quem cadastra, pelo catálogo do fabricante: a plataforma não indica lente. Na lente de contato, a variante é a caixa, e a faixa traz tambémbase_curve(a curva base, de 7.0 a 10.0 mm) ediameter(o diâmetro, de 8.0 a 16.0 mm), com uma casa decimal; a receita que traz outra curva base ou outro diâmetro não serve para a caixa. O grau da lente de contato vendida pela receita não é variante: ele vem da receita. Na lente de contato por grau (contact_lens_mode: "by_power"), a faixa sai do grau da variante (ver abaixo).
{
"sphere_min": -6,
"sphere_max": 4,
"cylinder_max": 2,
"add_min": null,
"add_max": null
}contact_lens_mode: como a lente de contato é vendida.by_prescription(o padrão): o grau vem da receita, e a variante é a caixa, com a faixa que ela atende (a tórica e a multifocal, feitas sob encomenda).by_power: a esférica pronta, uma caixa por grau, com SKU, código de barras e estoque próprios. O produto tem uma opção só, a do grau, e cada variante é um grau escrito como na receita (-2,00,+1,25,0,00;-2.00e-2também servem e são gravados com a vírgula), de -20 a +20, de 0,25 em 0,25, sem repetir. Aprescription_rangesai do grau (sphere_min=sphere_max= o grau,cylinder_max0, sem adição): da que vier, só valembase_curveediameter. Cada curva base é um produto à parte (no catálogo do fabricante, o código muda), com até 100 graus. O grau sem estoque fica indisponível na vitrine. Só vale comprescription_type: "contact_lens"; nos outros produtos é sempreby_prescription.
A esférica por grau, como o ERP a cria (o saldo de cada grau depois vem pelo POST /inventory/set, pelo SKU):
{
"title": "Lume Diária BC 8,6",
"requires_prescription": true,
"prescription_type": "contact_lens",
"contact_lens_mode": "by_power",
"options": [{ "name": "Grau" }],
"variants": [
{
"options": ["-2,00"],
"sku": "LUME86-200",
"barcode": "7891234567895",
"price": 8990,
"inventory_quantity": 12,
"prescription_range": {
"sphere_min": -2,
"sphere_max": -2,
"cylinder_max": 0,
"base_curve": 8.6,
"diameter": 14.2
}
}
]
}images: até 10, guardadas em WebP em dois tamanhos (1.600 e 480 px pelo lado maior). A primeira é a capa.variant_optiondiz o valor de opção da foto ("Cor: Azul"), quando o painel marcou.try_on: o resumo do provador virtual da armação.available: o produto ou alguma variante tem arquivo;type: o tipo do arquivo do produto (2dou3d), ounullsem ele. Os arquivos ficam emGET /products/{id}/try_on.
Os produtos
GET /products
Os produtos, na ordem da última mudança, da mais antiga à mais nova (o id desempata). É a lista da sincronização: guarde o updated_at do último produto que leu e, na próxima vez, peça updated_at_min com ele (a data entra na lista: o último lido vem de novo).
| Parâmetro | O que faz |
|---|---|
limit | de 1 a 100 (50 sem ele) |
cursor | o next_cursor da página anterior, como veio |
ids | só estes ids, separados por vírgula (até 100) |
updated_at_min | só o que mudou a partir desta data |
created_at_min | só o criado a partir desta data |
created_at_max | só o criado até esta data |
active | true (à venda) ou false (ocultos) |
collection_id | só os da coleção e das coleções de baixo dela |
A resposta é { "data": [produto...], "next_cursor": "..." }; com mais páginas, o cabeçalho Link leva à próxima (rel="next"). Ver Listas.
A mudança de estoque de uma variante (um pedido, um cancelamento, um ajuste) muda o updated_at da variante, e não o do produto: para sincronizar o estoque, use GET /variants.
GET /products/count
Quantos produtos casam com os mesmos filtros da lista: { "count": 128 }.
GET /products/{id}
Um produto.
POST /products
Cria o produto, com as variantes. Responde 201 com ele. Só title e variants (com price) são obrigatórios; sem handle, o endereço sai do título (com um número no fim, se já existir).
{
"title": "Camiseta Básica",
"options": [{ "name": "Tamanho" }],
"variants": [
{
"options": ["P"],
"sku": "CB-P",
"price": 4990,
"inventory_quantity": 12
},
{ "options": ["M"], "sku": "CB-M", "price": 4990, "inventory_quantity": 8 }
]
}Cada variante tem um valor para cada opção do produto, e a combinação não se repete. SKU, código de barras ou handle que já existem na loja: 409 com o código duplicate_sku, duplicate_barcode ou duplicate_handle.
PATCH /products/{id}
Muda só o que vier. As variantes ficam como estão (mudam pelas rotas delas e pelos lotes), e o estoque que um pedido acabou de descontar não volta: a mudança é aplicada sobre o que está gravado, com o produto e as variantes travados.
{
"title": "Camiseta Básica Algodão",
"seo": { "title": "Camiseta de algodão" }
}seoetaxmudam campo a campo (o que não veio fica).options,collection_idsespecificationstrocam a lista inteira. Mudar o número de opções exige mudar os valores das variantes: a API recusa o produto que ficaria com variantes sem valor numa opção.dimensions: nulltira as medidas;brand_id: nulltira a marca.- Um
handlenovo muda o endereço; o antigo continua levando ao produto. requires_prescription: falsetira as faixas de grau das variantes;inventory_tracked: falsezera o estoque delas.
DELETE /products/{id}
Exclui o produto, com as variantes e as fotos (204). Os pedidos já feitos continuam com o nome e o preço da compra.
As variantes
POST /products/{id}/variants
Acrescenta uma variante no fim da lista do produto. Responde 201 com a variante.
{ "options": ["G"], "sku": "CB-G", "price": 4990, "inventory_quantity": 5 }PATCH /products/{id}/variants/{variant_id}
Muda só o que vier: options, sku, barcode, price, compare_at_price, cost, inventory_quantity, prescription_range (null tira a faixa). O inventory_quantity grava o número absoluto: com pedidos acontecendo, prefira o ajuste.
DELETE /products/{id}/variants/{variant_id}
Exclui a variante (204). A única variante do produto não sai (400): exclua o produto.
GET /variants
As variantes da loja, na ordem da última mudança, como a lista de produtos. É por aqui que se sincroniza o estoque e o preço: updated_at_min traz as variantes que mudaram (por um pedido, pelo painel ou pela API). Também aceita limit, cursor, ids, product_id e sku: GET /variants?sku=CB-P acha a variante pelo SKU.
As fotos
POST /products/{id}/images
Acrescenta uma foto no fim da lista. Responde 201 com a foto. Dois jeitos:
- o arquivo, num
multipart/form-data: a foto no campofile(JPEG, PNG ou WebP, até 10 MB) e o texto dela emalt, opcional; - o endereço, em JSON:
{ "src": "https://...", "alt": "..." }. A API baixa a foto.
curl https://api.denaryx.com/admin/2026-10/products/<id>/images \
-H "Authorization: Bearer dnxa_..." \
-F file=@frente.jpg -F alt="Camiseta de frente"O endereço precisa ser https://, na porta padrão, e público na internet: a API não busca endereços de redes internas e confere cada redirecionamento (até 3). A foto tem até 10 MB e 15 segundos para chegar.
code | Status | Quando |
|---|---|---|
invalid_image_url | 400 | O endereço não é https:// na porta 443, ou tem usuário e senha. |
image_url_not_allowed | 400 | O endereço (ou um redirecionamento) leva a uma rede interna. |
image_too_large | 413 | A foto passou de 10 MB. |
image_download_failed | 422 | O endereço não respondeu, respondeu com erro ou redirecionou demais. |
invalid_request | 400 | O arquivo não é uma foto JPEG, PNG ou WebP, ou o produto já tem 10. |
PATCH /products/{id}/images/{image_id}
Muda o texto da foto (alt, até 125 caracteres), que os buscadores e os leitores de tela leem. Vazio, a vitrine usa o nome do produto. Responde com a foto. Vale com write_products ou write_content.
{ "alt": "Camiseta azul dobrada sobre a mesa" }DELETE /products/{id}/images/{image_id}
Exclui a foto (204).
As coleções
As coleções são as categorias da loja, em árvore de até 3 níveis (departamento › categoria › subcategoria).
{
"id": "7d1e...",
"parent_id": null,
"title": "Roupas",
"handle": "roupas",
"path": "roupas",
"level": 1,
"products_count": 42,
"attributes": [{ "attribute_id": "a3c9...", "required": true }],
"google_category_id": 1604,
"description": "Tudo de roupa.",
"seo": { "title": "", "description": "" },
"template_suffix": ""
}path: o caminho na vitrine (/categorias/<path>), com oshandledesde o topo. Ohandlenão se repete entre as coleções do mesmo lugar da árvore.products_count: os produtos ligados direto a ela.attributes: os atributos que ela pede nos produtos dela (e nos das de baixo).google_category_id: a categoria da taxonomia do Google, para o feed.
GET /collections
Todas as coleções (no máximo 2.000), numa lista plana na ordem do menu: cada uma seguida das de baixo dela. Cabem numa página. Os itens da lista não trazem description, seo e template_suffix.
GET /collections/{id}
Uma coleção, com o texto da página e o SEO.
POST /collections
Cria a coleção no fim do lugar dela na árvore. Só title é obrigatório; sem parent_id, fica no primeiro nível.
{
"title": "Camisetas",
"parent_id": "7d1e...",
"description": "As camisetas da loja."
}PATCH /collections/{id}
Muda só o que vier. Um parent_id novo move a coleção, com as de baixo dela, para o fim do lugar novo; os endereços antigos levam aos novos. attributes troca a lista inteira.
DELETE /collections/{id}
Exclui a coleção (204). Os produtos saem dela e continuam nas outras. A coleção com outras debaixo dela não sai (409).
As marcas
{
"id": "e40b...",
"name": "Tramontina",
"handle": "tramontina",
"description": "Utensílios de cozinha.",
"logo_url": null,
"seo": { "title": "", "description": "" },
"products_count": 18
}GET /brands (todas, no máximo 2.000, em ordem de nome), GET /brands/{id}, POST /brands (só name é obrigatório; sem handle, o endereço sai do nome), PATCH /brands/{id} (só o que vier muda) e DELETE /brands/{id} (os produtos dela ficam sem marca). O logo se envia pelo painel; a API não muda o logo.