Pular para conteúdo

Sincronizar catálogo de produtos

Esta jornada descreve como um ERP integrador deve sincronizar um catálogo grande (dezenas de milhares de produtos) com o Gubee seguindo a ordem correta de dependências entre domínios.

Recomendado: use POST /integration/products/v2/createupdate para tudo

O endpoint /v2/createupdate cria automaticamente categoria, marca e atributos que não existirem ainda, na mesma chamada em que cria o produto. Em 95% dos casos de integração, você não precisa criar categorias, marcas ou atributos separadamente — basta enviar tudo no payload do produto.

POST /integration/products/v2/createupdate
{
  "externalId": "X",
  "name": "...",
  "mainCategory": "Esportes > Calçados > Tênis",   # criado em cascata
  "brand": "MarcaDemo",                             # criado se não existe
  "specifications": [{ "name": "Gênero", "values": ["Masculino"] }],
  "variations": [
    {
      "sku": "SKU-X",
      "variantSpecification": [{ "name": "Cor", "values": ["Preto"] }],
      "prices": [...], "stocks": [...], "images": [...]
    }
  ]
}

Use os passos abaixo (bulk de categoria/atributo/marca) apenas quando:

  • Você quer reutilizar categorias em 10k+ produtos sem reenviar a string a cada chamada (economia de processamento)
  • Você precisa criar categorias com metadados adicionais (externalId próprio, attributes obrigatórios por categoria) antes de subir produtos
  • Você está migrando um catálogo legado comHierquia rígida que precisa ser validada primeiro

Para a maioria dos ERPs, o fluxo é simplesmente: loop de POST /v2/createupdate para cada produto. Fim.

Pré-requisitos

  • sellerId já cadastrado no Gubee (vem da autorização do seller, extraído do token).
  • Access token de um app OAuth2 autorizado pelo seller, com product:edit. Veja o Guia do desenvolvedor.
  • Os domínios abaixo têm dependência estrita de ordem (quando você escolhe criar separadamente):
categorias ──┐
marcas     ──┼──> produtos ──> variações (sku) ──> estoque/preço
atributos  ──┘

Visão geral do fluxo

Fluxo recomendado (90% dos casos):

  1. Produtos — loop de POST /integration/products/v2/createupdate enviando mainCategory (string com >), brand, specifications e variations[].variantSpecification no mesmo payload. Backend resolve tudo.

Fluxo alternativo (catálogo massivo, 10k+ produtos):

  1. Categorias — enviar em bulk, pais antes dos filhos (uma vez).
  2. Atributos — enviar em bulk (uma vez).
  3. Marcas — enviar em loop ou bulk (uma vez).
  4. Produtos — upsert via POST /integration/products/v2/createupdate (passa mainCategory como string; backend reutiliza a categoria já criada sem duplicar).
  5. Mapear SKUs entre o ERP e o skuId interno do Gubee.

A separação entre externalId (chave do ERP) e skuId/hubeeId (chaves internas) é central no modelo — leia Identificadores: externalId, skuId, hubeeId antes de codificar.

Passo 1 — Criar categorias em bulk

POST /integration/categories/bulk aceita uma lista de CategoryApiDTO e cria em uma única transação. A ordem dentro do array importa: categorias pai devem vir antes das filhas, porque o campo parent referencia o name (chave natural) da categoria pai já integrada.

Request

curl -X POST 'https://api.gubee.com.br/integration/categories/bulk' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '[
    {
      "id": "cat-erp-1",
      "name": "Eletrônicos",
      "description": "Categoria raiz de eletrônicos",
      "active": true,
      "enabledAutoIntegration": true
    },
    {
      "id": "cat-erp-2",
      "parent": "Eletrônicos",
      "name": "Smartphones",
      "active": true
    },
    {
      "id": "cat-erp-3",
      "parent": "Eletrônicos > Smartphones",
      "name": "Acessórios para Smartphones",
      "active": true
    }
  ]'

Response 200

