In short: A Magento 2 product can hold stock at a source and still show a salable quantity of 0, because the website’s stock has no source that holds it or because the inventory index is stale. Resolve the website’s stock, list its sources, then check the index.
A support ticket says a product is in stock, but customers on one website cannot buy it. A screenshot from the warehouse source shows twelve units. The salable quantity on the website says 0, and a requested-quantity check returns "The requested sku is not assigned to given stock."
Twelve units at a source and zero salable on a website are not a contradiction. They answer two different questions, and the fix depends on which link between them is missing. This guide shows how to find that link with a few read-only checks, what each failure looks like in the API responses, and what to confirm before you change an assignment.
The checks use the Inventory Management (MSI) modules that ship with Magento Open Source and Adobe Commerce. A store without those modules needs a different diagnostic path.
Why can a source hold 12 units while the website can sell none?
Because the website does not read sources. A source is a physical location where inventory is kept, and a stock is a virtual, aggregated inventory built from the sources assigned to it[1]. A website, as a sales channel, reads exactly one stock: each sales channel can have only a single stock assigned to it[2]. A source can be reused in more than one stock[1].
So the units reach the storefront only through a chain: source item, then source, then the stock the source is assigned to, then the website that stock serves. A break anywhere in that chain gives a salable quantity of 0 for that website, whatever the source screen says.
The number the website uses is computed from the stock's index. In the Magento source, the salable quantity is the indexed quantity for that SKU and stock, plus the reservations recorded against that stock, minus the out-of-stock threshold. It returns 0 when the stock has no index row for the SKU or the row is not marked salable[9].

