Pular para conteúdo

Estoque e preço por SKU

Atualizar preço e estoque é a operação de maior frequência em uma integração ERP → Gubee. Este documento descreve os endpoints disponíveis, quando usar cada um e como tratar a consistência eventual inerente ao modelo CQRS do Gubee.

Modelo de dados

Cada variação de produto (SKU) tem:

  • 1 estoque por warehouseqty, priority, crossDockingTime, warehouseId. Default warehouse: "DEFAULT".
  • N preços — exatamente um DEFAULT (sem validityPeriod) e zero ou mais PROMOTION (com janela de validade).
  • N preços por plataforma — sobrepõe o default para uma plataforma específica (ex.: preço diferenciado no Mercado Livre).

Princípios do modelo

  1. Comando é assíncrono — todos os PUT de estoque/preço retornam 202 Accepted. A escrita é publicada como evento de domínio; o modelo de leitura converge em segundos.
  2. Leitura resolve por sku ou por itemIditemId é o skuId interno para produtos, adId para anúncios.
  3. skuId em path pode quebrar rota quando contém / (marketplaces específicos geram SKU com : e /). Por isso todos os endpoints críticos têm variante com identificadores no body.

Leia Eventual consistency e CQRS para entender o lag entre escrita e leitura.

Quando usar qual endpoint

Cenário Endpoint
Atualizar estoque de 1 SKU (caso comum) PUT /integration/stocks/bysku
Atualizar preço de 1 SKU (default) PUT /integration/prices/bysku
Atualizar preço default + promoção de 1 SKU PUT /integration/prices/list/bysku
Preço diferenciado por marketplace (mesmo SKU) PUT /integration/prices/platforms/v2
Ler preço efetivo de 1 SKU em 1 plataforma GET /integration/prices/{platform}/{itemId}
Ler estoque de 1 SKU em 1 warehouse GET /integration/stocks/{platform}/{itemId}/{whId}

Recomendado: para a maioria dos ERPs, bysku resolve 95% dos casos. Use endpoints de plataforma apenas quando há estratégia comercial de preço diferenciado por canal.

1. Atualizar estoque — single SKU

PUT /integration/stocks/bysku é o endpoint canônico. Resolve o skuId internamente a partir do SKU natural do ERP.

Request

curl -X PUT 'https://api.gubee.com.br/integration/stocks/bysku' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "qty": 100,
    "priority": 1,
    "sku": "PHONE-1001-BLK",
    "crossDockingTime": { "value": 1, "unit": "DAY" },
    "warehouseId": "DEFAULT"
  }'

Response

202 Accepted com body {}.

Campos da StockApiDTO

Campo Tipo Obrigatório Descrição
qty int sim Quantidade física. Mínimo 0.
sku string sim SKU natural do ERP.
priority int não (1) Prioridade do warehouse (multi-armazém).
crossDockingTime UnitTime sim Tempo de cruzamento (cross-docking).
warehouseId string não (DEFAULT) Identificador do warehouse.

Erros

Status Quando
400 SKU em branco ou payload inválido.
404 SKU não encontrado (precisa criar o produto).

Variantes para SKU com / no identificador

Se seu SKU contém / (ex.: 028021:mercadolivre:gold_premium:MLB2774308716), o BFF não consegue rotear via path param. Use a variante com identificadores no body:

curl -X PUT 'https://api.gubee.com.br/integration/stocks/v2' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "productId": "prod-erp-1001",
    "skuId": "028021:mercadolivre:gold_premium:MLB2774308716",
    "stock": {
      "qty": 50,
      "crossDockingTime": { "value": 1, "unit": "DAY" }
    }
  }'

Para manter o código uniforme, muitas integrações usam apenas /bysku e abstraem o caso de SKU com barra dentro do cliente HTTP.

Ler estoque

# Todos os warehouses de um SKU
curl 'https://api.gubee.com.br/integration/stocks/bysku?sku=PHONE-1001-BLK' \
  -H 'Authorization: Bearer $TOKEN'

# Estoque específico de plataforma + itemId + warehouse
curl 'https://api.gubee.com.br/integration/stocks/MERCADO_LIVRE/sku-uuid-1001-blk/CD_SP' \
  -H 'Authorization: Bearer $TOKEN'

