Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

shopify-inventory-sync-cli

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.

Example

$ 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.)

Features

  • 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. sync fetches 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-run computes 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 available or on_hand, with a real Shopify reason code per sync (--reason, defaults to correction) and an optional --reference-uri for your own audit trail, both fed straight into inventorySetQuantities.
  • Cost-aware retry on HTTP 429 and GraphQL THROTTLED errors, so a large catalog sync doesn't die halfway.
  • Zero runtime dependencies. Node's built-in fetch and test runner only.
  • Usable as a library — fetchAllVariantLevels, computeDiff and runSync are exported for your own pipeline.

Installation

npx shopify-inventory-sync-cli --help
# or
npm install -g shopify-inventory-sync-cli

The 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+.

Getting an access token

  1. In your store admin: Settings → Apps and sales channels → Develop apps → Create an app.
  2. Under Configuration → Admin API integration, grant read_products, read_locations, read_inventory, and write_inventory (only needed for sync; locations and export are read-only).
  3. Install the app and copy the Admin API access token (shpat_...).
  4. Export it together with your store domain:
export SHOPIFY_STORE_DOMAIN=my-store.myshopify.com
export SHOPIFY_ADMIN_ACCESS_TOKEN=shpat_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Usage

isc 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 received

As a library

const { 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}`);
}

What it does NOT do

  • No compare-and-set concurrency guard. Every sync sends ignoreCompareQuantity: true and absolute quantities — exactly what Shopify's own inventorySetQuantities docs 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, a sync run 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 available or on_hand. It cannot set committed, incoming, reserved, damaged, safety_stock or quality_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 from export and unmatchable in sync — rare, but worth knowing if you run a very large location count.
  • No location creation, activation or deactivation. isc locations is read-only; manage locations in the admin or with locationActivate/locationAdd directly.
  • No product or variant creation. A SKU has to already exist in the store — sync reports "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 for inventorySetQuantities' quantities input 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-run on a development store first.

FAQ

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.

Related Shopify tools

Contributing

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.

Roadmap

  • isc sync --format json for scripting around the diff instead of parsing stdout.
  • A --location filter so export/sync can scope to a subset of locations without editing the query string.
  • Optional CSV output for the markdown report's zero-stock/oversold lists.

License

MIT © Ecom Swift LLC

Need help?

This project is maintained by Ecom Swift LLC, a Shopify Partner.


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.

About

Export Shopify inventory to CSV and sync it back with a diff-based, idempotent apply against the Admin GraphQL API. Zero dependencies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages