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

CampoO que é
quantityEstoque físico total na sua loja. Só cai no despacho.
reserved_quantityComprometido em carrinho/checkout, ainda não pago.
sold_reserved_quantityVendido e pago, ainda não despachado. Continua na prateleira, mas já tem dono.
available_quantityO 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

MomentoO que acontece
Item entra no carrinhoSai do disponível, entra em reserved_quantity. Tem TTL de 15 minutos.
Checkout iniciadoContinua em reserved_quantity, agora sem TTL: só sai por pagamento ou cancelamento.
Pagamento confirmadoSai de reserved_quantity, entra em sold_reserved_quantity. quantity não muda.
Despachoquantity cai, e sold_reserved_quantity cai junto.
Cancelamento antes do despachoVolta 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 é o available_quantity. Espelhe available_quantity como "estoque vendável" e quantity como "o que está no galpão"; usar quantity como 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 enviadasEfeito sobre quantity
nenhumaSobrescreve com o valor enviado. Útil pra acerto de inventário.
increment: trueSoma. Útil pra entrada de fornecedor.
decrement: trueSubtrai. Ú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_quantity e sold_reserved_quantity sã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

GET/api/products/{id}/stock

{
  "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_sku aparece resumido; na prática ele vem com o cadastro do SKU (unidade, dimensões, flags de venda, imagens). Dois detalhes desta rota: prices vem vazio e os quatro totais de estoque dentro do product_sku vêm zerados — os números que valem são os de cada linha de stocks. 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

POST/api/products/{id}/stock

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_id e quantity são obrigatórios.
  • quantity é um inteiro. No balanço (sem flag) aceita 0 — é assim que se zera um saldo. Com increment ou decrement o mínimo é 1: movimentar 0 não é operação.
  • Reenviar o mesmo balanço responde sucesso mesmo quando nada muda, inclusive 0 sobre um saldo que já é 0. Quem espelha um ERP pode mandar o saldo de cada item sem consultar antes o que existe aqui.
  • location_id precisa ser o location_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 increment ou decrement, o quantity da resposta é o delta enviado, não a quantidade final;
  • reserved_quantity e sold_reserved_quantity saem sempre 0, e o available_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

ErroQuando aconteceComo tratar
400 BAD_REQUESTincrement e decrement enviados juntos.Escolha um dos dois, ou nenhum para sobrescrever.
422O local é do tipo fulfillment.Esse saldo é sincronizado pelo WMS da plataforma e não muda por esta rota.
422 SUBSTORE_CATALOG_READ_ONLYA loja opera em modo espelho.SKU, preço e estoque vêm da matriz; a subloja não movimenta estoque.
422 VALIDATION_ERRORlocation_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_FOUNDO produto não existe no catálogo efetivo da loja.Confira o {id} do produto.