Response (StockQuery):

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "sellerId": "SELLER123",
  "itemId": "sku-uuid-1001-blk",
  "warehouseId": "DEFAULT",
  "qty": 100,
  "booking": 10,
  "priority": 1,
  "sku": "PHONE-1001-BLK",
  "stockType": "DEFAULT",
  "domainType": "PRODUCT",
  "crossDockingTime": "P1D",
  "location": "A1-B2-C3"
}

2. Atualizar preço — single SKU (default)

PUT /integration/prices/bysku atualiza o preço DEFAULT do SKU.

Request

curl -X PUT 'https://api.gubee.com.br/integration/prices/bysku' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "sku": "PHONE-1001-BLK",
    "price": {
      "value": 4999.90,
      "type": "DEFAULT"
    }
  }'

Response

202 Accepted sem body.

Regras de validação

  • Preço DEFAULT não pode ter validityPeriod. Se enviar, será ignorado.
  • Preço PROMOTION deve ter validityPeriod com beginDt < endDt.
  • Para atualizar uma promoção existente, o validityPeriod deve ser idêntico. Caso contrário, será criada uma nova promoção (e a antiga permanece ativa até expirar).

3. Atualizar preço — default + promoções

PUT /integration/prices/list/bysku envia uma lista de preços (default + promoções agendadas) para o mesmo SKU:

curl -X PUT 'https://api.gubee.com.br/integration/prices/list/bysku' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "sku": "PHONE-1001-BLK",
    "prices": [
      { "value": 4999.90, "type": "DEFAULT" },
      {
        "value": 4499.90,
        "type": "PROMOTION",
        "validityPeriod": {
          "beginDt": "2026-07-10T00:00:00Z",
          "endDt": "2026-07-15T23:59:59Z"
        }
      },
      {
        "value": 4799.90,
        "type": "PROMOTION",
        "validityPeriod": {
          "beginDt": "2026-08-01T00:00:00Z",
          "endDt": "2026-08-15T23:59:59Z"
        }
      }
    ]
  }'

Constraints de validade

  • beginDt de uma nova promoção não pode cair dentro do intervalo de uma promoção existente para o mesmo SKU.
  • endDt deve ser maior que beginDt.
  • Janelas sobrepostas geram erro 400.

4. Preço diferenciado por plataforma

Para definir preço diferente do DEFAULT em marketplaces específicos (ex.: preço promocional só no Mercado Livre), use PUT /integration/prices/platforms/v2.

curl -X PUT 'https://api.gubee.com.br/integration/prices/platforms/v2' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "itemId": "sku-uuid-1001-blk",
    "domainType": "PRODUCT",
    "prices": [
      {
        "value": 4799.90,
        "type": "DEFAULT",
        "platform": "MERCADO_LIVRE"
      },
      {
        "value": 4599.90,
        "type": "PROMOTION",
        "platform": "MERCADO_LIVRE",
        "validityPeriod": {
          "beginDt": "2026-07-10T00:00:00Z",
          "endDt": "2026-07-15T23:59:59Z"
        }
      },
      {
        "value": 4899.90,
        "type": "DEFAULT",
        "platform": "SHOPEE"
      }
    ]
  }'

Response (200)

{
  "itemId": "sku-uuid-1001-blk",
  "sellerId": "SELLER123",
  "domainType": "PRODUCT",
  "status": "SUCCESS",
  "message": "Prices updated successfully"
}

Resolução de preço efetivo

Em leitura, o Gubee resolve na seguinte ordem:

  1. Preço plataforma-específico (se existir para o canal consultado).
  2. Preço DEFAULT global.
  3. Promoção ativa (dentro do validityPeriod).

domainType

  • PRODUCTitemId é o skuId interno.
  • ADVERTISEMENT (anúncio) — itemId é o adId.

5. Leituras — queries de preço

Por plataforma

curl 'https://api.gubee.com.br/integration/prices/MERCADO_LIVRE/sku-uuid-1001-blk' \
  -H 'Authorization: Bearer $TOKEN'

Response (SkuPlatformPriceQuery):

{
  "defaultPrice": {
    "id": "uuid-default",
    "platform": "MERCADO_LIVRE",
    "store": "STORE1",
    "value": 4799.90,
    "priceType": "DEFAULT",
    "status": "ACTIVE"
  },
  "promotionPrice": {
    "id": "uuid-promo",
    "platform": "MERCADO_LIVRE",
    "value": 4599.90,
    "beginDt": "2026-07-10T00:00:00Z",
    "endDt": "2026-07-15T23:59:59Z",
    "priceType": "PROMOTION",
    "status": "ACTIVE"
  },
  "scheduledPromotionPrices": [
    {
      "id": "uuid-future",
      "platform": "MERCADO_LIVRE",
      "value": 4699.90,
      "beginDt": "2026-08-01T00:00:00Z",
      "endDt": "2026-08-15T23:59:59Z",
      "priceType": "PROMOTION"
    }
  ],
  "skuPriceId": "uuid-skuprice",
  "itemId": "sku-uuid-1001-blk",
  "platform": "MERCADO_LIVRE",
  "domain": "PRODUCT"
}

