Produtos e SKUs
O modelo
Na Grocers, produto é a entidade de vitrine (nome, descrição, categoria, marca) e SKU é a unidade vendável (EAN, peso, dimensões, embalagem). Um leite pode ser um produto com três SKUs: unidade, fardo de 6 e fardo de 12.
Preço e estoque nunca ficam no produto — sempre no SKU.
Enviar catálogo
POST /v1/batch/Products
[
{
"externalReference": "PROD-114970",
"categoryExternalReference": "CAT-MASSAS",
"brandExternalReference": "MARCA-001",
"commercialPolicyId": "9c1f5a3e-7b2d-4c8e-9f01-2a3b4c5d6e7f",
"name": "Macarrão Espaguete 500g",
"title": "Macarrão Espaguete Grano Duro 500g",
"shortDescription": "Massa de sêmola de grano duro",
"longDescription": "Macarrão tipo espaguete produzido com sêmola de grano duro...",
"similarTerms": "espaguete, spaghetti, talharim",
"slug": "macarrao-espaguete-500g",
"fiscalCode": "19021900",
"ean": "7891234567890",
"punctuation": 5,
"displayInSite": true,
"displayInSoldOut": true,
"active": true,
"skus": [
{
"externalReference": "114970",
"name": "Macarrão Espaguete 500g - Unidade",
"ean": "7891234567890",
"reference": "MAC-ESP-500",
"unitOfMeasurement": "Un",
"weightForFreight": 0.52,
"heightForFreight": 5,
"widthForFreight": 10,
"lengthForFreight": 25,
"weightReal": 0.5,
"heightReal": 4,
"widthReal": 9,
"lengthReal": 24,
"arrivalDate": "2026-07-29",
"manufacturerCode": "ME500"
},
{
"externalReference": "114970-CS",
"name": "Macarrão Espaguete 500g - Fardo 12un",
"ean": "7891234567891",
"reference": "MAC-ESP-500-CS",
"unitOfMeasurement": "Un",
"skuPackageMinAmount": 1,
"skuPackageMaxAmount": 12,
"weightForFreight": 6.2,
"heightForFreight": 20,
"widthForFreight": 30,
"lengthForFreight": 40,
"weightReal": 6,
"heightReal": 19,
"widthReal": 29,
"lengthReal": 39,
"arrivalDate": "2026-07-29",
"manufacturerCode": "ME500-CS"
}
]
}
]Resposta: 200 OK com o operationId do lote. O processamento é assíncrono — o
resultado chega no webhook ProductWithSKUs_Created ou ProductWithSKUs_Updated.
Esta rota devolve o GUID cru no corpo, sem envelope:
"402fe685-2060-432b-ac57-223eb2e680f3"As demais rotas em lote (Brands, Categories, Pricing, Warehouses,
Warehouses/sku) devolvem { "operationId": "..." }. Se o seu cliente lê sempre
body.operationId, ele perde exatamente o operationId do lote de produtos.
No SKU o campo da unidade de medida se chama unitOfMeasurement. Nos itens de pedido
(PUT /v1/Orders/{orderNumber}) o mesmo conceito se chama unitOfMeasure, sem o
-ment. Os valores aceitos são os mesmos; só o nome do campo muda entre os dois
contratos.
Campos do produto
| Campo | Obrigatório | Limite | Observação |
|---|---|---|---|
externalReference | sim | 60 | chave de idempotência |
commercialPolicyId | sim | — | política comercial a que o produto pertence |
categoryExternalReference | recomendado | 30 | sem ela o item não aparece na navegação |
brandExternalReference | não | 30 | habilita o filtro por marca |
name | sim | 5–400 | nome interno |
slug | sim | 400 | precisa ser único |
title | sim | 3–400 | texto exibido na vitrine |
shortDescription | sim | 3–500 | descrição curta |
longDescription | sim | mín. 3 | descrição completa |
fiscalCode | não pela API | — | NCM, usado na nota — ver aviso abaixo |
ean | não | 48 | código de barras do produto |
punctuation | sim | ≥ 0 | peso de relevância na busca |
displayInSite | não | — | false esconde o produto sem excluir |
displayInSoldOut | não | — | mostra o item mesmo com estoque zero (esgotado) |
similarTerms | não | — | sinônimos de busca, separados por vírgula |
characteristics | não | — | características descritivas |
active | não | — | ativa ou desativa o produto |
A atualização é parcial: a plataforma rastreia quais campos vieram no JSON. Enviar
só { "externalReference": "PROD-1", "active": false } altera apenas active — os
demais campos ficam como estão. Omitir é diferente de mandar null.
fiscalCode não tem validação nenhuma: a regra existe no validador, mas está
vazia — sem obrigatoriedade, sem tamanho, sem formato. Produto sem NCM, com NCM de
4 dígitos ou com texto no lugar do código passa sem reclamação. O erro só aparece na
emissão da nota, muito depois da carga. Valide o NCM no seu lado antes de enviar.
Campos do SKU
| Campo | Obrigatório | Limite | Observação |
|---|---|---|---|
externalReference | sim | 60 | chave usada por estoque e preço |
name | sim | 400 | nome da variação |
ean | sim | 30 | código de barras |
reference | sim | 100 | referência interna do SKU |
unitOfMeasurement | sim | — | Un (1), Kg (2), G (3) ou Mg (4) |
weightForFreight e dimensões | sim | > 0 | weight/height/width/lengthForFreight — entram no frete |
weightReal e dimensões reais | sim | > 0 | weight/height/width/lengthReal |
arrivalDate | sim | — | data de chegada |
manufacturerCode | sim | 50 | código do fabricante |
arithmeticFactor | não | — | fator de conversão para itens fracionados |
weight | não | > 0 | peso, quando informado |
minimumWeightForSale | não | > 0 | peso mínimo para venda |
skuPackageMinAmount / MaxAmount | não | > 0 | quantidade mínima e máxima por embalagem |
exhibitionBadge1..3 | não | 15 | selos exibidos no card do produto |
O externalReference do SKU é o identificador usado em estoque, preço e itens do
pedido. Mudá-lo depois da loja no ar quebra o vínculo com o estoque e preços já
publicados.
Quando a validação falha
POST /v1/batch/Products não valida o conteúdo do lote. A API de integração recebe,
gera o operationId e enfileira — responde 200 OK mesmo com um produto de nome vazio
ou EAN de 200 caracteres.
Quem valida é o core, quando o processador aplica cada item. Um produto reprovado gera o
evento ProductWithSKUs_CreationFailed (ou _UpdateFailed); um produto que entra mas
com SKUs reprovados gera ProductWithSKUs_PartiallyFailed.
Nas notificações de catálogo o erro não está no campo error do envelope — ele fica
dentro de data. Um consumidor que só olha evento.error não enxerga nenhuma falha
de produto ou de SKU.
O data é um relatório do produto inteiro: um produto e seus SKUs viram uma
notificação, com a contagem de sucessos e a lista dos SKUs que falharam.
{
"operationId": "402fe685-2060-432b-ac57-223eb2e680f3",
"contractAccountId": "9f36a666-acd5-4987-a47f-3de247f65d82",
"relatedEntity": "ProductWithSKUs",
"eventType": "ProductWithSKUs_PartiallyFailed",
"data": {
"product": {
"externalReference": "PROD-114970",
"name": "Macarrão Espaguete 500g",
"processed": true,
"error": null
},
"skus": {
"total": 2,
"successful": 1,
"failed": 1,
"successfulSKUs": ["114970"],
"failedSKUs": [
{
"externalReference": "114970-CS",
"error": {
"statusCode": 400,
"message": "{\"errors\":{\"ManufacturerCode\":[\"Código do fabricante é obrigatório\"]}}",
"title": "Requisição inválida",
"detail": "Ocorreram um ou mais erros de validação",
"validationErrors": [
{ "id": 1, "key": "ManufacturerCode", "validation": "Código do fabricante é obrigatório" }
]
}
}
]
},
"summary": {
"status": "PartialFailure",
"processedAt": "2026-07-29T14:32:10.512Z"
}
}
}| Campo | Como usar |
|---|---|
data.product.externalReference | qual produto do seu lote esta notificação descreve |
data.product.processed | false = o produto não entrou e nenhum SKU foi tentado (fail-fast) |
data.product.error | o erro do produto, quando processed é false |
data.skus.successfulSKUs | externalReference dos SKUs que entraram |
data.skus.failedSKUs[].externalReference | qual SKU falhou |
data.skus.failedSKUs[].error.validationErrors | key é a propriedade reprovada, validation a mensagem do validador |
data.summary.status | Success, PartialFailure ou Failed |
error.message carrega o corpo cru devolvido pelo core — guarde-o quando
validationErrors vier vazio.
Quando o produto falha, os SKUs nem são tentados: a lista successfulSKUs vem
vazia e failedSKUs também. Corrigir só o produto e reenviar reprocessa o conjunto
inteiro — é o comportamento desejado, mas significa que “0 SKUs falharam” não quer
dizer “todos os SKUs entraram”. Leia data.product.processed primeiro.
Produto e SKU são validados em chamadas separadas ao core. Um SKU reprovado devolve key
com o nome da propriedade do próprio SKU (Name, EAN, Reference,
ManufacturerCode), sem prefixo do produto.
Não espere 400 do POST /v1/batch/Products para descobrir dado inválido. Ele nunca
vem: a rota só declara 200 e 500, e a validação de modelo está desligada na API de
integração. Um integrador que trata erro apenas pelo código HTTP conclui que o lote
inteiro entrou — e a falha some.
Como o produto reprovado vira uma notificação isolada, é obrigatório assinar
ProductWithSKUs_CreationFailed, ProductWithSKUs_UpdateFailed e
ProductWithSKUs_PartiallyFailed junto com os eventos de sucesso. Veja
Registrar o webhook.
Itens pesáveis
Para produtos vendidos por peso (frios, hortifruti, açougue), a combinação é:
Unidade de medida
unitOfMeasurement: "Kg" no SKU.
Fator aritmético
arithmeticFactor converte a quantidade escolhida pelo cliente para a unidade de
cobrança — é o que permite vender “300 g” de um item precificado por quilo.
Peso real
weightReal alimenta o cálculo de frete e a conferência na separação.
Consultar um produto
curl $GROCERS_API/v1/batch/Products/PROD-114970 \
-H "Authorization: Basic $BASIC" \
-H "x-contractAccountId: $TENANT"A resposta é o produto com marca, categoria e a lista de SKUs — é a forma de conferir o que a plataforma realmente gravou depois de um lote:
{
"id": "402fe685-2060-432b-ac57-223eb2e680f3",
"contractAccountId": "9f36a666-acd5-4987-a47f-3de247f65d82",
"externalReference": "PROD-114970",
"name": "Macarrão Espaguete 500g",
"title": "Macarrão Espaguete Grano Duro 500g",
"slug": "macarrao-espaguete-500g",
"shortDescription": "Massa de sêmola de grano duro",
"longDescription": "Macarrão tipo espaguete produzido com sêmola de grano duro...",
"similarTerms": "espaguete, spaghetti, talharim",
"characteristics": null,
"fiscalCode": "19021900",
"ean": "7891234567890",
"punctuation": 5,
"displayInSite": true,
"displayInSoldOut": true,
"active": true,
"hasImage": true,
"commercialPoliticId": "9c1f5a3e-7b2d-4c8e-9f01-2a3b4c5d6e7f",
"brandId": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"brand": { "id": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f", "name": "Grano Duro", "externalReference": "MARCA-001" },
"categoryId": "8d9e0f1a-2b3c-4d4e-9f5a-6b7c8d9e0f1a",
"category": { "id": "8d9e0f1a-2b3c-4d4e-9f5a-6b7c8d9e0f1a", "name": "Massas", "externalReference": "CAT-MASSAS" },
"createdAt": "2026-07-29T14:32:10",
"updatedAt": "2026-07-30T08:11:04",
"approvedByUserId": null,
"skus": [
{
"id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
"externalReference": "114970",
"name": "Macarrão Espaguete 500g - Unidade",
"ean": "7891234567890",
"reference": "MAC-ESP-500",
"unitOfMeasure": "Un",
"manufacturerCode": "ME500",
"arrivalDate": "2026-07-29",
"weightForFreight": 0.52,
"heightForFreight": 5,
"widthForFreight": 10,
"lengthForFreight": 25,
"weightReal": 0.5,
"heightReal": 4,
"widthReal": 9,
"lengthReal": 24,
"active": true
}
]
}Duas armadilhas de nome nesta resposta. O campo da política volta como
commercialPoliticId — sem o y, e diferente de commercialPolicyId, que é o nome
aceito no envio. E a unidade de medida do SKU volta em unitOfMeasure, enquanto no
envio o campo se chama unitOfMeasurement. O valor é o mesmo texto ("Un") nos dois
lados; só a chave muda. Um cliente que lê esta resposta e a reenvia sem tradução perde
os dois campos silenciosamente — a atualização é parcial, e campo com nome desconhecido
é simplesmente ignorado.
O Swagger declara o retorno como lista de produtos; o corpo real é um objeto.
Um parser que espera array recebe erro de tipo. Produto inexistente devolve 404.
A unidade de medida muda de tipo conforme o serviço. Na API de integração ela é
texto ("Un", "Kg") — no envio, no retorno desta rota e no payload do webhook de
pedido. Nas rotas do core (/v1/gateway/..., como
GET /v1/gateway/orders/{id}/details e
Consultar clientes) ela vem como número:
| Texto | Número |
|---|---|
Un | 1 |
Kg | 2 |
G | 3 |
Mg | 4 |
Um switch sobre unitOfMeasure que funciona no webhook não casa nenhum caso na
resposta do core, e o item pesável passa a ser tratado como unitário.
Tirar um produto de circulação
Envie active: false no mesmo POST /v1/batch/Products. A atualização é parcial, então
o corpo pode ter só a chave e o campo:
curl -X POST $GROCERS_API/v1/batch/Products \
-H "Authorization: Basic $BASIC" \
-H "x-contractAccountId: $TENANT" \
-H "Content-Type: application/json" \
-d '[{"externalReference": "PROD-114970", "active": false}]'Não use DELETE /v1/batch/Products. A rota ainda existe e aparece no Swagger, mas
a implementação lança NotSupportedException: toda chamada volta 500 com
Product deletion is now handled through ProductWithSKUs flow. This method is obsolete.
e nenhum produto é excluído. A exclusão de produto passou a ser feita pelo fluxo de
ProductWithSKUs; hoje o caminho suportado é active: false.