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 warehouse —
qty,priority,crossDockingTime,warehouseId. Default warehouse:"DEFAULT". - N preços — exatamente um
DEFAULT(semvalidityPeriod) e zero ou maisPROMOTION(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¶
- Comando é assíncrono — todos os
PUTde estoque/preço retornam202 Accepted. A escrita é publicada como evento de domínio; o modelo de leitura converge em segundos. - Leitura resolve por
skuou poritemId—itemIdé oskuIdinterno para produtos,adIdpara anúncios. skuIdem 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,
byskuresolve 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
DEFAULTnão pode tervalidityPeriod. Se enviar, será ignorado. - Preço
PROMOTIONdeve tervalidityPeriodcombeginDt < endDt. - Para atualizar uma promoção existente, o
validityPerioddeve 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¶
beginDtde uma nova promoção não pode cair dentro do intervalo de uma promoção existente para o mesmo SKU.endDtdeve ser maior quebeginDt.- 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:
- Preço plataforma-específico (se existir para o canal consultado).
- Preço
DEFAULTglobal. - Promoção ativa (dentro do
validityPeriod).
domainType¶
PRODUCT—itemIdé oskuIdinterno.ADVERTISEMENT(anúncio) —itemIdé oadId.
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¶
- Sincronizar catálogo — criar produtos antes de preço/estoque.
- Receber pedidos — consumir pedidos que usam esses SKUs.
- Faturamento e DANFE — emitir NF após pagamento.