Por SKU natural

curl 'https://api.gubee.com.br/integration/prices/bysku?sku=PHONE-1001-BLK' \
  -H 'Authorization: Bearer $TOKEN'

Por lista de itemIds (batch read)

curl -X POST 'https://api.gubee.com.br/integration/prices/byItemIds' \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '["sku-uuid-1001-blk", "sku-uuid-1001-wht", "sku-uuid-1002-blk"]'

Retorna List<SkuPriceQuery> — útil para sincronização inicial de cache local do ERP.

6. Árvore de decisão — qual endpoint usar

Você precisa atualizar…
├── Quantidade em estoque?
│   ├── 1 SKU, caso comum ───────────► PUT /integration/stocks/bysku
│   └── SKU com "/" no identificador ─► PUT /integration/stocks/v2
├── Preço?
│   ├── Apenas preço DEFAULT ────────► PUT /integration/prices/bysku
│   ├── DEFAULT + promoções ─────────► PUT /integration/prices/list/bysku
│   └── Diferenciado por marketplace ► PUT /integration/prices/platforms/v2
└── Apenas ler?
    ├── Preço de um SKU por canal ───► GET /integration/prices/{platform}/{itemId}
    ├── Preço de um SKU por nome ────► GET /integration/prices/bysku?sku=...
    ├── Batch de N itemIds ─────────► POST /integration/prices/byItemIds
    └── Estoque por SKU + warehouse ► GET /integration/stocks/{platform}/{itemId}/{warehouseId}

Consistência eventual — leitura após escrita

Após um PUT .../bysku retornar 202 Accepted, o evento de domínio é publicado em ~100-500ms e o modelo de leitura é atualizado em 1-3s. Em picos de carga pode chegar a 10s.

Padrão anti-pattern

# ERRADO: lê imediatamente após escrever
put_stock(sku, qty=100)
stock = get_stock(sku)
assert stock.qty == 100  # PODE FALHAR

Padrão correto

# CERTO: agenda sync read-after-write com retry
put_stock(sku, qty=100)
for attempt in range(10):
    sleep(2)
    stock = get_stock(sku)
    if stock and stock.qty == 100:
        break
else:
    log.warning(f"lag persistente em {sku}")

Para detalhes do modelo, veja Eventual consistency e CQRS.

Padrão de sincronização ERP → Gubee

Estrutura típica de um worker de sync incremental:

def sync_stock_and_price_loop():
    while True:
        changes = erp.fetch_changed_skus(since=last_sync)
        if not changes:
            sleep(30)
            continue

        # Paraleliza por SKU (até 20 concorrentes)
        with ThreadPoolExecutor(max_workers=20) as pool:
            for change in changes:
                pool.submit(sync_one, change)

        last_sync = now()


def sync_one(change):
    try:
        put_stock(sku=change.sku, qty=change.qty)
        if change.price_changed:
            put_price(sku=change.sku, value=change.price)
    except ConflictError as e:
        log.warning(f"conflict in {change.sku}: {e}")
        # Re-faz o GET para reconciliar
        reconcile(change.sku)
    except NotFoundError:
        log.error(f"SKU {change.sku} não existe no Gubee — produto pendente")

Frequência recomendada

  • Estoque: a cada 30s-2min por seller. Reduzido conforme volume.
  • Preço: a cada 1-5min. Promoções agendadas devem ser enviadas com até 1h de antecedência para garantir propagação.
  • Workers horizontais: 1 worker por seller é o padrão seguro. Para sellers com > 50k SKUs ativos, considere 2-3 workers particionando por hash do SKU.

Tratamento de erros

Status Causa Ação
202 Aceito (sucesso) Aguarde propagação.
400 Payload inválido ou janela de promoção conflitante Corrija e refaça. Log do erro tem o campo conflitante.
404 SKU não existe Crie o produto via sync-catalog antes.
409 Conflito de estado Re-leia e reconcilie.
5xx Falha temporária Backoff exponencial (até 3 retries).

Próximos passos