Estoque
O estoque tem quatro visões do mesmo saldo: o que existe fisicamente, o que está reservado em carrinhos e pedidos em aberto, o que já foi vendido e ainda não saiu da prateleira, e o que sobra disponível pra nova venda. Este guia cobre como consultar e movimentar cada uma pela API.
Conceitos
| Campo | O que é |
|---|---|
quantity | Estoque físico total na sua loja. Só cai no despacho. |
reserved_quantity | Comprometido em carrinho/checkout, ainda não pago. |
sold_reserved_quantity | Vendido e pago, ainda não despachado. Continua na prateleira, mas já tem dono. |
available_quantity | O que sobra pra venda: quantity - reserved_quantity - sold_reserved_quantity. Nunca negativo. |
Regra: o catálogo respeita o available_quantity. Se ele zera, o produto deixa de aparecer pra novas compras, mesmo que ainda exista fisicamente.
O ciclo do saldo
| Momento | O que acontece |
|---|---|
| Item entra no carrinho | Sai do disponível, entra em reserved_quantity. Tem TTL de 15 minutos. |
| Checkout iniciado | Continua em reserved_quantity, agora sem TTL: só sai por pagamento ou cancelamento. |
| Pagamento confirmado | Sai de reserved_quantity, entra em sold_reserved_quantity. quantity não muda. |
| Despacho | quantity cai, e sold_reserved_quantity cai junto. |
| Cancelamento antes do despacho | Volta pro disponível, saindo do balde em que estava. quantity nunca foi tocado. |
Despacho é a confirmação de empacotamento, nas lojas com posto avançado, ou a marcação de pronto para envio nas lojas sem posto. Depois dele não há volta pelo estoque: mercadoria despachada que retorna é devolução, não liberação de saldo.
Se você espelha estoque num ERP, leia isto. O pagamento não debita mais o
quantity. Entre pagar e despachar, a mercadoria continua contada no físico, porque ela continua na prateleira — é o que faz a conferência de galpão contra sistema fechar. Quem sai na hora do pagamento é oavailable_quantity. Espelheavailable_quantitycomo "estoque vendável" equantitycomo "o que está no galpão"; usarquantitycomo vendável revende o que já tem dono.
O objeto que GET /products/{id}/stock devolve (total consolidado + saldo por localização):
{
"total": 100, // soma das quantidades físicas em todos os locais
"stocks": [ // ← saldo por localização
{
"id": 321, // ID do registro de saldo
"product_sku": {…}, // cadastro do SKU dono do saldo
"location_stock": {…}, // a localização deste saldo
"quantity": 100, // físico
"reserved_quantity": 5, // em carrinho/checkout, não pago
"sold_reserved_quantity": 20, // vendido, esperando despacho
"available_quantity": 75 // o que sobra pra venda
}
],
"locations": [{…}] // metadados dos locais (name, status, location_type…)
}O total é físico: conta também o que já foi vendido e ainda não despachou.
Tipos de movimentação
A rota de movimentação aceita 3 modos, controlados pelas flags booleanas increment e decrement:
| Flags enviadas | Efeito sobre quantity |
|---|---|
| nenhuma | Sobrescreve com o valor enviado. Útil pra acerto de inventário. |
increment: true | Soma. Útil pra entrada de fornecedor. |
decrement: true | Subtrai. Útil pra baixa manual (perda, avaria, venda fora do canal). |
Mandar as duas flags juntas é recusado com 400.
Os 3 modos mexem só em
quantity.reserved_quantityesold_reserved_quantitysão geridos pela plataforma, que move o saldo de balde conforme os pedidos abrem, pagam e despacham. Você cuida apenas do estoque físico.
Consultar saldo
{
"data": {
"total": 150,
"stocks": [
{
"id": 321,
"product_sku": {
"id": "01H81AV32307PVBSV4RXF15EK9",
"name": "Caneta Esferográfica Azul",
"sku": "CAN-AZ-01"
},
"location_stock": {
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"store_id": 42,
"name": "Matriz - São Paulo",
"status": "available",
"location_type": "seller_location",
"allows_pickup": false,
"allows_production": false
},
"quantity": 100,
"reserved_quantity": 5,
"sold_reserved_quantity": 20,
"available_quantity": 75
}
],
"locations": [
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"store_id": 42,
"name": "Matriz - São Paulo",
"status": "available",
"location_type": "seller_location",
"allows_pickup": false,
"allows_production": false
}
]
}
}No exemplo, o
product_skuaparece resumido; na prática ele vem com o cadastro do SKU (unidade, dimensões, flags de venda, imagens). Dois detalhes desta rota:pricesvem vazio e os quatro totais de estoque dentro doproduct_skuvêm zerados — os números que valem são os de cada linha destocks. Pra ver o SKU com preço e saldo consolidado, use Produtos.
Quem está segurando o saldo
GET/api/products/{id}/stock/holds
Os totais dizem quanto está preso; esta rota diz quem está prendendo. Uma linha por reserva ativa, com o pedido e o local, separando as duas situações no campo bucket:
reserved— carrinho ou checkout ainda não pago. Some sozinho no TTL ou vira venda. Nada a fazer.sold_reserved— pago, esperando despacho. É mercadoria com dono: se está parada há tempo demais, o problema é de expedição, não de estoque.
Vem ordenado com o vendido na frente e, dentro de cada balde, o mais antigo primeiro.
{
"data": {
"totals": {
"reserved": 30,
"sold_reserved": 70
},
"holds": [
{
"bucket": "sold_reserved",
"quantity": 70,
"order_id": "01H81AV32307PVBSV4RXF15ORD",
"order_number": "STD-ABC-999",
"order_status": "paid",
"customer_name": "Maria Souza",
"location_name": "Matriz - São Paulo",
"held_since": "2026-09-15T14:32:10-03:00"
},
{
"bucket": "reserved",
"quantity": 30,
"order_id": null,
"order_number": null,
"order_status": null,
"customer_name": null,
"location_name": "Matriz - São Paulo",
"held_since": "2026-09-16T09:05:44-03:00"
}
]
}
}Reserva de carrinho que ainda não virou pedido vem sem order_id, order_number, order_status e customer_name. Reserva já baixada, liberada ou expirada não aparece: se não segura saldo, não tem o que explicar.
Mesma permissão da consulta de saldo (store_products_read).
Movimentar
O location_id é obrigatório: diz em qual local o saldo se move. Sem flag nenhuma, o valor enviado sobrescreve a quantidade:
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 150
}Exemplos por cenário
Recebi 40 unidades do fornecedor:
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 40,
"increment": true
}Inventário deu 95 (era 100):
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 95
}Avaria, baixar 3:
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 3,
"decrement": true
}Zerei o item no ERP:
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 0
}Zerar o saldo tira o produto de circulação pra novas compras. O que já está em carrinho ou vendido e não despachado não é afetado: reserved_quantity e sold_reserved_quantity continuam onde estavam, e quem já pagou continua com a mercadoria reservada.
Regras do corpo
location_idequantitysão obrigatórios.quantityé um inteiro. No balanço (sem flag) aceita0— é assim que se zera um saldo. Comincrementoudecremento mínimo é1: movimentar0não é operação.- Reenviar o mesmo balanço responde sucesso mesmo quando nada muda, inclusive
0sobre um saldo que já é0. Quem espelha um ERP pode mandar o saldo de cada item sem consultar antes o que existe aqui. location_idprecisa ser olocation_id(ULID) de um local acessível pela loja (os mesmos que aparecem em Locais de estoque).
A resposta é um eco, não o saldo final
A resposta devolve o que você mandou, não o resultado da conta:
- com
incrementoudecrement, oquantityda resposta é o delta enviado, não a quantidade final; reserved_quantityesold_reserved_quantitysaem sempre0, e oavailable_quantityé calculado em cima desses zeros.
No banco nada disso se perde: a movimentação mexe só no físico e as reservas continuam intactas. Mas pra ver o saldo depois de movimentar, consulte o GET — não confie nos números do POST.
O que pode dar errado
| Erro | Quando acontece | Como tratar |
|---|---|---|
400 BAD_REQUEST | increment e decrement enviados juntos. | Escolha um dos dois, ou nenhum para sobrescrever. |
422 | O local é do tipo fulfillment. | Esse saldo é sincronizado pelo WMS da plataforma e não muda por esta rota. |
422 SUBSTORE_CATALOG_READ_ONLY | A loja opera em modo espelho. | SKU, preço e estoque vêm da matriz; a subloja não movimenta estoque. |
422 VALIDATION_ERROR | location_id inexistente, quantity negativo, ou quantity igual a 0 junto de increment/decrement. | Corrija os campos apontados em errors. Pra zerar um saldo, mande o balanço 0 sem flag. |
404 NOT_FOUND | O produto não existe no catálogo efetivo da loja. | Confira o {id} do produto. |