[
  { "id": "cat-erp-1", "hubeeId": "60f8a5c2e3b2a87654321000" },
  { "id": "cat-erp-2", "hubeeId": "60f8a5c2e3b2a87654321001" },
  { "id": "cat-erp-3", "hubeeId": "60f8a5c2e3b2a87654321002" }
]

Campos da CategoryApiDTO

Campo Tipo Obrigatório Descrição
id string não externalId no ERP. Recomendado p/ idempotência.
name string sim Chave natural única por seller.
parent string não Nome da categoria pai. Para níveis profundos use "A > B > C".
description string não —
active boolean não (false) —
enabledAutoIntegration boolean não (true) Replicar automaticamente para marketplaces.

Atualização em bulk

PUT /integration/categories/bulk com o mesmo formato, localizando por externalId (id). Se a categoria não existir, retorna 404. Para atualizar uma única, use PUT /integration/categories/{externalId}.

Passo 2 — Criar atributos em bulk

POST /integration/attributes/bulk segue o mesmo padrão. Atributos podem ser de produto (aplicados ao produto todo) ou variantes (definem combinações de variação — ex.: "Cor", "Tamanho").

Request

curl -X POST 'https://api.gubee.com.br/integration/attributes/bulk' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '[
    {
      "id": "attr-cor",
      "name": "Cor",
      "label": "Cor",
      "attrType": "TEXT",
      "variant": true,
      "required": true,
      "options": ["Preto", "Branco", "Azul", "Vermelho"]
    },
    {
      "id": "attr-tamanho",
      "name": "Tamanho",
      "attrType": "TEXT",
      "variant": true,
      "options": ["P", "M", "G", "GG"]
    },
    {
      "id": "attr-material",
      "name": "Material",
      "attrType": "TEXT",
      "variant": false,
      "options": ["Plástico", "Alumínio", "Vidro"]
    }
  ]'

Campos da AttributeApiDTO

Campo Tipo Obrigatório Descrição
id string não externalId no ERP.
name string sim Chave natural única.
label string não Rótulo exibido.
attrType TEXT não (TEXT) Apenas TEXT é tratado atualmente.
variant boolean não (false) Se true, define variações do produto.
required boolean não (false) Obriga preenchimento no produto.
options List<String> não Lista de valores permitidos para o atributo.

Passo 3 — Criar marcas

Marcas não têm endpoint bulk. Crie uma a uma (paralelize, se necessário):

curl -X POST 'https://api.gubee.com.br/integration/brands' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "brand-samsung",
    "name": "Samsung",
    "description": "Samsung Electronics"
  }'

Para atualizar: PUT /integration/brands/byExternalId/{externalId}. Para remover uma marca obsoleta que está bloqueando upsert por nome: DELETE /integration/brands/byExternalId/{externalId} (caso raro — Magento recria marcas e o registro antigo conflita).

Passo 4 — Upsert de produtos via V2 (recomendado)

POST /integration/products/v2/createupdate é o endpoint canônico para sincronização em massa. Ele é idempotente por externalId: se já existe, atualiza; se não, cria.

A vantagem principal sobre o V1 (POST /integration/products) é que o campo mainCategory aceita a string completa "level1 > level2 > level3" — o backend resolve o hubeeId internamente, sem precisar de lookup prévio.

Request completo

