Export Shopify inventory levels to a CSV, edit them, and sync them back with a diff-based, idempotent apply against the Admin GraphQL API — one CLI, zero dependencies.
We built this because every multi-location store eventually needs to push inventory from somewhere else — a warehouse spreadsheet, a stocktake, a 3PL export — back into Shopify, and the usual options are a manual admin edit per SKU or a full CSV inventory app subscription. isc reads the store's current levels, diffs them against your CSV, and only sends the rows that actually changed.
Free audit for your own store while you're here: audit.ecomswiftllc.com checks SEO, speed, CRO and AI-visibility in one pass.
$ export SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
$ export SHOPIFY_ADMIN_ACCESS_TOKEN=shpat_xxx
$ isc locations
id,name,isActive
gid://shopify/Location/1,Main Warehouse,true
gid://shopify/Location/2,Downtown Store,true
$ isc export --tracked-only --out inventory.csv
Wrote inventory.csv
128 sku/location row(s) across 64 variant(s).
$ isc sync inventory-target.csv --dry-run
4 change(s), 11 already match, 0 skipped.
sku,location,current,target,delta
WIDGET-RED-S,Main Warehouse,38,42,+4
WIDGET-RED-M,Main Warehouse,20,17,-3
WIDGET-BLUE-S,Main Warehouse,2,0,-2
WIDGET-RED-S,Downtown Store,0,5,+5
Dry run: 4 change(s) in 1 batch(es) would be written for "available". Nothing was sent.
$ isc sync inventory-target.csv
4 change(s), 11 already match, 0 skipped.
Applied 4 of 4 change(s) in 1 batch(es).
(Illustrative output — your numbers will differ.)
- Export to CSV, JSON or a markdown report. The report format (
--format md) flags out-of-stock (available = 0) and oversold (available < 0) SKU/location pairs without needing a spreadsheet. - Diff-based sync, not blind overwrite.
syncfetches the store's current levels first and only sends rows whose quantity actually differs from your CSV — unchanged rows are skipped, and the diff is printed before anything is written. --dry-runcomputes and prints the full diff (current, target, delta per row) without calling the write mutation.- Idempotent by design. Because sync only ever sends what still differs, re-running it after a partial failure or a network error is always safe — rows that already match your CSV are simply left out of the next diff.
- Every bad row is reported with its CSV line number — unknown SKU/location pairs, non-integer or negative quantities, missing columns — and nothing is sent until you've seen the full list.
- Set
availableoron_hand, with a real Shopifyreasoncode per sync (--reason, defaults tocorrection) and an optional--reference-urifor your own audit trail, both fed straight intoinventorySetQuantities. - Cost-aware retry on HTTP 429 and GraphQL
THROTTLEDerrors, so a large catalog sync doesn't die halfway. - Zero runtime dependencies. Node's built-in
fetchand test runner only. - Usable as a library —
fetchAllVariantLevels,computeDiffandrunSyncare exported for your own pipeline.
npx shopify-inventory-sync-cli --help
# or
npm install -g shopify-inventory-sync-cliThe published npm package is not live yet; until it is, clone this repo and run node bin/isc.js, or npm link inside it to get the isc command.
Requires Node.js 18+.
- In your store admin: Settings → Apps and sales channels → Develop apps → Create an app.
- Under Configuration → Admin API integration, grant
read_products,read_locations,read_inventory, andwrite_inventory(only needed forsync;locationsandexportare read-only). - Install the app and copy the Admin API access token (
shpat_...). - Export it together with your store domain:
export SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
export SHOPIFY_ADMIN_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxxisc locations List locations (id, name, active)
isc export Export current sku/location inventory to CSV/JSON/markdown report
isc sync <file.csv> Diff a target-state CSV against the store and apply the changes
| Flag | Applies to | Meaning |
|---|---|---|
--query <search> |
export |
Shopify search syntax, e.g. status:active, sku:WIDGET-* |
--limit <n> |
export |
stop after n variants |
--tracked-only |
export |
skip variants that are not inventory-tracked |
--format <fmt> |
locations, export |
csv/json (export also takes md for the out-of-stock/oversold report) |
--out <file> |
locations, export |
write to a file instead of stdout |
--name <available|on_hand> |
sync |
which quantity the CSV sets (default available) |
--reason <reason> |
sync |
one of the Shopify reason codes below (default correction) |
--reference-uri <uri> |
sync |
optional audit-trail URI stored on the adjustment |
--batch-size <n> |
sync |
rows per inventorySetQuantities call (default 100) |
--dry-run |
sync |
compute and print the diff, send nothing |
Valid --reason values: correction, cycle_count_available, damaged, movement_created, movement_updated, movement_received, movement_canceled, other, promotion, quality_control, received, reservation_created, reservation_deleted, reservation_updated, restock, safety_stock, shrinkage — the exact list Shopify's Admin API accepts for inventorySetQuantities.
The sync CSV needs exactly these headers: sku,location,quantity. location must match a location's name exactly as isc locations or isc export prints it (case-insensitive). A ready-to-edit example lives in examples/inventory-target.csv.
# See what a stocktake spreadsheet would change, before touching anything
isc sync stocktake.csv --dry-run
# Apply it, with a reason that shows up in the Shopify admin's inventory history
isc sync stocktake.csv --reason cycle_count_available
# Sync on-hand counts instead of available
isc sync received-shipment.csv --name on_hand --reason receivedconst { ShopifyGraphQLClient, fetchAllVariantLevels, rowsFromVariants, parseTargetCsv, computeDiff, runSync } = require('shopify-inventory-sync-cli');
const client = new ShopifyGraphQLClient({
shop: process.env.SHOPIFY_STORE_DOMAIN,
accessToken: process.env.SHOPIFY_ADMIN_ACCESS_TOKEN,
});
const variants = await fetchAllVariantLevels(client, { query: 'status:active' });
const currentRows = rowsFromVariants(variants);
const targetRows = parseTargetCsv(require('fs').readFileSync('stocktake.csv', 'utf8'));
const { diff, problems } = computeDiff(targetRows, currentRows, { name: 'available' });
if (problems.length === 0) {
const result = await runSync(client, diff, { reason: 'cycle_count_available' });
console.log(`Applied ${result.applied} of ${result.attempted}`);
}- No compare-and-set concurrency guard. Every sync sends
ignoreCompareQuantity: trueand absolute quantities — exactly what Shopify's owninventorySetQuantitiesdocs recommend for "a system that acts as the source of truth for inventory quantities," which is this tool's whole premise. If another system (POS, a second app) also writes inventory concurrently, asyncrun can overwrite its change. Don't point this at a store where something else is the source of truth for the SKUs you're syncing. - It only sets
availableoron_hand. It cannot setcommitted,incoming,reserved,damaged,safety_stockorquality_control— those are derived or managed through orders, transfers and other flows, not this mutation. - Reads at most 50 locations' inventory levels per variant. The query asks for
inventoryLevels(first: 50). A SKU stocked at more than 50 locations will have the rest silently missing fromexportand unmatchable insync— rare, but worth knowing if you run a very large location count. - No location creation, activation or deactivation.
isc locationsis read-only; manage locations in the admin or withlocationActivate/locationAdddirectly. - No product or variant creation. A SKU has to already exist in the store —
syncreports "no tracked SKU" for anything it can't find, it does not create it. --batch-size(default 100) is a conservative default, not a documented Shopify hard limit — no maximum array size forinventorySetQuantities'quantitiesinput is published as of the 2025-10 Admin API. Raise it if your store handles larger calls comfortably; lower it if you see timeouts.- Not run against a live store by us in CI. All three GraphQL operations were validated against the live 2025-10 Admin schema, and the 19 tests exercise pagination, CSV parsing, diffing and batching against stubbed responses. Try
--dry-runon a development store first.
Why not just edit inventory in the admin? For a handful of SKUs, the admin is fine. This is for stocktakes, warehouse spreadsheets, or any workflow where the source of truth for quantities lives outside Shopify and needs to land in it in one pass.
Is sync safe to re-run?
Yes — that's the point of diffing first. If a run fails partway (network error, a rejected batch), just run it again; rows that already match are excluded from the next diff automatically.
What happens if my CSV has a typo in a location name?
sync reports it as "no tracked SKU ... at location ..." with the CSV line number, and sends nothing for that row. Nothing else in the file is blocked by it.
Can I sync both available and on_hand in one pass?
Not in one command — --name picks one per run, matching the mutation's own shape (name is a single field per call). Run sync twice with two CSVs (or two columns and two invocations) if you need both.
Will a big catalog hit the rate limit?
Requests retry on HTTP 429 and THROTTLED errors with a backoff, so a large sync takes longer rather than failing outright.
Inventory drift between a warehouse system and Shopify is one of the more common causes of overselling and support tickets we see. If you'd like a second pair of eyes on your store beyond inventory, our free Shopify tools and the store audit are a good starting point.
shopify-order-export-cli— export Shopify orders to CSV/JSON and get a revenue and customer report.shopify-metafields-manager— export, bulk-import and audit Shopify metafields.shopify-webhook-toolkit— verify, log and replay Shopify webhooks locally.shopify-api-python-client— a zero-dependency Python client for the Admin REST and GraphQL APIs.shopify-store-audit-toolkit— CLI that audits a live store's SEO, structured data and performance signals.
Issues and PRs welcome — especially a --diff output format for spreadsheet review, location creation from a CSV of names, and support for setting available and on_hand together in one CSV.
isc sync --format jsonfor scripting around the diff instead of parsing stdout.- A
--locationfilter soexport/synccan scope to a subset of locations without editing the query string. - Optional CSV output for the markdown report's zero-stock/oversold lists.
MIT © Ecom Swift LLC
This project is maintained by Ecom Swift LLC, a Shopify Partner.
- 🛍️ Shopify Partner Directory: https://www.shopify.com/partners/directory/partner/waowy
- ✉️ Email: support@ecomswiftllc.com
- 💬 WhatsApp: https://wa.me/16312511767
Want inventory drift, overselling and stock accuracy checked as part of a full pass on your store? Get a free store audit (SEO, speed, CRO, AI visibility) or browse our other free Shopify tools. We're Ecom Swift LLC, a Shopify Partner Agency — www.ecomswiftllc.com.