Orientações de Integração — API Magento 2
Leitura e atualização de Preços e Estoque (lojas com MSI)
Documento de referência para integrações ERP ↔ Magento 2 em lojas que operam com Multi-Source Inventory (MSI) — múltiplos depósitos/fontes, com estoque vendável calculado por stock MSI. Cobre tanto os endpoints customizados de leitura dos módulos MageShop_PriceApi e MageShop_StockApi quanto os endpoints nativos de escrita do Magento.
1. Contexto
Em integrações ERP ↔ Magento, é comum observar volume excessivo de requisições nos endpoints de atualização de estoque e preços. Quando isso ocorre, a aplicação sofre com:
- Concorrência elevada no PHP-FPM e contenção em locks de tabela.
- Reindexação MSI acionada repetidamente.
- Aumento de timeouts/5xx e degradação para o cliente final da loja.
Este documento orienta o time de integração sobre:
- Como ler preços e estoque em massa de forma eficiente (módulos MageShop).
- Como escrever preços e estoque MSI com o menor custo possível (Magento core).
- Boas práticas de paralelismo, delta e retry.
ℹ️ As orientações assumem que a loja utiliza MSI (módulo
Magento_Inventoryhabilitado). Endpoints específicos de estoque legacy (sem MSI) são mencionados como alternativa, mas não são o caminho recomendado.
2. Convenções e Autenticação
2.1. Convenções
- Base URL:
https://<dominio>/rest/<store_code>—store_codeé o código da loja (ex.:default). Operações globais podem ser chamadas em/rest/V1/.... - Verbos:
GETpara leitura (módulos MageShop),POSTpara escrita (Magento nativo). - Content-Type:
application/jsonem request e response, UTF-8. - Numéricos retornados como string — comportamento padrão do Magento. O cliente deve converter (
parseFloat/Decimal) conforme necessário.
2.2. Autenticação
ACLs exigidas:
Magento_Catalog::productspara preços e leitura de estoque.Magento_InventoryApi::source_itemspara escrita de estoque MSI.
Token de admin (uso interno/scripts)
TOKEN=$(curl -s -X POST https://loja.com/rest/V1/integration/admin/token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"senha"}' | tr -d '"')
curl -H "Authorization: Bearer $TOKEN" https://loja.com/rest/V1/product-prices
Token de integration (recomendado para ERPs/terceiros)
- Admin → System → Extensions → Integrations → Add New Integration.
- Em API → Resource Access selecione
Catalog → Inventory → Products. - Salve, ative e copie o Access Token gerado.
curl -H "Authorization: Bearer <access_token>" https://loja.com/rest/V1/product-prices
Tokens de integration não expiram automaticamente — revogue pelo admin quando deixar de usar.
3. Leitura para conferência (módulos MageShop)
Os módulos MageShop_PriceApi e MageShop_StockApi expõem endpoints customizados de extração em massa. São otimizados para conferência e reconciliação de catálogo, sem o custo do endpoint genérico /V1/products.
3.1. Paginação
Paginação é opt-in. Se page_size não for enviado, a resposta traz todos os registros (legado).
| Parâmetro | Tipo | Default | Máximo (price/legacy) | Máximo (msi) |
|---|---|---|---|---|
page_size |
int | — | 10.000 | 5.000 |
current_page |
int | 1 | — | — |
- Valores acima do máximo são clampeados silenciosamente.
- A resposta é sempre um array JSON; não há
total_count. Para detectar fim das páginas, itere até receber[].
Tamanhos recomendados por endpoint
| Endpoint | page_size sugerido |
Por quê |
|---|---|---|
/V1/product-prices |
1000 | Linha pequena (4 campos). |
/V1/product-stocks/legacy |
1000 | Linha pequena. |
/V1/product-stocks/msi |
500 | Linha carrega array sources[] aninhado, payload 3–10× maior. |
3.2. GET /V1/product-prices (MageShop_PriceApi)
Lista preços base (price) de produtos simples, expandidos por store_id.
Parâmetros (query string)
| Nome | Tipo | Descrição |
|---|---|---|
website_id |
int | Restringe ao website informado. |
sku_like |
string | Filtro LIKE SQL no SKU. Use % como wildcard (URL-encode → %25). |
page_size |
int | Ativa paginação. Máx. 10.000. |
current_page |
int | Página (1-based). Só usado com page_size. |
Exemplo
curl -H "Authorization: Bearer $TOKEN" \
'https://loja.com/rest/V1/product-prices?website_id=1&sku_like=ABC-%25&page_size=1000¤t_page=1'
Resposta
[
{ "sku": "ABC-001", "store_id": "1", "attribute_code": "price", "price": "199.9000" },
{ "sku": "ABC-001", "store_id": "2", "attribute_code": "price", "price": "189.9000" }
]
Notas
priceaplicaCOALESCE(override_da_loja, default)— se a loja tem preço sobrescrito, vem o da loja; caso contrário, o global.- Não inclui
special_price,tier_priceoucost. Para promoções, ver seção 5.3. - Ordenação:
skuASC,attribute_codeASC,store_idASC.
3.3. GET /V1/product-stocks/msi (MageShop_StockApi)
Lista estoque MSI, expandindo cada stock_id pelas stores do website e detalhando sources. Este é o endpoint recomendado para lojas com MSI.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
website_id |
int | Restringe ao website. |
stock_id |
int | Restringe a um stock MSI específico. |
sku_like |
string | Filtro LIKE no SKU. |
page_size |
int | Ativa paginação. Máx. 5.000. |
current_page |
int | Página (1-based). |
Exemplo
curl -H "Authorization: Bearer $TOKEN" \
'https://loja.com/rest/V1/product-stocks/msi?stock_id=2&page_size=500'
Resposta
[
{
"sku": "ABC-001",
"store_id": "1",
"website_id": "1",
"stock_id": "2",
"salable_qty": "10.0000",
"is_salable": "1",
"sources": [
{ "source_code": "default", "qty": 5, "status": 1 },
{ "source_code": "warehouse_sp", "qty": 5, "status": 1 }
]
}
]
Notas
salable_qtyvem da view MSIinventory_stock_{stock_id}— é a quantidade efetivamente vendável (considera reservations).sources[]é o detalhamento por source (depósito físico):qtyé a quantidade bruta da fonte;status:1em estoque,0fora.- Se o módulo
Magento_Inventoryestiver desabilitado: HTTP 400 com"MSI is not enabled on this instance." - Se a view
inventory_stock_{N}não existir (indexer nunca executado): HTTP 400 com instrução para rodarbin/magento indexer:reindex inventory. - Ordenação:
skuASC,store_idASC.
3.4. GET /V1/product-stocks/legacy (alternativa)
Lê de cataloginventory_stock_status — útil para reconciliação rápida sem o detalhamento de sources[].
curl -H "Authorization: Bearer $TOKEN" \
'https://loja.com/rest/V1/product-stocks/legacy?website_id=1&page_size=1000'
Resposta
[
{
"sku": "ABC-001",
"store_id": "1",
"website_id": "1",
"stock_id": "1",
"salable_qty": "12.0000",
"is_salable": "1"
}
]
ℹ️ Mesmo em lojas com MSI, este endpoint funciona porque o Magento mantém
cataloginventory_stock_statussincronizado. Não substitui o endpoint MSI: não trazsources[]e ostock_idé sempre1(formato legacy). Use só para conferência simplificada.
4. Escrita — Estoque MSI
4.1. Endpoint
POST /rest/V1/inventory/source-items
Content-Type: application/json
Authorization: Bearer {token}
Atualiza quantidades e status de SKUs por source (depósito) em lote.
ACL: Magento_InventoryApi::source_items
Payload
{
"sourceItems": [
{ "sku": "ABC-001", "source_code": "default", "quantity": 10, "status": 1 },
{ "sku": "ABC-001", "source_code": "warehouse_sp", "quantity": 5, "status": 1 },
{ "sku": "ABC-002", "source_code": "default", "quantity": 0, "status": 0 }
]
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sku |
string | sim | SKU do produto. |
source_code |
string | sim | Código do source (depósito). Deve existir em inventory_source. |
quantity |
decimal | sim | Quantidade disponível na fonte. |
status |
int | sim | 1 = em estoque, 0 = esgotado. |
Resposta
- Sucesso total:
[] - Falha parcial:
[
{ "message": "Source \"%1\" does not exist.", "parameters": ["deposito_xx"] }
]
4.2. Recomendações
💡 O endpoint aceita batch real. A otimização vem de consolidar SKUs por chamada + delta + paralelismo controlado.
-
Lotes consolidados — 100 a 200 itens por requisição
- Cada par
sku + source_codeconta como um item no arraysourceItems. - Um SKU em 3 sources consome 3 posições do array. Considere isso ao dimensionar o lote.
- Sweet spot: 100–200 itens. Lotes muito grandes (>500) começam a atingir timeouts de proxy/PHP.
- Cada par
-
Enviar apenas o delta
- Atualizar somente combinações
sku + source_codecuja quantidade ou status mudou. - Manter no ERP um snapshot da última carga enviada e comparar antes de disparar.
- Isoladamente, esta é a otimização de maior impacto.
- Atualizar somente combinações
-
Paralelismo controlado
- Máximo 1 a 2 requisições simultâneas ao endpoint.
- O Magento serializa internamente operações sobre as mesmas linhas — paralelismo maior gera contenção em locks.
-
Intervalo entre lotes
- 200–500 ms entre chamadas sequenciais para evitar acúmulo na fila do PHP-FPM.
-
Retry com backoff exponencial
- Em
5xx/timeout, não retentar imediatamente. - Backoff sugerido: 1s → 2s → 4s → 8s, máximo 3–4 tentativas.
- Em
-
Janela de sincronização
- Preferir sincronização incremental contínua (delta a cada N minutos) em vez de cargas massivas em horários cheios.
- Cargas full (reconciliação diária) → agendar fora dos horários de pico.
4.3. Notas operacionais sobre MSI
salable_qtyé assíncrono —salable_qtyconsultado via/V1/product-stocks/msisó reflete a mudança após o indexer MSI rodar. Geralmente isso é automático em segundos. Para forçar:bin/magento indexer:reindex inventory.quantity = 0não remove o source item — apenas zera a quantidade. Para marcar como indisponível, usestatus = 0. Para remover de fato, há um endpoint específico (/V1/inventory/source-items-delete), raramente necessário.- Sempre inspecione a resposta — HTTP 200 pode conter falhas parciais por item (seção 6.2).
5. Escrita — Preços
5.1. Preço base — POST /V1/products/base-prices
⚠️ Não usar
PUT /V1/products/{sku}apenas para atualizar preço. Esse endpoint processa reindexação, EAV, mídia e categorias — gera carga desnecessária.
Use o endpoint dedicado, que aceita lote real e é independente de MSI:
POST /rest/V1/products/base-prices
Content-Type: application/json
Authorization: Bearer {token}
Payload
{
"prices": [
{ "price": 27.34, "store_id": 0, "sku": "123" },
{ "price": 49.90, "store_id": 0, "sku": "124" },
{ "price": 99.00, "store_id": 0, "sku": "125" }
]
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sku |
string | sim | SKU do produto. |
price |
decimal | sim | Novo preço base. |
store_id |
int | sim | 0 = global (default), >0 = override por loja. |
Sobre store_id em lojas com uma única store view
Use sempre store_id = 0 — representa o escopo global e aplica o preço a todas as views. Não é necessário replicar a linha para store_id 1, 2 etc.; isso apenas multiplica o tamanho do payload.
Boas práticas
- Lotes: 100 a 200 SKUs por requisição.
- Delta apenas: enviar somente os SKUs cujo preço mudou.
- Paralelismo: máximo 1 a 2 requisições simultâneas.
- Intervalo entre lotes: 200–500 ms entre chamadas sequenciais.
- Retry com backoff exponencial em
5xx/timeout. - Uma entrada por par
sku + store_idno mesmo payload — múltiplas linhas para a mesma chave geram comportamento "última ganha" não garantido.
5.2. Preço promocional — POST /V1/products/special-price
Cria ou atualiza special_price com janela de vigência opcional.
Payload
{
"prices": [
{
"sku": "411",
"price": 13,
"store_id": 0,
"price_from": "2025-11-19 00:00:00",
"price_to": ""
}
]
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sku |
string | sim | SKU. |
price |
decimal | sim | Valor da promoção. |
store_id |
int | sim | 0 = global. |
price_from |
string | recomendado | Início (YYYY-MM-DD HH:MM:SS). |
price_to |
string | opcional | Fim. "" ou omitido = promoção sem data fim. |
Notas
- O
special_pricesó é exibido seprice_from <= now <= price_to(quandoprice_toestá definido). - O Magento não valida que
special_price < pricebase; a vitrine simplesmente exibe o menor. - Evite enviar
nullparaprice_tona criação — alguns clientes serializam como string"null". Use""ou omita.
5.3. Consultar preços promocionais — POST /V1/products/special-price-information
Apesar do verbo POST, é leitura — o POST é exigido porque o payload tem array.
{ "skus": ["411"] }
Resposta
[
{
"price": 13,
"store_id": 0,
"sku": "411",
"price_from": "2025-11-19 00:00:00",
"price_to": null
}
]
- Array vazio
[]= nenhum SKU consultado tem promoção configurada. - Um SKU pode aparecer múltiplas vezes (uma por
store_idcom promoção distinta).
5.4. Remover preços promocionais — POST /V1/products/special-price-delete
A correspondência é feita por sku + store_id + price + price_from + price_to — todos precisam bater com o registro existente, caso contrário a remoção é silenciosamente ignorada.
Payload
{
"prices": [
{
"sku": "411",
"price": 13,
"store_id": 0,
"price_from": "2025-11-19 00:00:00",
"price_to": null
}
]
}
💡 Fluxo recomendado: chamar
special-price-informationprimeiro para obter os campos exatos, depois passar o objeto retornado como payload despecial-price-delete. É idempotente — não erra se o registro não existir.
6. Erros e códigos HTTP
6.1. Códigos
| Código | Quando | Corpo (resumido) |
|---|---|---|
| 200 | Sucesso (mesmo se array vazio). | [] ou [ {...}, ... ] |
| 401 | Token ausente ou inválido. | { "message": "Consumer is not authorized..." } |
| 403 | Token sem a ACL exigida. | { "message": "..." } |
| 400 | MSI desabilitado, view ausente, payload inválido. | { "message": "..." } |
| 500 | Erro inesperado. | { "message": "..." } |
6.2. Erro global
{ "message": "MSI is not enabled on this instance." }
6.3. Erros parciais (endpoints de escrita em lote)
Endpoints de escrita (source-items, base-prices, special-price etc.) retornam HTTP 200 mesmo quando alguns itens falham. O resultado é um array por item:
[
{
"message": "Invalid attribute value. Row ID: SKU = %1, Store ID: %2",
"parameters": ["411", "99"]
},
{
"message": "Source \"%1\" does not exist.",
"parameters": ["deposito_xx"]
}
]
- Array vazio
[]→ todos os itens foram aceitos. - Array com objetos → cada objeto descreve um item que falhou.
- Sempre inspecione o tamanho do array nos endpoints de escrita — HTTP 200 não significa sucesso total.
7. Receitas práticas
7.1. Extração completa paginada (Python)
import requests
TOKEN = "..."
BASE = "https://loja.com/rest/V1/product-prices"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
page = 1
out = []
while True:
r = requests.get(BASE, headers=HEADERS,
params={"page_size": 1000, "current_page": page})
r.raise_for_status()
chunk = r.json()
if not chunk:
break
out.extend(chunk)
page += 1
print(f"Total: {len(out)} registros")
7.2. Atualização em massa de estoque MSI (Python)
import requests, csv
TOKEN = "..."
URL = "https://loja.com/rest/default/V1/inventory/source-items"
HDRS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
# Carrega CSV: sku,source_code,quantity,status
items = []
with open("estoque.csv") as f:
for row in csv.DictReader(f):
items.append({
"sku": row["sku"],
"source_code": row["source_code"],
"quantity": float(row["quantity"]),
"status": int(row["status"]),
})
# Envia em lotes de 200
for i in range(0, len(items), 200):
batch = items[i:i+200]
r = requests.post(URL, headers=HDRS, json={"sourceItems": batch}, timeout=120)
r.raise_for_status()
errors = r.json()
if errors:
print(f"Lote {i}: {len(errors)} erros")
for e in errors:
print(" -", e["message"], e.get("parameters"))
7.3. Sync incremental por prefixo de SKU (Bash)
for prefix in A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9; do
curl -s -H "Authorization: Bearer $TOKEN" \
"https://loja.com/rest/V1/product-stocks/msi?sku_like=${prefix}%25" \
> "stock_msi_${prefix}.json"
done
7.4. Atualização de preço base seguindo extração
# 1) Extrai preços atuais
curl -s -H "Authorization: Bearer $TOKEN" \
'https://loja.com/rest/V1/product-prices?sku_like=ABC-%25' > current.json
# 2) Gera payload com aumento de 10% (jq)
jq '{ prices: [ .[] | { sku, store_id: (.store_id|tonumber), price: ((.price|tonumber) * 1.10) } ] }' \
current.json > new_prices.json
# 3) Aplica
curl -X POST 'https://loja.com/rest/V1/products/base-prices' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
--data @new_prices.json
7.5. Remover promoção a partir da consulta
# 1) Consulta promoções ativas
INFO=$(curl -s -X POST 'https://loja.com/rest/V1/products/special-price-information' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
--data '{ "skus": ["411"] }')
# 2) Usa o próprio retorno como payload de delete
curl -X POST 'https://loja.com/rest/V1/products/special-price-delete' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
--data "{ \"prices\": $INFO }"
7.6. Comparar estoque legacy vs MSI no mesmo SKU
Útil em auditoria para detectar divergências entre as duas views.
SKU="ABC-001"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://loja.com/rest/V1/product-stocks/legacy?sku_like=${SKU}" > legacy.json
curl -s -H "Authorization: Bearer $TOKEN" \
"https://loja.com/rest/V1/product-stocks/msi?sku_like=${SKU}" > msi.json
diff <(jq '.[] | {sku,store_id,salable_qty}' legacy.json) \
<(jq '.[] | {sku,store_id,salable_qty}' msi.json)
8. Resumo das recomendações
| Item | Forma ineficiente | Forma recomendada |
|---|---|---|
| Leitura de preços em massa | GET /V1/products (pesado) |
GET /V1/product-prices (MageShop) |
| Leitura de estoque em massa | GET /V1/products ou /V1/stockItems/{sku} por SKU |
GET /V1/product-stocks/msi (MageShop) |
| Endpoint de escrita de estoque | PUT /V1/products/{sku}/stockItems/{itemId} (legacy, sem batch) |
POST /V1/inventory/source-items (MSI, em lote) |
| Endpoint de escrita de preço | PUT /V1/products/{sku} |
POST /V1/products/base-prices (lote) |
store_id em preço |
Repetido em várias views | Somente store_id = 0 (loja com store view única) |
| SKUs enviados | Todos, sempre | Apenas os que mudaram (delta) |
| Lotes (estoque e preço) | Várias requisições, poucos itens | 100 a 200 itens por requisição |
| Paralelismo (estoque) | Não controlado | Máx. 1–2 requisições simultâneas |
| Paralelismo (preços) | Não controlado | Máx. 1–2 requisições simultâneas |
| Intervalo entre lotes | Sem controle | 200–500 ms |
| Retry | Imediato | Backoff exponencial (1s→2s→4s→8s) |
9. Boas práticas e resultado esperado
Leitura
- Sempre pagine em produção — catálogos com 50k+ SKUs × N stores × M sources podem estourar memória do PHP-FPM e timeout de proxy. O endpoint MSI é especialmente sensível por causa do array
sources[]. - Use filtros agressivamente (
website_id,stock_id,sku_like) para reduzir o custo SQL no banco. O MSI fazUNION ALLpor stock — filtrar é importante. - URL-encode wildcards —
%precisa virar%25na URL (ABC-%25≠ABC-%). - Evite polling intenso — endpoints são read-only, mas executam SQL pesado.
Escrita
- Inspecione sempre a resposta — HTTP 200 pode conter falhas parciais (seção 6.3).
- Uma entrada por chave única no payload:
sku + source_codeem estoque,sku + store_idem preços. - Reindex após escritas em massa — após carga grande em
source-items, osalable_qtysó reflete depois do indexer MSI rodar (geralmente automático). Em produção pode levar segundos. - Token de integration, não de admin, para clientes externos persistentes.
- Idempotência — reenviar o mesmo payload não duplica registros.
special-price-deleteé totalmente idempotente.
Resultado esperado da adoção das recomendações
- Redução significativa do número total de requisições por minuto (via batch + delta).
- Menor consumo de CPU e banco de dados na aplicação Magento.
- Menor risco de timeouts, bloqueios e degradação para o cliente final.
- Sincronizações de catálogo mais rápidas e previsíveis.
- Reconciliação ERP ↔ Magento simplificada pelos endpoints de leitura em massa.
Em caso de dúvidas sobre a implementação ou sobre limites adequados de paralelismo para o ambiente, fica à disposição o time de integração.