How do you check which stock the website reads?
Start with the website, not with the product. Three read-only REST calls, the same ones Adobe documents for checking salable quantities, answer the first questions[3]:
GET /V1/inventory/stock-resolver/website/:websiteCode
GET /V1/inventory/get-product-salable-quantity/:sku/:stockId
GET /V1/inventory/is-product-salable-for-requested-qty/:sku/:stockId/:requestedQty
These are route templates. Add your REST base path and an integration token with access to the inventory resources. Resolve the website code to a stock ID first, then use that ID in the other two calls. Do not assume stock 1: that is the Default Stock, and your website may read a different one.
Keep the response of the third call as it is. Its errors list carries a code and a message, and those codes separate the causes in the table further down.
What to put in the ticket so the next person does not repeat the work:
- the website code and the stock ID it resolves to;
- the SKU, and for a configurable product the selected child SKU;
- the output of the three calls, with the time of each;
- the state of the inventory indexer (
bin/);magento indexer:status inventory - the sources assigned to that stock.
What did a test show? (field note)
In a test on Magento Open Source 2.4.9 with the inventory modules, the ticket above was reproduced. A website with code uk read a stock called "UK Stock" (ID 3). A source warehouse_a was enabled and held 12 units of a simple product, MUG-BLUE, with status In Stock. It was assigned only to a different stock, "EU Stock" (ID 2). UK Stock had no sources at all.
The checks returned this:
| Check | Result |
|---|---|
Stock resolver for website uk |
Stock 3, "UK Stock" |
Salable quantity, MUG-BLUE, stock 3 |
0 |
| Requested quantity 1, stock 3 | {"salable":false,"errors":[{"code":"requested- |
| Sources assigned to stock 3 | [] |
Storefront, uk store view |
The product query returned no items, and adding the SKU to a cart returned PRODUCT_NOT_FOUND |
The source screen was telling the truth: twelve units existed. They were simply in a stock that no website read. The error message names the cause directly: the stock the website reads has no row for this SKU.
How do you find the missing link?
Read the assignments from both ends. Both calls below are read-only. Replace the filter values with your own source code and stock ID:
GET /V1/inventory/get-sources-assigned-to-stock-ordered-by-priority/3
GET /V1/inventory/stock-source-links?searchCriteria[filterGroups][0][filters][0][field]=source_code&searchCriteria[filterGroups][0][filters][0][value]=warehouse_a
In the test, the first call returned an empty list. The second returned one link, for stock 2, which is the evidence that the source was assigned somewhere other than the stock the website reads.
If you have read access to the database, three queries give the same picture in one place. Add your table prefix if your installation uses one:
SELECT sc.code AS website, sc.stock_id, s.name AS stock
FROM inventory_stock_sales_channel sc
JOIN inventory_stock s ON s.stock_id = sc.stock_id
WHERE sc.type = 'website';
SELECT l.stock_id, l.source_code
FROM inventory_source_stock_link l
ORDER BY l.stock_id, l.priority;
SELECT source_code, quantity, status
FROM inventory_source_item
WHERE sku = 'MUG-BLUE';
In the test, the first query listed uk with stock 3. The second listed the default source on stock 1 and warehouse_a on stock 2, and nothing on stock 3. The third listed warehouse_a with 12 units and status 1 (In Stock), plus a default-source row with quantity 0 that is not the source in question. Read together, they show the break: the website reads stock 3, and no source with this SKU is assigned to stock 3.
Why does the number stay 0 after you assign the source?
Because saving a stock-source assignment does not rebuild the stock index; it marks it out of date. In the Magento source, the plugin that runs after the links are saved invalidates the inventory indexer if it is currently valid[10]. In Adobe's index management page, the Reindex Required status means a change needs reindexing but the indexers cannot be updated automatically[8]. The CLI documentation describes an invalid indexer as prompting reindexing on the next cron run[7].
In the test, we assigned warehouse_a to stock 3 and checked again straight away:
bin/showed Reindex required.magento indexer:status inventory - The salable quantity was still
0. - The requested-quantity check now returned three entries, including
is_with the message "The requested sku is not assigned to given stock". The text is the same as before, but the cause is different: the assignment exists and the index has no row yet.salable_ with_ reservations- no_ data - The storefront still returned no product.
Then we ran either of these, and each fixed it:
bin/magento indexer:reindex inventory
or, with cron configured, the index cron group (bin/, run twice because the first run only schedules the jobs). Magento's index group runs the job indexer_ every minute[12]. After either, the salable quantity was 12, the check returned {"salable":true,"errors":[]}, the product query returned the product as IN_STOCK, and one unit went into a cart.
On a store where cron is not running, an assignment can look broken for as long as nobody rebuilds the index. On a large catalogue, choose a quiet moment for a manual reindex.
What else gives a salable quantity of 0 when the source has stock?
The same three calls separate the common causes. Each row below was reproduced in the same test, on the same product:
| Cause | What the checks showed | Where to look |
|---|---|---|
| Source not assigned to the website's stock | Salable quantity 0; code requested-; no sources listed for the stock |
The stock's assigned sources |
| Stock has a source, but the source holds no row for the SKU | Salable quantity 0 and the same requested-quantity error, but the stock's source list is not empty |
The SKU's source items |
| Assignment saved, index not rebuilt | Salable 0; is_; indexer shows Reindex required |
indexer:status |
| Source disabled | The requested-quantity check failed at once with "There are no source items with the in stock status", while the salable quantity still read 12 and the product query still listed the product, until the index was rebuilt; then 0 and no product |
The source's Enabled flag[5] |
| Source item status set to Out of Stock, quantity still 12 | Salable 0; same message as a disabled source; product gone from the query |
The source item's status |
| Out-of-stock threshold of 12 with 12 units | Salable 0; "Not enough items for sale"; the product query lagged until the index was rebuilt |
Product options, Out-of-Stock Threshold[4] |
| Product not assigned to the website | Inventory calls looked healthy (salable 12, "salable":true), but the storefront could not find the product |
The product's websites, not inventory |
| Open order for 5 of the 12 units | Salable 7 |
Reservations[6]; this is expected |
Two rows deserve a second look. The disabled source shows that the salable quantity can lag behind the requested-quantity check, so a stale figure on a product grid does not always mean a stale answer at checkout. The product-not-on-the-website row is the reverse: every inventory call says yes while the storefront says no, which is the cue to stop looking at inventory.
If the requested-quantity check succeeds for the quantity the customer wants but the storefront still refuses the product, check the product's website assignment, status, visibility and cart quantity limits.
What should you check before you change an assignment?
Assigning a source to a stock changes more than the SKU in the ticket.
- Every product the source holds. A stock is built from all the sources assigned to it[1], so assigning
warehouse_achanges its contribution to UK Stock for every SKU it holds, not only the mug. Whether each product is then purchasable still depends on indexing, website assignment and product settings. - Whether the units may be shared. A source can feed several stocks[1], and reservations are recorded against a stock ID. If two websites sell the same twelve units from two stocks, each stock counts its own orders. Confirm with whoever owns the inventory that sharing is intended, and not a leftover from a migration.
- Reassigning a website. If the fix is to point the website at a different stock instead, note that a new stock assigned to a sales channel unassigns the pre-existing one[2]. Open orders were placed against the old stock. This test did not cover that case, so plan it on a copy.
- Who changed it, and when. Adobe Commerce has an Admin Actions Log that records changes made by admin users[11]. Magento Open Source does not include it, and a missing entry does not prove nobody changed the assignment. A deployment record or a setup ticket is better evidence.
A stock feed that writes source items fills the source and nothing else. In the test, the 12 units were in place from the start while the assignment was missing, so a feed can be working correctly while a stock stays empty.
How do you confirm the fix worked?
Repeat the same checks on the same website, in this order:
- Resolve the website's stock again and confirm the ID.
- Confirm the inventory indexer is Ready, not Reindex required.
- Check the salable quantity and the requested-quantity response for the quantity the customer wanted.
- Add the product to a cart on that website, using a test procedure that does not create a real order.
Record the responses next to the original ones in the ticket. Treat the ticket as closed only when the storefront behaves the same way as the API.
References
Numbers in square brackets in the text link to these sources.
- Adobe, "Stocks and sources" (opens in a new tab).Used for: what a source and a stock are, and that sources can be reused.
- Adobe, "Manage stock" (opens in a new tab).Used for: one stock per sales channel, and a new assignment replacing the old one.
- Adobe, "Check salable quantities" (opens in a new tab).Used for: the stock resolver, salable quantity and requested-quantity routes.
- Adobe, "Configure Inventory Management product options" (opens in a new tab).Used for: the out-of-stock threshold, Manage Stock and backorders.
- Adobe, "Manage inventory sources" (opens in a new tab).Used for: the source's Enabled flag.
- Adobe Commerce developer documentation, "Reservations" (opens in a new tab).Used for: reservations tracking the salable quantity of each product in a stock.
- Adobe, "Manage the indexers" (opens in a new tab).Used for: the reindex and status commands and what an invalid indexer triggers.
- Adobe, "Index management" (opens in a new tab).Used for: the Ready and Reindex Required statuses.
- magento/inventory,
GetProductSalableQty.php(opens in a new tab).Used for: how the salable quantity is calculated from the index, reservations and threshold. - magento/inventory,
InvalidateAfterStockSourceLinksSavePlugin.(opens in a new tab).Used for: saving stock-source links invalidating the inventory indexer.php - Adobe, "Action logs" (opens in a new tab).Used for: the Admin Actions Log being available in Adobe Commerce only.
- magento/magento2, Indexer
crontab.xml(opens in a new tab).Used for: the indexer_reindex_all_invalid job running every minute.