curl -X POST 'https://api.gubee.com.br/integration/products/v2/createupdate' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "externalId": "prod-erp-1001",
    "mainSku": "PHONE-1001-BLK",
    "name": "Samsung Galaxy S24 256GB Preto",
    "mainCategory": "Eletrônicos > Smartphones",
    "brand": "Samsung",
    "status": "ACTIVE",
    "type": "SIMPLE",
    "origin": "NATIONAL",
    "ncm": "8517.12.31",
    "specifications": [
      { "name": "Material", "value": "Alumínio" }
    ],
    "variations": [
      {
        "sku": "PHONE-1001-BLK",
        "main": true,
        "ean": "7891234567890",
        "eans": [
          { "value": "7891234567890", "main": true },
          { "value": "17891234567897", "main": false }
        ],
        "cost": 3200.00,
        "warrantyTime": 365,
        "warrantyType": "FACTORY_WARRANTY",
        "handlingTime": 1,
        "dimension": {
          "weight": { "value": 0.167, "unit": "KILOGRAM" },
          "length": 7.0, "width": 15.0, "height": 0.6
        },
        "images": [
          { "url": "https://cdn.erp.com/img/s24-front.jpg", "main": true },
          { "url": "https://cdn.erp.com/img/s24-back.jpg" }
        ],
        "stocks": [
          { "qty": 50, "warehouseId": "DEFAULT", "crossDockingTime": { "value": 1, "unit": "DAY" } }
        ],
        "prices": [
          { "value": 4999.90, "type": "DEFAULT" }
        ]
      },
      {
        "sku": "PHONE-1001-WHT",
        "main": false,
        "ean": "7891234567891",
        "cost": 3200.00,
        "prices": [{ "value": 4999.90, "type": "DEFAULT" }],
        "stocks": [{ "qty": 25, "warehouseId": "DEFAULT" }]
      }
    ]
  }'

Response 200

O endpoint retorna o productId (externalId) em text/plain:

prod-erp-1001

Campos principais da CreateUpdateProductApiDTO

Campo Tipo Obrigatório Notas
externalId string recomendado Chave do ERP. Idempotência baseada aqui.
mainSku string recomendado SKU da variação principal.
name string sim —
mainCategory string sim "Cat1" ou "Cat1 > Cat2 > Cat3".
brand string sim Nome exato da marca já integrada.
status ACTIVE/INACTIVE não (ACTIVE) —
type SIMPLE/KIT não (SIMPLE) —
origin NATIONAL/IMPORTED não (NATIONAL) —
ncm string não —
specifications Set<Attribute> não Atributos de produto (não variantes).
variations List<Variation> sim Pelo menos 1. Variação main=true será a principal.

Variação (CreateUpdateVariationApiDTO)

Campo Tipo Obrigatório Notas
skuId string não no create Gerado automaticamente no create; obrigatório no update. No PUT, variação sem skuId é recriada — veja PUT sem skuId.
sku string sim SKU natural do ERP.
main boolean não (false) Exatamente uma variação deve ser main=true.
eans List<Ean> não Códigos EAN/GTIN da variação: itens { "value", "main" }, até 20, um deles principal. Veja Lista de EANs.
ean string não Depreciado (sem data de remoção): EAN principal. Continua aceito. Ao enviar eans, envie também ean com o valor do item principal.
cost BigDecimal não Custo para cálculo de rentabilidade.
dimension Dimension não Peso + medidas para cálculo de frete.
warrantyTime Long não Dias.
warrantyType SELLER_WARRANTY/FACTORY_WARRANTY não Tipo de garantia. Omitido = mantém o valor atual. Valor fora da lista → 400.
handlingTime Long não Dias para despacho.
images Set<Image> não Veja Gestão de imagens.
stocks Set<Stock> não Atalho para criaar estoque junto.
prices Set<Price> não Atalho para criar preço junto.
videos Set<Video> não Merge cirúrgico (somente main/order editáveis).

Importante sobre videos: o campo no payload de produto serve apenas para metadados de apresentação (main, order). Não é possível criar vídeo via PUT de produto — use o fluxo dedicado em Upload de vídeos.

Lista de EANs

Uma variação pode ter mais de um código de barras (unidade e embalagem, GTIN-13 e GTIN-14, troca de fornecedor). O campo eans leva todos; um deles é o principal.

{
  "ean": "7891234567890",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "17891234567897", "main": false }
  ]
}
Campo Tipo Descrição
eans[].value string Valor do EAN.
eans[].main boolean true no EAN principal. Exatamente um item por lista.
  • A lista aceita até 20 itens, contados depois de juntar os repetidos.
  • ean está depreciado, sem data de remoção. Na leitura, devolve o EAN principal de eans.
  • Os valores são gravados sem espaços nas pontas. Formato e dígito verificador não são validados.
  • Valor repetido na lista não é erro: os repetidos viram um item só, na posição do primeiro, e ele é o principal se alguma das ocorrências estiver marcada. Item com value em branco é descartado; uma lista só com itens em branco vale como eans: [].

