Repair Inventory (MSI)
Out of stock threshold change not applied to existing items
Someone lowers the Out-of-Stock Threshold to sell down the last few units of everything, or raises it to stop selling on razor thin stock. The setting saves without an error. But a pile of products that should have flipped in stock or out of stock never move, the admin grid disagrees with what the storefront shows, and the MSI salable quantity API tells a third story entirely. The threshold changed. The existing inventory did not catch up. Here is why, and a small script that finds and repairs the exact source items the change left behind.
Saving a new value for cataloginventory/options/stock_threshold_qty under Stores, Configuration, Catalog, Inventory, Product Stock Options fires the admin_system_config_changed_section_cataloginventory observer, which correctly recalculates the legacy cataloginventory_stock_item.is_in_stock flag for every product. MSI never got the matching piece. There is no observer or plugin that recomputes the status column on existing inventory_source_item rows against the new threshold, so those rows keep whichever status value the old threshold produced. This is a confirmed defect, tracked as magento/inventory issue #3061, and it shows up as the admin grid, the storefront, and the MSI salable quantity API all disagreeing on the same SKU until something forces a recompute. Run a small Python or Node.js script that reads the current threshold, pages through your catalog, reads every source item's quantity and stored status, recomputes what that status should be, and reports or repairs the ones that fell out of sync. Full code, tests, and a dry run guard are below.
The problem in plain words
The Out-of-Stock Threshold is one of the oldest settings in Magento's inventory model. It says how many units of buffer stock to hold back, so a product goes out of stock a little before it truly hits zero. Under classic single-source inventory, that setting only ever touched one table, and changing it and saving the config was enough, since the legacy observer recalculates is_in_stock for every stock item right then.
MSI split that single table into inventory_source_item rows, one per source per SKU, each carrying its own status column that is supposed to mean the same thing is_in_stock used to mean. The legacy observer still runs and still updates the old table correctly. Nothing was ever wired up to walk the MSI source item table and do the same recompute. So the moment you save a new threshold, the legacy flag and the MSI source item status start telling two different stories about the same product, and they stay that way until quantity changes trigger a fresh calculation on their own, or a full reindex and cron pass happens to touch them.
Why it happens
- The config save handler wires
admin_system_config_changed_section_cataloginventoryonly to the legacy stock item recalculation, a holdover from before MSI existed, and no equivalent hook was added forinventory_source_itemwhen MSI shipped. - MSI treats
statuson a source item as something recomputed when quantity changes at that source, not something tied to a global config value, so a config-only change has no code path that revisits existing rows. - A full reindex of
cataloginventory_stockplus a cron pass can eventually catch some of the drift, but nothing forces that to happen the moment the threshold is saved, so the gap can persist for a long time on a quiet catalog. - The admin grid, the storefront, and MSI's own salable quantity and reservation APIs each read from a different piece of this split state, so the same SKU can look in stock in one place and out of stock in another until every source item is corrected.
This is a confirmed, reported defect rather than a guess. See the citations at the end for the exact GitHub issue and the MSI FAQ describing the split between legacy stock status and source item status.
A threshold change is a global rule. Applying it to existing rows is a separate, missing step. So the fix is not to wait for the next reindex to maybe catch it. It is to recompute, in one pure function, what every existing source item's status should be under the current threshold and backorders setting, compare that against what is actually stored, and only then decide whether a given source item is stale. Quantity is never touched, only the status field the threshold change was supposed to update in the first place.
The fix, as a flow
We do not touch quantity anywhere in this script. We add a job that reads the current threshold, pages through the catalog, reads every SKU's source items, recomputes the status each one should have, and writes back only the ones that disagree with what MSI is currently storing.
Build it step by step
Get an admin bearer token and the current threshold
Authenticate the way any Magento REST client does, either POST /rest/V1/integration/admin/token with an admin username and password, or an integration token you already have. There is no public REST getter for cataloginventory/options/stock_threshold_qty, so read the current threshold from the admin config UI or database once and pass it in as a script input.
pip install requests
export MAGENTO_URL="https://your-store.example.com"
export MAGENTO_ADMIN_TOKEN="eyJraWQ..."
export STOCK_THRESHOLD_QTY="5"
export BACKORDERS_ENABLED="false"
export PAGE_SIZE="100"
export DRY_RUN="true" # start safe, change to false to write corrected status
// Node 18+ has fetch built in, no dependencies needed
export MAGENTO_URL="https://your-store.example.com"
export MAGENTO_ADMIN_TOKEN="eyJraWQ..."
export STOCK_THRESHOLD_QTY="5"
export BACKORDERS_ENABLED="false"
export PAGE_SIZE="100"
export DRY_RUN="true" // start safe, change to false to write corrected status
Talk to the Magento REST API
Every call sends Authorization: Bearer <token> to a /rest/V1 route. A small helper wraps GET and PUT and raises on a non 200 response, since Magento returns structured error bodies worth surfacing as is.
import os, requests
MAGENTO_URL = os.environ["MAGENTO_URL"].rstrip("/")
TOKEN = os.environ["MAGENTO_ADMIN_TOKEN"]
def magento_get(path, params=None):
r = requests.get(
f"{MAGENTO_URL}/rest/V1{path}",
params=params or {},
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
r.raise_for_status()
return r.json()
def magento_put(path, body):
r = requests.put(
f"{MAGENTO_URL}/rest/V1{path}",
json=body,
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
timeout=30,
)
r.raise_for_status()
return r.json()
const MAGENTO_URL = (process.env.MAGENTO_URL || "").replace(/\/$/, "");
const TOKEN = process.env.MAGENTO_ADMIN_TOKEN;
async function magentoGet(path, params = {}) {
const url = new URL(`${MAGENTO_URL}/rest/V1${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!res.ok) throw new Error(`Magento ${res.status}`);
return res.json();
}
async function magentoPut(path, body) {
const res = await fetch(`${MAGENTO_URL}/rest/V1${path}`, {
method: "PUT",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`Magento ${res.status}`);
return res.json();
}
List SKUs and their source items
Page through GET /rest/V1/products with searchCriteria[pageSize] and searchCriteria[currentPage]. For each SKU, call GET /rest/V1/inventory/source-items filtered by sku equal to that value to get every source item's sku, source_code, quantity, and stored status.
def products_page(page_size, current_page):
params = {
"searchCriteria[pageSize]": page_size,
"searchCriteria[currentPage]": current_page,
}
return magento_get("/products", params)["items"]
def source_items_for_sku(sku):
params = {
"searchCriteria[filterGroups][0][filters][0][field]": "sku",
"searchCriteria[filterGroups][0][filters][0][value]": sku,
"searchCriteria[filterGroups][0][filters][0][conditionType]": "eq",
}
return magento_get("/inventory/source-items", params)["items"]
async function productsPage(pageSize, currentPage) {
const params = {
"searchCriteria[pageSize]": pageSize,
"searchCriteria[currentPage]": currentPage,
};
const data = await magentoGet("/products", params);
return data.items;
}
async function sourceItemsForSku(sku) {
const params = {
"searchCriteria[filterGroups][0][filters][0][field]": "sku",
"searchCriteria[filterGroups][0][filters][0][value]": sku,
"searchCriteria[filterGroups][0][filters][0][conditionType]": "eq",
};
const data = await magentoGet("/inventory/source-items", params);
return data.items;
}
Cross-check with live salable quantity
Optionally call GET /rest/V1/inventory/get-product-salable-quantity/{sku}/{stockId}, default stockId 1, to see the salable quantity MSI is currently exposing for the SKU. It is a useful sanity check alongside the recomputed status, since a SKU with real salable stock but a stale zero status is exactly the case worth fixing.
def salable_quantity(sku, stock_id=1):
return magento_get(f"/inventory/get-product-salable-quantity/{sku}/{stock_id}")
async function salableQuantity(sku, stockId = 1) {
return magentoGet(`/inventory/get-product-salable-quantity/${sku}/${stockId}`);
}
Decide, with one pure function
Keep the recompute in its own function that takes quantity, the current threshold, and whether backorders are enabled, and returns 0 or 1. A pure function like this is easy to read and easy to test, which we do later. Salable quantity is quantity minus threshold, and status flips to out of stock at salable quantity zero or below, except when backorders are enabled and the threshold is zero or negative, since that combination means infinite or threshold-extended backorders keep the item salable regardless of quantity.
def recompute_source_item_status(quantity, threshold, backorders_enabled):
if backorders_enabled and threshold <= 0:
return 1
salable = quantity - threshold
return 1 if salable > 0 else 0
export function recomputeSourceItemStatus(quantity, threshold, backordersEnabled) {
if (backordersEnabled && threshold <= 0) return 1;
const salable = quantity - threshold;
return salable > 0 ? 1 : 0;
}
Repair only the status field, with a dry run guard
For each source item whose stored status disagrees with the recomputed value, send PUT /rest/V1/inventory/source-items with a body that repeats the item's sku, source_code, and existing quantity, and overwrites only status with the corrected value. Guard every write behind DRY_RUN. When it is true, the default, only log the diff of sku, source_code, old status, new status, quantity, and threshold, and skip the write entirely. When it is false, perform the PUT, then log that bin/magento indexer:reindex cataloginventory_stock plus bin/magento cron:run should follow, since those are CLI-only and out of REST's reach, and they are what makes cataloginventory_stock_item and the salable quantity index reflect the corrected source items.
Always start with DRY_RUN=true. The write path only ever changes the status field on source items the script itself recomputed as stale, quantity is never part of the payload, and it never invents a source item that was not already there. Follow every real run with a CLI reindex and cron pass, since that is the only way the legacy stock item and salable quantity index pick up the correction.
The full code
Here is the complete script in one file for each language. It reads settings from the environment, pages through the catalog, recomputes status for every source item, respects the dry run flag, and is safe to run again and again because it only ever overwrites status on source items it confirmed are stale.
"""Repair Magento 2 or Adobe Commerce inventory_source_item rows left stale
after an Out-of-Stock Threshold change.
Saving a new cataloginventory/options/stock_threshold_qty value fires
admin_system_config_changed_section_cataloginventory, which correctly
recalculates the legacy cataloginventory_stock_item.is_in_stock flag. MSI has
no matching observer for inventory_source_item, so existing source items keep
whichever status value the old threshold produced until quantity changes on
its own or a full reindex and cron pass happen to touch them. This script
pages through the catalog, reads every source item's quantity and stored
status, recomputes the status each should have under the current threshold
and backorders setting, and by default only reports the mismatches. Only
under an explicit DRY_RUN=false operator override does it PUT the corrected
status. It never touches quantity. Run on a schedule after any threshold
change. Safe to run again and again.
"""
import os
import logging
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("repair_threshold_source_items")
MAGENTO_URL = os.environ.get("MAGENTO_URL", "https://demo.example.com").rstrip("/")
TOKEN = os.environ.get("MAGENTO_ADMIN_TOKEN", "token_dummy")
STOCK_THRESHOLD_QTY = float(os.environ.get("STOCK_THRESHOLD_QTY", "0"))
BACKORDERS_ENABLED = os.environ.get("BACKORDERS_ENABLED", "false").lower() == "true"
PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "100"))
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
def magento_get(path, params=None):
r = requests.get(
f"{MAGENTO_URL}/rest/V1{path}",
params=params or {},
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
r.raise_for_status()
return r.json()
def magento_put(path, body):
r = requests.put(
f"{MAGENTO_URL}/rest/V1{path}",
json=body,
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
timeout=30,
)
r.raise_for_status()
return r.json()
def recompute_source_item_status(quantity, threshold, backorders_enabled):
if backorders_enabled and threshold <= 0:
return 1
salable = quantity - threshold
return 1 if salable > 0 else 0
def products_page(page_size, current_page):
params = {
"searchCriteria[pageSize]": page_size,
"searchCriteria[currentPage]": current_page,
}
return magento_get("/products", params)["items"]
def all_products(page_size):
page = 1
while True:
items = products_page(page_size, page)
if not items:
return
for item in items:
yield item
if len(items) < page_size:
return
page += 1
def source_items_for_sku(sku):
params = {
"searchCriteria[filterGroups][0][filters][0][field]": "sku",
"searchCriteria[filterGroups][0][filters][0][value]": sku,
"searchCriteria[filterGroups][0][filters][0][conditionType]": "eq",
}
return magento_get("/inventory/source-items", params)["items"]
def repair_source_item(sku, source_code, quantity, new_status):
body = {"sourceItems": [{"sku": sku, "source_code": source_code, "quantity": quantity, "status": new_status}]}
return magento_put("/inventory/source-items", body)
def run():
fixed = 0
for product in all_products(PAGE_SIZE):
sku = product.get("sku")
for item in source_items_for_sku(sku):
source_code = item.get("source_code")
quantity = item.get("quantity", 0)
old_status = item.get("status")
new_status = recompute_source_item_status(quantity, STOCK_THRESHOLD_QTY, BACKORDERS_ENABLED)
if old_status == new_status:
continue
log.warning(
"Stale status: sku=%s source_code=%s quantity=%s threshold=%s old_status=%s new_status=%s. %s",
sku, source_code, quantity, STOCK_THRESHOLD_QTY, old_status, new_status,
"would repair" if DRY_RUN else "repairing",
)
if not DRY_RUN:
repair_source_item(sku, source_code, quantity, new_status)
fixed += 1
if not DRY_RUN and fixed:
log.info("Run bin/magento indexer:reindex cataloginventory_stock and bin/magento cron:run to reconcile the legacy stock item and salable quantity index.")
log.info("Done. %d source item(s) %s.", fixed, "to repair" if DRY_RUN else "repaired")
if __name__ == "__main__":
run()
/**
* Repair Magento 2 or Adobe Commerce inventory_source_item rows left stale
* after an Out-of-Stock Threshold change.
*
* Saving a new cataloginventory/options/stock_threshold_qty value fires
* admin_system_config_changed_section_cataloginventory, which correctly
* recalculates the legacy cataloginventory_stock_item.is_in_stock flag. MSI
* has no matching observer for inventory_source_item, so existing source
* items keep whichever status value the old threshold produced until
* quantity changes on its own or a full reindex and cron pass happen to
* touch them. This script pages through the catalog, reads every source
* item's quantity and stored status, recomputes the status each should have
* under the current threshold and backorders setting, and by default only
* reports the mismatches. Only under an explicit DRY_RUN=false operator
* override does it PUT the corrected status. It never touches quantity. Run
* on a schedule after any threshold change. Safe to run again and again.
*
* Guide: https://www.allanninal.dev/magento/threshold-change-not-applied-existing-items/
*/
import { pathToFileURL } from "node:url";
const MAGENTO_URL = (process.env.MAGENTO_URL || "https://demo.example.com").replace(/\/$/, "");
const TOKEN = process.env.MAGENTO_ADMIN_TOKEN || "token_dummy";
const STOCK_THRESHOLD_QTY = Number(process.env.STOCK_THRESHOLD_QTY || 0);
const BACKORDERS_ENABLED = (process.env.BACKORDERS_ENABLED || "false").toLowerCase() === "true";
const PAGE_SIZE = Number(process.env.PAGE_SIZE || 100);
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
export function recomputeSourceItemStatus(quantity, threshold, backordersEnabled) {
if (backordersEnabled && threshold <= 0) return 1;
const salable = quantity - threshold;
return salable > 0 ? 1 : 0;
}
async function magentoGet(path, params = {}) {
const url = new URL(`${MAGENTO_URL}/rest/V1${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!res.ok) throw new Error(`Magento ${res.status}`);
return res.json();
}
async function magentoPut(path, body) {
const res = await fetch(`${MAGENTO_URL}/rest/V1${path}`, {
method: "PUT",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`Magento ${res.status}`);
return res.json();
}
async function productsPage(pageSize, currentPage) {
const params = {
"searchCriteria[pageSize]": pageSize,
"searchCriteria[currentPage]": currentPage,
};
const data = await magentoGet("/products", params);
return data.items;
}
async function* allProducts(pageSize) {
let page = 1;
while (true) {
const items = await productsPage(pageSize, page);
if (!items.length) return;
for (const item of items) yield item;
if (items.length < pageSize) return;
page++;
}
}
async function sourceItemsForSku(sku) {
const params = {
"searchCriteria[filterGroups][0][filters][0][field]": "sku",
"searchCriteria[filterGroups][0][filters][0][value]": sku,
"searchCriteria[filterGroups][0][filters][0][conditionType]": "eq",
};
const data = await magentoGet("/inventory/source-items", params);
return data.items;
}
async function repairSourceItem(sku, sourceCode, quantity, newStatus) {
const body = { sourceItems: [{ sku, source_code: sourceCode, quantity, status: newStatus }] };
return magentoPut("/inventory/source-items", body);
}
export async function run() {
let fixed = 0;
for await (const product of allProducts(PAGE_SIZE)) {
const sku = product.sku;
for (const item of await sourceItemsForSku(sku)) {
const sourceCode = item.source_code;
const quantity = item.quantity || 0;
const oldStatus = item.status;
const newStatus = recomputeSourceItemStatus(quantity, STOCK_THRESHOLD_QTY, BACKORDERS_ENABLED);
if (oldStatus === newStatus) continue;
console.warn(
`Stale status: sku=${sku} source_code=${sourceCode} quantity=${quantity} threshold=${STOCK_THRESHOLD_QTY} old_status=${oldStatus} new_status=${newStatus}. ${
DRY_RUN ? "would repair" : "repairing"
}`
);
if (!DRY_RUN) await repairSourceItem(sku, sourceCode, quantity, newStatus);
fixed++;
}
}
if (!DRY_RUN && fixed) {
console.log("Run bin/magento indexer:reindex cataloginventory_stock and bin/magento cron:run to reconcile the legacy stock item and salable quantity index.");
}
console.log(`Done. ${fixed} source item(s) ${DRY_RUN ? "to repair" : "repaired"}.`);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((err) => { console.error(err); process.exit(1); });
}
Add a test
The recompute rule is the part most worth testing, because it decides which source items are treated as stale and what value they get repaired to. Because we kept recompute_source_item_status pure, the test needs no network, no Magento store, and no database. It just feeds in numbers and checks the answer, covering a positive threshold, a zero threshold with backorders, and a negative threshold with backorders.
from repair_threshold_source_items import recompute_source_item_status
def test_in_stock_when_quantity_above_positive_threshold():
assert recompute_source_item_status(10, 5, False) == 1
def test_out_of_stock_when_quantity_at_positive_threshold():
assert recompute_source_item_status(5, 5, False) == 0
def test_out_of_stock_when_quantity_below_positive_threshold():
assert recompute_source_item_status(2, 5, False) == 0
def test_out_of_stock_when_quantity_zero_and_threshold_zero_no_backorders():
assert recompute_source_item_status(0, 0, False) == 0
def test_in_stock_when_zero_threshold_and_backorders_enabled():
assert recompute_source_item_status(0, 0, True) == 1
def test_in_stock_when_negative_threshold_and_backorders_enabled():
assert recompute_source_item_status(-3, -2, True) == 1
def test_uses_normal_math_when_positive_threshold_even_with_backorders():
assert recompute_source_item_status(10, 5, True) == 1
assert recompute_source_item_status(3, 5, True) == 0
import { test } from "node:test";
import assert from "node:assert/strict";
import { recomputeSourceItemStatus } from "./repair-threshold-source-items.js";
test("in stock when quantity above positive threshold", () => {
assert.equal(recomputeSourceItemStatus(10, 5, false), 1);
});
test("out of stock when quantity at positive threshold", () => {
assert.equal(recomputeSourceItemStatus(5, 5, false), 0);
});
test("out of stock when quantity below positive threshold", () => {
assert.equal(recomputeSourceItemStatus(2, 5, false), 0);
});
test("out of stock when quantity zero and threshold zero, no backorders", () => {
assert.equal(recomputeSourceItemStatus(0, 0, false), 0);
});
test("in stock when zero threshold and backorders enabled", () => {
assert.equal(recomputeSourceItemStatus(0, 0, true), 1);
});
test("in stock when negative threshold and backorders enabled", () => {
assert.equal(recomputeSourceItemStatus(-3, -2, true), 1);
});
test("uses normal math when positive threshold even with backorders", () => {
assert.equal(recomputeSourceItemStatus(10, 5, true), 1);
assert.equal(recomputeSourceItemStatus(3, 5, true), 0);
});
Case studies
The clearance push that never showed up
A merchant lowered the Out-of-Stock Threshold from 5 to 0 to sell down leftover units before a season change. The legacy stock item flipped in stock immediately for everything with 1 to 4 units left, exactly as expected. But dozens of the same SKUs stayed marked out of stock in the MSI source item status, so the storefront still hid them and the salable quantity API reported zero even though units were sitting in the warehouse.
Running the repair script in dry run listed every source item where quantity now cleared the new threshold but status still said 0. A real run corrected just those rows, followed by the reindex and cron pass, and the clearance stock actually became purchasable the same day instead of quietly sitting unsold.
The buffer increase that left phantom stock live
A different store raised its threshold from 0 to 10 after a run of oversold orders on thin margins. The legacy flag correctly flipped several products to out of stock right away. Their MSI source items, untouched by the config save, still carried status 1 from before, so those exact SKUs kept accepting orders through the storefront and MSI's reservation system for two more days until a routine cron pass happened to recompute them.
After that, the team ran the detection and repair script immediately after every threshold change instead of waiting on the next scheduled reindex, closing the gap down to minutes instead of days.
After this runs right after a threshold change, every source item's status matches what the new threshold, quantity, and backorders setting actually say, not whatever the old threshold left behind. The admin grid, the storefront, and the salable quantity API agree again, and the only follow-up left is the CLI reindex and cron pass that pulls the legacy stock item into line with the source items this script already corrected.
FAQ
Why does changing the Out-of-Stock Threshold not update my existing products?
Saving a new threshold in Stores, Configuration, Catalog, Inventory, Product Stock Options fires an observer that recalculates the legacy cataloginventory_stock_item row so is_in_stock reflects the new threshold right away. MSI has no matching observer for inventory_source_item, so every existing source item keeps whichever status value was computed under the old threshold until something else touches it.
How do I find the source items the threshold change missed?
Page through GET /rest/V1/products, then for each SKU call GET /rest/V1/inventory/source-items filtered by sku to read every source item's quantity and stored status. Recompute what that status should be under the current threshold and backorders setting with a pure function, and flag any source item whose stored status disagrees with the recomputed value.
Is it safe to fix the mismatch through the REST API?
Yes, when the fix only overwrites the status field on the exact source items the script flagged, leaving quantity untouched, and stays behind a DRY_RUN guard that defaults to true. The PUT to /rest/V1/inventory/source-items is a REST-safe write, but the reindex and cron run that make cataloginventory_stock_item and the salable quantity index reflect the correction are CLI-only, so the script reports those as a required follow-up rather than pretending it can trigger them.
Related field notes
Citations
On the problem:
- GitHub Issue: Products stock status issues with Out-of-Stock Threshold. github.com/magento/inventory/issues/3061
- MSI FAQs, magento/inventory Wiki. github.com/magento/inventory/wiki/MSI-FAQs
- Adobe Commerce: Configure Inventory Management product options. experienceleague.adobe.com/en/docs/commerce-admin/inventory/configuration/product-options
On the solution:
- Adobe Commerce REST API: Manage source items. developer.adobe.com/commerce/webapi/rest/inventory/manage-source-items
- Adobe Commerce REST API: Check salable quantities. developer.adobe.com/commerce/webapi/rest/inventory/check-salable-quantity
- Commerce PHP Extensions: InventoryApi module reference. developer.adobe.com/commerce/php/module-reference/module-inventory-api
Stuck on a tricky one?
If you have a problem in Magento 2 or Adobe Commerce inventory, catalog data, orders, or indexing that you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this clear up your stock mismatch?
If this saved you a confusing inventory report or a threshold change that never seemed to take, you can buy me a coffee. It is the best way to keep these field notes free and growing.
Buy me a coffee on Ko-fi