Orientações de Integração — API Magento 2
Leitura e atualização de Preços e Estoque (lojas com estoque único / single stock)
Documento de referência para integrações ERP ↔ Magento 2 em lojas que operam com estoque único (sem multi-source de fato), mas que tipicamente rodam em Magento 2.3+ — versão em que o módulo Magento_Inventory (MSI) vem habilitado por padrão com um source default que representa o estoque legacy.
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 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 com o menor custo possível (Magento core).
- Boas práticas de paralelismo, delta e retry.
ℹ️ As orientações assumem loja com estoque único (sem multi-source configurado). Para escrita de estoque, recomenda-se o endpoint nativo de MSI fixando
source_code = "default"— isso dá acesso a batch real sem precisar configurar nada adicional, já quedefaulté o source criado automaticamente pelo Magento 2.3+. Um caminho alternativo via endpoint legacy é mantido como fallback para ambientes onde o módulo MSI esteja completamente desabilitado.
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),POST/PUTpara 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 viainventory/source-items.
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 |
|---|---|---|---|
page_size |
int | — | 10.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[]. - Page size recomendado:
1000para os dois endpoints abaixo.
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 consultar promoções, ver seção 5.3. - Ordenação:
skuASC,attribute_codeASC,store_idASC.
3.3. GET /V1/product-stocks/legacy (MageShop_StockApi)
Lista estoque, lendo de cataloginventory_stock_status. Em lojas com estoque único, este endpoint é suficiente para conferência — não há sources[] aninhado para apresentar.
Parâmetros
| Nome | Tipo | Descrição |
|---|---|---|
website_id |
int | Restringe ao website. |
sku_like |
string | Filtro LIKE no SKU. |
page_size |
int | Ativa paginação. Máx. 10.000. |
current_page |
int | Página (1-based). |
Exemplo
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"
}
]
Notas
stock_idé sempre1neste modelo.is_salable:1(disponível) ou0(esgotado).salable_qtyvem como string — converter no consumidor.
4. Escrita — Estoque
4.1. Endpoint principal — POST /V1/inventory/source-items
Em Magento 2.3+, o módulo Magento_Inventory vem habilitado por padrão com um source chamado default que representa o estoque único da loja. Isso permite usar o endpoint nativo de inventory com batch real, mesmo em lojas que não operam com múltiplos depósitos — basta fixar source_code: "default" em todas as entradas.
POST /rest/V1/inventory/source-items
Content-Type: application/json
Authorization: Bearer {token}
ACL: Magento_InventoryApi::source_items
Payload
{
"sourceItems": [
{ "sku": "ABC-001", "source_code": "default", "quantity": 10, "status": 1 },
{ "sku": "ABC-002", "source_code": "default", "quantity": 0, "status": 0 },
{ "sku": "ABC-003", "source_code": "default", "quantity": 5, "status": 1 }
]
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sku |
string | sim | SKU do produto. |
source_code |
string | sim | Fixar em "default" — único source existente em lojas com estoque único. |
quantity |
decimal | sim | Quantidade disponível. |
status |
int | sim | 1 = em estoque, 0 = esgotado. |
✅ Em lojas com estoque único, sempre use
source_code: "default". Usar qualquer outro código retornará erro por item (Source "%1" does not exist.).
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 SKUs por requisição
- Em estoque único, cada SKU consome apenas uma posição do array (não há múltiplos sources).
- Sweet spot: 100–200 itens. Lotes muito grandes (>500) começam a atingir timeouts de proxy/PHP.
-
Enviar apenas o delta
- Atualizar somente SKUs cuja quantidade ou status mudou desde a última sincronização.
- Manter no ERP um snapshot da última carga enviada e comparar antes de disparar.
- Isoladamente, esta é a otimização de maior impacto: tipicamente reduz o volume em 80–95%.
-
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
salable_qtyé assíncrono —salable_qtyconsultado via/V1/product-stocks/legacysó reflete a mudança após o indexer 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.- Sempre inspecione a resposta — HTTP 200 pode conter falhas parciais por item (seção 6.3).
4.4. Fallback — endpoint legacy PUT /V1/products/{sku}/stockItems/{itemId}
Use apenas se o módulo Magento_Inventory estiver completamente desabilitado no ambiente — cenário raro em Magento 2.3+, mas possível em instalações customizadas.
Sinal para acionar este caminho: chamadas a /V1/inventory/source-items retornarem HTTP 400 com:
{ "message": "MSI is not enabled on this instance." }
Endpoint e payload
PUT /rest/V1/products/{sku}/stockItems/{itemId}
{
"stockItem": {
"qty": 25,
"is_in_stock": true
}
}
⚠️ Este endpoint não aceita batch — cada requisição atualiza um único SKU. As otimizações neste fallback são focadas em reduzir o volume total de requisições (delta + paralelismo controlado).
Obtenção do itemId (stock_item_id)
itemId é o stock_item_id, atribuído pelo Magento na criação do produto. Não é igual ao ID do produto.
GET /rest/V1/stockItems/{sku}
- Cachear o mapeamento
sku → stock_item_idno lado do ERP. - Refazer a busca apenas em caso de erro
404ou quando um SKU novo for sincronizado pela primeira vez.
Recomendações neste fallback
- Delta apenas (mesmo princípio do caminho principal).
- Paralelismo controlado — máximo 2 a 4 requisições simultâneas (pode ser maior que no batch porque cada requisição toca um único SKU).
- Retry com backoff exponencial — 1s → 2s → 4s → 8s.
- Janela de sincronização — incremental contínua, evitar cargas massivas em horários de pico.
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:
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 | Payload inválido ou pré-requisito não atendido (ex.: MSI desabilitado). | { "message": "..." } |
| 500 | Erro inesperado. | { "message": "..." } |
6.2. Erro global — MSI desabilitado
Indica que source-items não pode ser usado neste ambiente. Acionar o fallback descrito em 4.4.
{ "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 (Python)
Lê CSV com sku,quantity,status e envia em lotes de 200 com source_code fixo em default.
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,quantity,status
items = []
with open("estoque.csv") as f:
for row in csv.DictReader(f):
items.append({
"sku": row["sku"],
"source_code": "default",
"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/legacy?sku_like=${prefix}%25" \
> "stock_legacy_${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 }"
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/legacy (MageShop) |
| Endpoint de escrita de estoque | PUT /V1/products/{sku}/stockItems/{itemId} (per-SKU) |
POST /V1/inventory/source-items com source_code: "default" (lote) |
| Endpoint de escrita de preço | PUT /V1/products/{sku} |
POST /V1/products/base-prices (lote) |
source_code em estoque |
— | Fixo em "default" |
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 batch) | Não controlado | Máx. 1–2 requisições simultâneas |
| Paralelismo (estoque fallback per-SKU) | Não controlado | Máx. 2–4 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 podem estourar memória do PHP-FPM e timeout de proxy.
- Use filtros agressivamente (
website_id,sku_like) para reduzir o custo SQL no banco. - 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 (source_codesempre"default"),sku + store_idem preços. - Reindex após escritas em massa — após carga grande em
source-items, osalable_qtysó reflete depois do indexer rodar (geralmente automático). - 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.