Leitura. As consultas de produto devolvem ean e eans em cada variação. Variação sem EAN devolve eans: [].

Escrita. Vale para POST /integration/products, POST /integration/products/v2/createupdate, PUT /integration/products/{externalId} e para o PATCH de variação.

Envie sempre ean junto com eans

A gravação da lista eans é habilitada por ambiente. Enquanto não estiver habilitada no seu, o campo eans da requisição é ignorado e só o ean é gravado: um payload só com eans não grava os EANs enviados, mesmo quando a resposta é de sucesso. Envie os dois campos, com ean igual ao value do item main: true — como nos exemplos desta página. Esse par grava o principal com a lista desabilitada e a lista inteira com ela habilitada.

Com a lista desabilitada, eans: [] sem ean só limpa o EAN em POST e PUT de produto; no PATCH de variação é ignorado e nada muda. Para remover os EANs em qualquer caso, envie ean: "".

Quando o payload traz um campo só:

O que o payload traz Resultado
Nem ean nem eans (ausentes ou null) Os EANs gravados não são alterados.
Só ean com valor O valor vira o principal. Se já era um secundário, é promovido e o principal anterior continua na lista; se não estava na lista, substitui o principal. Os demais secundários são mantidos.
Só ean: "" Remove todos os EANs da variação.
Só eans com itens A lista enviada substitui a gravada. O principal é o item com main: true; sem item marcado, o primeiro da lista.
Só eans: [] Remove todos os EANs da variação. Com a gravação da lista desabilitada, só em POST e PUT; no PATCH é ignorado.

Quem envia só ean não precisa mudar nada: o valor é gravado como principal e a leitura seguinte devolve o mesmo valor em ean.

Quando o payload traz os dois campos (ean com valor e eans) numa variação que já existe, cada um é comparado com o que está gravado — é o que protege quem lê o produto, altera um campo e devolve o corpo inteiro:

eans enviada × gravada ean enviado × principal gravado Resultado
igual igual Nada muda.
igual diferente Vale o ean: vira o principal, como em "Só ean com valor". A lista enviada é o eco do que estava gravado e não é validada.
diferente igual ou diferente Vale a lista, e o par tem de ser coerente: ean precisa estar em eans e, se a lista traz um item com main: true, esse item precisa ser o ean. Se a lista não traz item marcado, o ean é o principal. Par incoerente → 400.

"eans diferente da gravada" quer dizer: depois de tirar espaços, itens em branco e repetidos, os valores ou a ordem mudaram. A marca main só entra na comparação quando a lista enviada traz algum item marcado.

Na criação (produto novo ou variação nova) não há estado gravado: vale a lista, o principal é o item com main: true (sem item marcado, o ean enviado; sem ean, o primeiro) e o ean enviado precisa estar na lista. ean: "" com eans com itens, na criação, grava a lista: o ean em branco conta como ean não enviado.

Quando o payload traz ean: "" (vazio ou só com espaços) junto com eans, numa variação que já existe:

eans enviada × gravada Resultado
eans: [] Remove todos os EANs da variação.
igual Remove todos os EANs da variação. É o caso de quem lê o produto, esvazia ean e devolve o corpo inteiro.
com itens e diferente 400, motivo EAN_NOT_MAIN_OF_EANS. Nada é gravado.

Com a gravação da lista desabilitada, eans é ignorado e ean: "" remove todos os EANs, qualquer que seja a lista enviada.

Para remover todos os EANs, a forma que vale sempre — POST, PUT e PATCH, com a gravação da lista habilitada ou não — é enviar só ean: "", sem eans.

Exemplos. Trecho da variação no corpo da requisição. A variação está gravada com 7891234567890 (principal) e 17891234567897.

Trocar o principal enviando só ean — aceito, o secundário é mantido:

{ "ean": "7891234567999" }

