Everything below was checked against Shopify's docs for Admin GraphQL API version 2026-10 on 15 October 2026.
What changed in 2026
Two things, both breaking from version 2026-04 onwards:
compareQuantityandignoreCompareQuantityare gone. They were replaced bychangeFromQuantity, which arrived in 2026-01 and became required at runtime in 2026-04. The schema still shows it as optional, but leave it out and the call fails.- An idempotency key is now required. Shopify added the
@idempotentdirective as optional in 2026-01 and made it mandatory for the inventory mutations in 2026-04.
If you have an older integration pinned to 2025-10 or earlier, it will keep working until that version is retired. When you move it forward, these are the two things to change.
The input, field by field
The mutation takes one argument, input, of type InventorySetQuantitiesInput:
name(required): which quantity you're setting. Onlyavailableoron_handare accepted.reason(required): why. Shopify's list includescorrection,cycle_count_available,received,restock,damaged,shrinkageand a few others. For an ERP sync,correctionis the usual choice.referenceDocumentUri(optional): a URI pointing at whatever caused the change. Shopify prefers a GID-style format such asgid://my-erp/StockTake/1234. Fill it in. When someone asks in six months why the stock jumped, this is how you'll find out.quantities(required): a list ofInventoryQuantityInput, each withinventoryItemId,locationId,quantityandchangeFromQuantity.
changeFromQuantity, in plain English
This is a compare-and-swap check. You tell Shopify what you think the quantity is right now. If it matches, Shopify sets the new value. If something else changed it in the meantime (a sale, a return, another app), the mutation fails with CHANGE_FROM_QUANTITY_STALE, and you re-read and try again.
If your system really is the only source of truth and you just want Shopify to match it, pass null. That skips the check. But you have to pass it explicitly: leaving the field out is an error.
A working mutation
mutation SetStock($input: InventorySetQuantitiesInput!, $key: String!) {
inventorySetQuantities(input: $input) @idempotent(key: $key) {
inventoryAdjustmentGroup {
reason
referenceDocumentUri
changes { name delta quantityAfterChange }
}
userErrors { code field message }
}
}
{
"key": "6f1c2b8e-3d0a-4a57-9a43-1b2f7e5d9c10",
"input": {
"name": "available",
"reason": "correction",
"referenceDocumentUri": "gid://my-erp/StockSync/2026-10-15T09:00",
"quantities": [
{
"inventoryItemId": "gid://shopify/InventoryItem/30322695",
"locationId": "gid://shopify/Location/124656943",
"quantity": 42,
"changeFromQuantity": 40
}
]
}
}
You'll need the write_inventory scope. Always ask for userErrors back: a call can return HTTP 200 and still have done nothing.
Getting the IDs
The mutation wants inventory item IDs, not variant IDs. Every variant has exactly one inventory item, so if your ERP keys on SKU, look the variant up and ask for its inventory item:
query {
productVariants(first: 50, query: "sku:TSHIRT-RED-M") {
nodes { id sku inventoryItem { id } }
}
}
Location IDs come from the locations query. Fetch them once and store them against your warehouse codes. They don't change, so there's no point asking every time.
query {
locations(first: 20) {
nodes { id name }
}
}
Do the same with the SKU to inventory item mapping: build it once, keep it in your own database, and refresh it when products change. Looking up every SKU on every sync is the quickest way to hit the rate limit.
Race conditions and duplicates
This is where stock syncs usually go wrong. Two problems, two tools.
Somebody else changed the number. Your ERP reads 40, a customer buys one, Shopify now has 39, and your sync sets 42. With changeFromQuantity: null you've just overwritten a sale. With changeFromQuantity: 40 the call fails and you can deal with it. Which is right depends on who owns the number. If the ERP already knows about Shopify orders, null is fine. If it doesn't, use the check.
The same request runs twice. A timeout, a retry, a queue that delivers twice. That's what the idempotency key is for. Generate a UUID per logical change and reuse it when you retry that change, so Shopify only applies it once. A new key on every retry defeats the point. We've written more about this in Shopify idempotency. Test the behaviour on a development store before you rely on it.
Rate limits and batching
The Admin GraphQL API uses a points bucket rather than a request count. The docs' example shows 1,000 points available and 50 restored per second, and every response tells you where you stand under extensions.cost.throttleStatus. Go over and you get a THROTTLED error, which is Shopify's version of a 429.
Batch several items into one call rather than sending one call per SKU. Shopify doesn't publish a maximum number of quantities per call, so start modestly (say 50 to 100), watch the cost figures, and adjust. Then make your client back off and retry when it sees THROTTLED, rather than hammering the API.
A small PHP example
<?php
function shopify_set_stock($shop, $token, array $rows, $key)
{
$query = 'mutation SetStock($input: InventorySetQuantitiesInput!, $key: String!) {
inventorySetQuantities(input: $input) @idempotent(key: $key) {
userErrors { code field message }
}
}';
$quantities = [];
foreach ($rows as $r) {
$quantities[] = [
'inventoryItemId' => $r['inventory_item_gid'],
'locationId' => $r['location_gid'],
'quantity' => (int) $r['qty'],
'changeFromQuantity' => null, // ERP is the source of truth
];
}
$body = json_encode(['query' => $query, 'variables' => [
'key' => $key,
'input' => ['name' => 'available', 'reason' => 'correction', 'quantities' => $quantities],
]]);
$ch = curl_init("https://$shop/admin/api/2026-10/graphql.json");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-Shopify-Access-Token: $token"],
]);
$res = json_decode(curl_exec($ch), true);
return $res['data']['inventorySetQuantities']['userErrors'] ?? ($res['errors'] ?? []);
}
An empty array back means it worked. Anything else, log it with the reference URI and the key so you can retry the same change safely.
Common errors
- Missing changeFromQuantity: you're on 2026-04 or later and left the field out. Pass the expected number, or
null. CHANGE_FROM_QUANTITY_STALE: the stock moved since you read it. Re-read, then decide whether to retry.- Unknown field compareQuantity: old code on a new API version. Swap it for
changeFromQuantity. - Missing idempotency key: add
@idempotent(key: ...)to the mutation. - Wrong ID type: a variant ID where an inventory item ID belongs, or a numeric ID without the
gid://shopify/...prefix. - Item not stocked at that location: if the item has never been stocked there, activate it at that location before setting a quantity.
Need a hand with it?
We build stock sync between Shopify and ERP or warehouse systems, from NetSuite and Shopify integrations to ShopFlow, our own stock and order system for Shopify. If yours has started throwing errors since Shopify's 2026 changes, or you're not sure it's safe to move off an old API version, get in touch and we'll take a look.
Cheers, Rob