Devolver o corpo lido alterando só ean — aceito, mesmo resultado (a lista é o eco do gravado):

{
  "ean": "7891234567999",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "17891234567897", "main": false }
  ]
}

Acrescentar um secundário, par coerente — aceito, vale a lista:

{
  "ean": "7891234567890",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "17891234567897", "main": false },
    { "value": "27891234567894", "main": false }
  ]
}

Devolver o corpo lido com ean em branco — aceito, remove os dois EANs (a lista é o eco do gravado):

{
  "ean": "",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "17891234567897", "main": false }
  ]
}

ean em branco com uma lista diferente da gravada — 400, motivo EAN_NOT_MAIN_OF_EANS, nada é gravado:

{
  "ean": "",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "27891234567894", "main": false }
  ]
}

Lista alterada e ean fora dela — 400, motivo EAN_NOT_IN_EANS:

{
  "ean": "7891234567999",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "27891234567894", "main": false }
  ]
}

Lista alterada e ean na lista, mas o item marcado é outro — 400, motivo EAN_NOT_MAIN_OF_EANS:

{
  "ean": "17891234567897",
  "eans": [
    { "value": "7891234567890", "main": true },
    { "value": "17891234567897", "main": false },
    { "value": "27891234567894", "main": false }
  ]
}

Repetir a mesma requisição. Um par coerente pode ser reenviado (retry) quantas vezes for: o resultado é o mesmo. Um par incoerente que foi aceito na primeira vez porque a lista era o eco do gravado (segundo exemplo acima) recebe 400 se for reenviado igual — na segunda vez a lista já não é a gravada. O EAN gravado pela primeira requisição continua lá. Antes de alterar de novo, leia o produto ou envie o par coerente.

O corpo com ean: "" e a lista lida segue a mesma lógica: a primeira requisição remove os EANs; repetida igual, a lista enviada já difere da gravada (agora vazia) e a resposta é 400, motivo EAN_NOT_MAIN_OF_EANS. Os EANs continuam removidos. Isso com a gravação da lista habilitada; com ela desabilitada, a repetição é aceita e nada muda. Para remover de forma que possa ser repetida sem erro, envie só ean: "", sem eans.

Erros de EAN

400 — lista recusada. Nada é gravado. A resposta é application/problem+json, com type terminado em /invalid-ean-list e o motivo, em texto, em detail (outros campos podem acompanhar):

{
  "type": "http://www.gubee.com.br/problem/invalid-ean-list",
  "status": 400,
  "detail": "ean enviado nao esta na lista eans"
}
Motivo Quando detail
MULTIPLE_MAIN eans tem mais de um item com main: true. eans com mais de um item principal
TOO_MANY eans tem mais de 20 itens, depois de juntar os repetidos. eans acima do limite de itens
EAN_NOT_IN_EANS ean com valor enviado junto com eans, e o valor não está na lista. Na atualização, só quando a lista difere da gravada. ean enviado nao esta na lista eans
EAN_NOT_MAIN_OF_EANS Atualização, quando a lista difere da gravada: o ean está nela, mas o item com main: true é outro; ou ean: "" enviado com eans com itens. ean enviado diverge do item principal de eans

O nome do motivo não vem como campo da resposta: identifique o erro pelo type e use o detail como mensagem. Só quem envia eans recebe esses erros.

409 — conflito no PATCH de variação que traz ean ou eans. type terminado em /ean-patch-conflict: outra escrita alterou o EAN da mesma variação enquanto o PATCH era gravado, em todas as tentativas internas. Nada do PATCH foi gravado (nem os outros campos do corpo); repita a requisição.

PUT sem skuId

Na atualização completa (PUT), a variação enviada sem skuId é tratada como nova, mesmo com o sku de uma variação que já existe: a variação gravada é removida e recriada só com o que veio no payload. Se ela tinha EANs secundários e o payload traz só ean, os secundários são perdidos, sem erro. Envie o skuId de cada variação existente (ele vem na leitura do produto e em POST /integration/products/skuIds/bySkus) ou use o PATCH de variação.

Atenção

Omitir ean (ou enviar null) numa atualização completa não apaga o EAN. Para remover, envie ean: "".

Busca. O filtro ean de POST /integration/products/list/search casa com qualquer EAN da lista, principal ou secundário, de qualquer variação.

Passo 5 — Atualizações incrementais

Atualização total (PUT)

Use PUT /integration/products/{externalId} quando precisar reescrever o produto inteiro (variações, dimensões, atributos, imagens, etc.). Substitui o estado anterior:

curl -X PUT 'https://api.gubee.com.br/integration/products/prod-erp-1001' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ ...produto completo... }'

Retorna 202 Accepted (sem body). Atualização é assíncrona via evento assíncrono na plataforma de streaming. Também disponível por mainSkuId: PUT /integration/products/byMainSkuId/{mainSkuId}.

Atualização parcial (PATCH) — recomendada para correções pontuais

PATCH /integration/products/variations/sku/{sku} altera apenas os campos informados no body. Nulos são ignorados. ean e eans seguem as regras de Lista de EANs: ean: "" remove todos; eans: null mantém, eans: [] remove todos e uma lista substitui a gravada (enviada junto com ean com valor, o par tem de ser coerente). Enquanto a gravação da lista não estiver habilitada no ambiente, o PATCH ignora eans — inclusive eans: [], que sozinho não altera nada; use ean: "" para remover. O PATCH com ean ou eans pode responder 409 — veja Erros de EAN. Semântica de images:

  • null → preserva atual
  • [] → remove todas
  • [...] → substitui atomicamente (URLs/existentes são preservadas via merge server-side por url+uuid)
curl -X PATCH 'https://api.gubee.com.br/integration/products/variations/sku/PHONE-1001-BLK' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "cost": 2999.00,
    "ean": "7891234567999",
    "warrantyTime": 540,
    "warrantyType": "SELLER_WARRANTY",
    "handlingTime": 2,
    "dimension": { "weight": { "value": 0.170, "unit": "KILOGRAM" } },
    "name": "Galaxy S24 256GB Preto Edição 2025"
  }'

Response:

{ "modified": true }

PATCH /integration/products/{externalId} atualiza campos escalares do produto (name, nbm, origin, originCountry, disableIntegration).

Endpoints granulares de imagem

Para operações cirúrgicas sem reescrever a lista:

Operação Endpoint
Adicionar imagem POST /integration/products/{externalId}/variations/{skuId}/images
Remover imagem DELETE /integration/products/{externalId}/variations/{skuId}/images/{imageUuid}
Definir como principal PUT /integration/products/{externalId}/variations/{skuId}/images/{imageUuid}/main
Reordenar PUT /integration/products/{externalId}/variations/{skuId}/images/order
Patch metadados PATCH /integration/products/{externalId}/variations/{skuId}/images/{imageUuid}

Passo 6 — Mapear SKUs do ERP para skuId interno

A função mais usada em integração contínua. O ERP trabalha com o SKU natural; os endpoints de leitura internos (estoque, preço, pedidos) frequentemente retornam o skuId interno do Gubee.

POST /integration/products/skuIds/bySkus resolve um lote de SKUs em skuIds:

curl -X POST 'https://api.gubee.com.br/integration/products/skuIds/bySkus' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '["PHONE-1001-BLK", "PHONE-1001-WHT", "PHONE-1002-BLK"]'

Response

[
  {
    "id": "60f8a5c2e3b2a87654321100",
    "skuId": "sku-uuid-1001-blk",
    "sku": "PHONE-1001-BLK",
    "ean": "7891234567890"
  },
  {
    "id": "60f8a5c2e3b2a87654321101",
    "skuId": "sku-uuid-1001-wht",
    "sku": "PHONE-1001-WHT",
    "ean": "7891234567891"
  }
]

SKUs não encontrados são omitidos do array de resposta (não geram erro). Compare o tamanho do array retornado com o enviado para detectar SKUs pendentes.

Também disponível: - GET /integration/products/skuIds/bySkus?skus=A,B,C (limitado pela URL). - GET /integration/products/skus/bySkuIds?skuIds=X,Y — caminho inverso. - POST /integration/products/skus/bySkuIds — versão POST do anterior.

Variação retornada: VariationSkuApiMapDTO

Campo Tipo Descrição
id string ID interno da variação.
skuId string skuId usado em endpoints internos.
sku string SKU natural do ERP.
ean string EAN principal, se cadastrado. Esta resposta não traz a lista eans.

Tratamento de erros

409 Conflict — EAN alterado durante o PATCH

type terminado em /ean-patch-conflict, só no PATCH de variação que traz ean ou eans. Nada foi gravado; repita a requisição. Veja Erros de EAN.

409 Conflict — externalId duplicado

Ocorre quando duas requests concorrentes tentam criar o mesmo externalId, ou quando há inconsistência no banco (extremamente raro). Solução:

  1. Faça GET /integration/products/{externalId} para checar o estado atual.
  2. Se existir, use PUT (ou PATCH) ao invés de POST.
  3. Se não existir mas ainda assim receber 409, aguarde 5s e refaça o GET.

404 — Relação não integrada

Some of relation [attribute, category, brand] not integrated before. Causa: você está referenciando no payload um nome/ID que ainda não foi integrado. Verifique se categoria/marca/atributo já existem (via GET /byName ou GET /{externalId} dos respectivos endpoints) antes de reenviar o produto.

400 — Validação

  • mainSku em branco.
  • variations[].sku em branco.
  • Mais de uma variação main=true.
  • Lista de EANs recusada (type terminado em /invalid-ean-list): mais de um item main=true, mais de 20 itens, ou — quando a lista difere da gravada — ean enviado que não está em eans ou que não é o item principal. ean novo com a lista igual à gravada não é erro. Veja Erros de EAN.
  • Categoria como hubeeId ao invés do nome completo (no V2, use sempre o caminho "A > B > C").

Performance — sincronizando 10k+ produtos

Paralelização

O limite prático por seller é ~20 requisições concorrentes no endpoint /v2/createupdate. Acima disso o backend começa a aplicar backpressure.

Estratégia recomendada:

              ┌── worker 1 (POST produtos 1..500)
              ├── worker 2 (POST produtos 501..1000)
fila ERP ────┼── worker 3 (POST produtos 1001..1500)
              ├── ...
              └── worker N (POST produtos 9501..10000)

Cada worker consome um batch local de 100-500 itens em sequência. A paralelização real acontece entre workers, não dentro de um único worker.

Tamanho de batch

  • Categorias/atributos em bulk: até 200 por chamada.
  • Produtos: o endpoint /v2/createupdate aceita um produto por chamada. Use pool de conexões HTTP persistente (Keep-Alive) e pipelining.

Idempotência

O campo externalId é a âncora de idempotência. Pode-se reexecutar a sincronização integral do catálogo a qualquer momento — produtos existentes serão atualizados em vez de duplicados.

Carga inicial vs incremental

  • Carga inicial: rode à noite ou em janela de baixo tráfego. 10k produtos com 3 variações em média = ~30k chamadas. A ~5/s por worker com 10 workers = ~10 min.
  • Incremental: monitore mudanças no ERP (timestamp de última alteração) e sincronize apenas o delta.

Read-after-write

O Gubee usa CQRS com eventual consistência entre comando e leitura. Após um POST /v2/createupdate retornar 200, o produto pode levar de 1 a 5 segundos para aparecer em GET /integration/products/{externalId}. Veja Eventual consistency e CQRS.

Checklist final

  • Categorias integradas (pais antes dos filhos).
  • Atributos integrados (variantes e não-variantes).
  • Marcas integradas.
  • Produtos upsertados via /v2/createupdate.
  • Tabela de mapeamento sku → skuId persistida no ERP (via /skuIds/bySkus).
  • Estoque e preço sincronizados — veja Estoque e preço.
  • Worker de pedidos em pé — veja Receber pedidos.

Próximos passos